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_rightPost-Discharge Vitals Relay Hub

Post-Discharge Vitals Relay Hub

Post-Discharge Vitals Relay Hub 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/post_discharge_vitals_hub/ so it can learn how the device works and help you adapt it to your hardware.

This project is a remote patient monitoring hub for 30–60-day post-discharge recovery programs. An nRF52840 host with native Bluetooth Low Energy, paired with a Blues Notecard Cell+WiFi, scans for the patient's BLE-enabled medical devices (weight scale, blood pressure cuff, pulse oximeter, and activity band). It relays each completed reading over cellular to the Blues Notehub cloud service. When a reading exceeds a configurable clinical threshold, the hub syncs it immediately, so the care team gets the alert without waiting for the next scheduled upload. The patient plugs it in and it works, with no WiFi, no app to configure, and no network credentials to enter.

1. Project Overview

warning

⚠️ Proof-of-concept only, not a medical device. This is a reference design for technical evaluation and developer education. It is not a cleared or certified medical device, it is not validated for diagnostic use, and it must not be used for emergency response or life-sustaining patient monitoring. Any deployment that collects, stores, or routes patient health data must address patient-data governance (including HIPAA/PHI compliance where applicable), secure data routing and storage, data retention and auditability requirements, and all applicable regulatory obligations. Consult qualified clinical, legal, and compliance teams before you collect real patient data.

The problem. Hospital discharge coordinators who manage heart failure, COPD (chronic obstructive pulmonary disease), post-surgical, and high-risk diabetic patients are sending more and more of them home with a pack of BLE-enabled monitoring devices. There's a connected scale to catch fluid retention, a BP (blood pressure) cuff to watch for hypertension spikes, a pulse oximeter to flag respiratory deterioration, and an activity band to monitor resting heart rate. The clinical evidence for remote patient monitoring in this window is strong. Daily weight checks and BP readings reduce 30-day readmissions, and early intervention on a trending SpO2 (blood oxygen saturation) drop catches pneumonia or PE (pulmonary embolism) while it's still outpatient-treatable.

The weak link is the hub. Existing RPM programs ship either a dedicated tablet the patient has to set up or a companion phone app the patient has to install, and both break constantly. The tablet needs a WiFi network that may not exist, or one with a password the patient can't find. The phone app needs a compatible phone and a patient who's comfortable installing software. In practice, a sizable share of enrolled patients never get a single reading uploaded in their first week, just because the connectivity layer fails them.

Why Notecard. Put a cellular radio in the hub and every one of those failure modes goes away. The hub doesn't need the patient's WiFi password or a companion app, and nobody in IT has to add it to the guest network. It ships pre-provisioned with a global SIM, so it's online the moment the patient plugs it into a wall outlet. That zero-touch deployment model is what cellular Notecard was built for. A device like this has to work for everyone, including an 82-year-old heart failure patient who has never configured a router. The hub is the one piece of the care program that the program can't afford to have the patient debug. Cellular removes that failure mode, and Notecard delivers the cellular connection with a SIM, an antenna, and a two-line I2C interface, so the host never deals with modem AT commands, socket management, or a session state machine.

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. A small enclosure, about the size of a phone charger, gets mailed to the patient at discharge along with the BLE device pack. The patient plugs the USB-C cable into the included charger, sets the hub on the nightstand, and forgets about it. The BLE devices use standard Bluetooth SIG health profiles (Weight Scale Service, Blood Pressure Service, Pulse Oximeter Service, Heart Rate Service). Indication-based devices (weight scale, blood pressure cuff, pulse oximeter) disconnect on their own after sending one measurement, and the hub goes straight back to scanning. The full connection cycle typically takes 2–5 seconds. Heart Rate Service devices like the Polar H10 work differently. They use Notification instead of Indication and stream samples continuously while they're worn. The firmware handles that by disconnecting after the first HR notification and then suppressing reconnection for 15 minutes (HR_SAMPLE_INTERVAL_MS), so a patient who wears the band for two hours produces a bounded set of HR Notes instead of a continuous stream. The care team sees readings in Notehub, routed to whatever RPM dashboard or EHR integration the program uses, and the patient doesn't have to do anything after plugging the hub in.

Device identity model. The hub is fail-closed by default. It only connects to BLE-bonded devices (the primary gate) or to devices whose MAC address appears in the allow-list in vitals_config.h (a secondary gate for stable-address devices). The primary gate relies on IRK resolution in the nRF52840 SoftDevice. When a bonded device advertises with a Resolvable Private Address, the SoftDevice resolves it with the stored IRK and sets addr_id_peer=1 in the scan report before the application layer sees it. Bond keys are written to flash automatically and loaded at every boot. For initial commissioning, build with both ALLOW_UNENROLLED_DEVICES_FOR_DEV=1 and ALLOW_COMMISSIONING_BUILD (the two flags are required together) so the hub accepts connections from unrecognized devices and pairing can happen. After commissioning, add any public/static-address devices to ENROLLED_DEVICES in vitals_config.h, then rebuild with both flags removed. The Commission step in §3 Technical Summary walks through the complete enrollment flow.

2. System Architecture

System architecture: BLE patient devices (weight scale, BP cuff, pulse oximeter, activity band) → Feather nRF52840 BLE Central on Notecarrier F with Notecard MBGLW → cellular (LTE Cat-1 bis) → Notehub → care-coordinator alert route and analytics / EHR route

Device-side responsibilities. The hub the patient plugged in is doing two jobs at once. The Adafruit Feather nRF52840 Express runs Nordic's certified SoftDevice as a BLE Central. It scans for advertisements that match a target service UUID, connects to the first one it sees, and subscribes to the right GATT characteristic (indication for weight, BP, and SpO2, and notification for heart rate). Then it decodes the raw bytes. Weight comes in as a fixed-point uint16, blood pressure and SpO2 as IEEE 11073 SFLOAT, and heart rate as a plain integer. The host hands the decoded reading across I²C to the Notecard through the Notecarrier F's Feather header, with note-arduino's JAdd* helpers building the JSON request. It never has to deal with a modem, an AT command, or a socket.

Notecard responsibilities. Once a reading lands in the Notecard's on-device queue, the radio takes over. The Notecard opens a cellular session on the outbound cadence configured with hub.set (15 minutes by default), and it sends any Note marked sync:true right away. In the other direction, it delivers environment variables pushed from Notehub. That lets a care coordinator tighten or loosen the alert thresholds for a specific patient without anyone touching the firmware.

Notehub responsibilities. Notehub ingests and stores every event and applies the project's routes to it. The four reading Notefiles (weight.qo, bp.qo, spo2.qo, activity.qo) stay separate from the alert Notefile (vitals_alert.qo) so each can go out at its own urgency. Readings go to a long-term analytics store, and alerts go to an on-call channel or EHR endpoint in near-real time. Fleets group devices by patient cohort or care program, and smart fleet rules automatically assign newly claimed devices to the right fleet based on serial number or provisioning tag.

Routing to the cloud (high level). Notehub can deliver to HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP Pub/Sub, and several other destinations. How you set up routes depends on your project, and this one doesn't include a specific downstream endpoint. The Notehub routing docs cover the options.

3. Technical Summary

Prerequisites before you start:

  • A Notehub account and project (to get your ProductUID)
  • The Arduino IDE with the Adafruit nRF52 board support package installed
  • USB-C and Micro-USB cables for power and firmware flashing
  • At least one test BLE health device (weight scale, BP cuff, pulse oximeter, or activity band) for commissioning

Here's the fastest path from parts on the bench to a first event in Notehub:

  1. Notehub: create a Notehub project and copy its ProductUID.
  2. Assemble: seat the nRF52840 Feather on the Notecarrier F, slot the Notecard into the M.2 connector, and put Mojo between a bench LiPo and the Notecarrier F's battery connector for bench validation.
  3. Edit one line: set PRODUCT_UID in firmware/post_discharge_vitals_hub/vitals_config.h. For the initial commissioning flash, also add both -DALLOW_UNENROLLED_DEVICES_FOR_DEV=1 and -DALLOW_COMMISSIONING_BUILD to your build flags. The two are required together, and neither one may appear in the shipping build.
  4. Flash: select Adafruit Feather nRF52840 Express in the Arduino IDE and upload.
  5. Commission: bring each patient BLE device within range and start a measurement. The hub connects, subscribes, and initiates BLE pairing, and the bond keys are stored in flash. Once every device is bonded, open Notehub → commissioning.db and look at the bond_established Notes. Each one shows the device's ble_addr and addr_type. For any device whose addr_type is 0x00 (public) or 0x01 (random static), add its address to ENROLLED_DEVICES in vitals_config.h. Those devices advertise with a fixed address that the hub can't identify through IRK resolution alone. Devices with addr_type 0x02 (random private resolvable / RPA) are recognized automatically through the stored IRK and don't need an ENROLLED_DEVICES entry. The fail-closed production flow does not support devices with addr_type 0x03 (random private non-resolvable). Non-resolvable addresses rotate without any IRK linkage, so the hub can't re-identify them across address changes. A patient device that reports addr_type 0x03 needs a different identity mechanism before it can be used in a production deployment. Once all the entries are added, remove both -DALLOW_UNENROLLED_DEVICES_FOR_DEV=1 and -DALLOW_COMMISSIONING_BUILD from your build flags and reflash. The hub is now fail-closed, and it only accepts bonded and enrolled devices.
  6. Watch: open Notehub → Events. A _session.qo shows up within a minute, and a normal reading Note appears in its Notefile within the next 15-minute outbound window. If the reading trips a threshold, sync:true is set on both the measurement Note and vitals_alert.qo. They typically appear in Notehub together in the same immediate cellular session (~15–60 s), or in back-to-back immediate sessions.

Here is a sample Note this device emits:

{
  "weight_kg": 84.35,
  "prev_kg": 82.10
}

4. Hardware Requirements

PartQtyRationale
Notecarrier F1Feather-compatible carrier with an M.2 slot for the Notecard, an onboard LiPo connector, and Feather-header I²C routed to the Notecard. It accepts any Feather-format MCU.
Notecard Cell+WiFi (MBGLW)1Cellular takes the patient's WiFi out of the picture, so nobody has to provision credentials. It ships with a global prepaid SIM, with no activation and no monthly commitment per device.
Cellular antenna, adhesive flex, u.FL connector (included in Notecarrier F package)1Adhesive planar antenna that's good enough for home and office environments. Make sure it's in the box before assembly, because the Notecard won't register without a connected antenna.
u.FL to SMA bulkhead pigtail + external LTE whip antenna (optional, metal-enclosure installs only)0–1Needed if the hub goes inside a metal enclosure or in a room with poor indoor signal. Route the SMA bulkhead through a port drilled in the enclosure wall, and connect the LTE whip outside.
Adafruit Feather nRF52840 Express1Nordic nRF52840 SoC with native BLE 5.0, which is the radio that scans for and connects to the patient's device pack. Its Feather form factor seats directly in the Notecarrier F. The board uses a Micro-USB port for firmware flashing and serial monitoring.
Blues Mojo1Coulomb counter for checking cellular session energy and BLE scanning current on the bench during bring-up. The deployed hub doesn't need it.
USB-C wall adapter, 5V/2A1The hub runs on wall power through the Notecarrier F's USB-C port.
USB-C cable, ~1.8 m / 6 ft1Long enough to reach from a bedside outlet to a nightstand. Powers the hub through the Notecarrier F's USB-C port.
Micro-USB cable (development only)1Needed to flash firmware from the Arduino IDE and monitor serial output through the Feather's Micro-USB port. Take it out of the patient kit before shipping.
Project enclosure, ~100 × 65 × 30 mm1A clear polycarbonate lid lets the care team see the power LED remotely during a telehealth video call.

BLE patient devices used for bench validation. The firmware implements the Bluetooth SIG standard service profiles and should work with any compliant device. The four models below are the specific devices each parser was validated against, and interoperability with untested devices is not guaranteed. §7.4 and §10 Limitations cover parser scope, especially for the Blood Pressure device.

PartQtyRationale
Withings Body (WBS06)1Bench-validation weight scale. Implements Weight Scale Service (0x181D) / Weight Measurement (0x2A9D) in SI mode. Used to validate parseWeightKg.
Omron M4 Intelli IT (HEM-7155T)1Bench-validation blood pressure cuff. Implements Blood Pressure Service (0x1810) with the timestamp optional field set. Used to validate parseBpMmhg. The parser walks the optional-field flags to compute the pulse-rate offset correctly for both timestamp-present and timestamp-absent layouts (see §7.4).
Nonin 32301Bench-validation pulse oximeter. Implements Pulse Oximeter Service (0x1822) / PLX Spot-Check Measurement (0x2A5E). Used to validate parseSpO2.
Polar H101Bench-validation heart-rate chest strap. Implements Heart Rate Service (0x180D) / Heart Rate Measurement (0x2A37) in 8-bit HR value format. Used to validate parseHeartRate. The Polar H10 streams notifications continuously while it's worn, so the firmware disconnects after the first sample and suppresses reconnection for HR_SAMPLE_INTERVAL_MS (15 minutes). See §7.3.

The 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: Feather nRF52840 seated via 2×16 Feather header on Notecarrier F (SDA/SCL/3V3/GND through header); Notecard MBGLW in M.2 Key-E slot; cellular antenna u.FL to adhesive flex patch; USB-C wall adapter → Notecarrier F USB-C → M.2 modem power + LDO → 3V3 rail to Feather MCU and Notecard VIO; Mojo on Qwiic port (bench only)

There's no sensor wiring, because all patient data arrives over BLE. The only electrical connections are the stacked-board I²C bus, the shared power rail, and the antenna. The table below lists each signal, and steps 1–5 walk through the assembly sequence.

Pin-by-pin connections:

SignalFeather nRF52840 pinNotecarrier F header pinNotecard / destinationNotes
I²C data (SDA)SDA (labeled pad, nRF52840 P0.12)Feather header pin 6 (SDA)Notecard SDAThe Notecarrier F has an onboard 10 kΩ pull-up to 3.3V, so you don't need an external one.
I²C clock (SCL)SCL (labeled pad, nRF52840 P0.11)Feather header pin 7 (SCL)Notecard SCLSame pull-up network as SDA.
3.3V logic rail (VIO)3V3 (Feather header pin 2)Feather header 3.3V railNotecard VIO + Feather MCU logic supplyGenerated by the Notecarrier F's onboard LDO. It only supplies logic/VIO. The Notecard's cellular modem and radio draw main power from the Notecarrier F's USB-C input through a separate path on the M.2 connector.
GroundGND (Feather header pin 1)Feather header GNDNotecard GNDCommon ground for the entire stack.
Cellular antennaN/AN/ANotecard MBGLW cellular u.FL port (MAIN)Connect the adhesive pigtail u.FL plug here. Stick the flat antenna patch to the inside of the enclosure lid, or to a non-metallic enclosure wall away from the Notecard edge.

Every connection from the Feather to the Notecard runs through the stacked Feather header, so the I²C bus and power rail don't need any jumper wires.

  1. Seat the Notecard. Insert the Notecard Cell+WiFi into the M.2 Key-E slot on the Notecarrier F and secure the single M.2 retention screw. Connect the adhesive cellular antenna's u.FL plug to the cellular MAIN u.FL port on the Notecard edge (the port closest to the M.2 connector). Stick the antenna patch to a flat, non-metallic surface. The inside of the enclosure lid works well. For metal enclosures or concrete-wall installations, use the optional u.FL-to-SMA bulkhead pigtail listed in §4. Thread it through a cable-gland port in the enclosure wall and connect an external LTE whip outside.

  2. Seat the Feather. Press the Adafruit Feather nRF52840 Express onto the Notecarrier F's two 16-pin Feather headers. The Feather's SDA and SCL pads (header pins 6 and 7, respectively) mate with the Notecarrier F's I²C pads, which route directly to the Notecard's SDA/SCL pins. The 3.3V supply and GND go through the header the same way, so you don't need any jumper wires.

  3. Mojo (bench only). Mojo's power connections are two JST-PH battery connectors, not USB, so measure the stack on battery. Plug a bench LiPo into Mojo's BAT connector and run Mojo's LOAD connector to the Notecarrier F's JST-PH battery connector. Leave the USB ports unplugged while you measure, so all of the stack's current flows through Mojo. Connect the Mojo's Qwiic cable to the Notecarrier F's Qwiic port (GND / 3.3V / SDA / SCL at 3.3V logic). The Qwiic bus shares the Feather header I²C lines, and the Notecard talks to the Mojo's LTC2959 coulomb counter over that same bus.

  4. Power and programming ports. The Notecarrier F powers the stack through two separate paths. On the main power path, USB-C 5V feeds the Notecard's M.2 power pins directly and drives the cellular modem and radio. On the 3.3V logic rail, the Notecarrier F's onboard LDO converts that same USB supply to 3.3V and distributes it to the Feather header 3V3 pin (powering the Feather MCU) and the Notecard VIO pin (I/O level reference). The Notecard's radio circuitry draws from main power, not from the 3.3V logic rail.

    Deployment: plug the USB-C cable from the wall adapter into the Notecarrier F's USB-C port. That one connection powers both paths and runs the entire stack.

    Development and firmware flashing: both ports must be connected at the same time. Connect a Micro-USB cable to the Feather's Micro-USB port for firmware upload and serial monitoring. The Feather's UF2 bootloader enumerates over its own Micro-USB connector, and the Notecarrier F's USB-C port can't flash the Feather. Meanwhile, keep the Notecarrier F's USB-C port connected to a USB supply so the Notecard has main power. If only the Feather Micro-USB is connected, the Notecard has no main power and every Notecard API call will time out.

  5. Enclosure. The Notecarrier F (about 96 × 62 mm) fits in an enclosure of 100 × 65 × 30 mm or larger. Drill one pass-through for the USB-C power cable on one side and a small vent on the opposite side, since the nRF52840 and Notecard put out modest heat during a cellular session. Home deployments with the adhesive antenna don't need an external antenna port.

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:vitals-hub.

  2. Set the ProductUID in firmware. Open vitals_config.h and add #define PRODUCT_UID "com.your-company.your-name:vitals-hub" at the very top of the file, above the #ifndef PRODUCT_UID guard, with your actual ProductUID in place of the quoted string. You can also pass it as a compiler flag: -DPRODUCT_UID='"com.your-company.your-name:vitals-hub"'. PRODUCT_UID is defined through vitals_config.h, and the .ino file has no #define PRODUCT_UID.

  3. Claim the Notecard. Power the assembled hub over USB-C. The Notecard associates itself with your Notehub project automatically on its first cellular connection, so you don't have to claim it manually. The device appears in the Devices tab within 1–2 minutes.

  4. Create a Fleet per patient cohort. Fleets group devices that share configuration. For an RPM program, one fleet per condition (for example, chf-30-day or copd-60-day) works well. You tune threshold defaults at the fleet level, then override them per device when a specific patient's care plan calls for tighter limits. Smart Fleet rules can assign devices automatically at provisioning time, based on a tag set during kitting.

  5. Set environment variables. In Notehub, go to Devices → your device → Environment, or set them at the Fleet level for defaults that apply to every device in the cohort. All variables are optional, and the firmware defaults are shown below.

    warning

    Clinical disclaimer. The default threshold values and any clinical commentary in this table are illustrative starting points for a proof-of-concept demonstration only. They are not medical advice, and they do not represent validated clinical standards. The clinical program team must select, review, and validate every alert threshold for the intended patient population and care workflow before this device is used in any patient-facing setting.

    VariableDefaultPurpose
    bp_systolic_high160Systolic BP threshold (mmHg) at or above which the bp_high alert fires. This is a configurable program threshold. AHA Stage 2 hypertension begins at ≥140 mmHg systolic (≥90 diastolic), but many RPM programs set a higher initial alert point (for example, 160/100) to reduce alarm fatigue.
    bp_diastolic_high100Diastolic BP threshold (mmHg) at or above which the bp_high alert fires, either together with or independently of the systolic threshold.
    spo2_low92SpO2 threshold (%) below which the spo2_low alert fires. Values under 92% in a recovering patient warrant clinical review.
    hr_high130Heart rate threshold (bpm) above which the hr_high alert fires. Applied only to Heart Rate Service (0x180D) readings from the activity band. The pulse values carried in blood pressure (bp.qo) and SpO2 (spo2.qo) Notes are recorded but not evaluated against this threshold.
    hr_low40Heart rate threshold (bpm) below which the hr_low alert fires. Same scope: activity band Heart Rate Service readings only.
    weight_delta_kg2.3Weight gain threshold (kg, ≈ 5 lbs). Fires weight_gain when the current reading exceeds the previous reading in the same boot session by at least this amount. The delta resets to zero on a power cycle, because the POC doesn't persist it across reboots. This is the standard CHF fluid-retention threshold in most heart failure disease management programs.

    Changes take effect on the hub's next inbound sync (every 60 minutes by default). To pull updated values sooner, temporarily reduce inbound in the Notecard's hub.set configuration, or trigger a sync from the blues.dev In-Browser Terminal. The Notecard fetches fresh env-var values from Notehub on each inbound session (60 minutes by default). The firmware then re-reads those locally cached values every 2 minutes (ENV_POLL_MS) with env.get, which the Notecard answers without a cellular round-trip. That way threshold changes apply within 2 minutes of the next inbound sync, instead of sitting unused for up to another full hour.

  6. Configure routes. At a minimum, add one route for vitals_alert.qo (real-time delivery to an on-call endpoint, SMS gateway, or EHR webhook) and one for each reading Notefile (to a long-term analytics store). Send vitals_fault.qo to the same real-time endpoint as vitals_alert.qo, because it reports readings and alerts that never left the hub. With the Notefiles split, alert Notes can go to a paging system and routine readings to a patient record, and the route itself needs no filtering logic. See the Notehub routing docs.

What to look for in Notehub

In the Notehub web console, go to Devices (your newly claimed hub) and click Events to see real-time and historical Notes. You'll see these Notefiles:

  • _session.qo: appears within a minute of first power-on and confirms that the Notecard has cellular coverage and your ProductUID is correct. Each inbound sync (once an hour by default) and each threshold-triggered outbound sync generates a session Note.
  • weight.qo, bp.qo, spo2.qo, activity.qo: normal readings (ones that don't trip a threshold) queue on the Notecard and show up in Notehub within the next 15-minute outbound window. A reading that does trip a threshold carries sync:true on the measurement Note itself. That reading and its companion vitals_alert.qo Note typically arrive in the same immediate cellular session, or in back-to-back immediate sessions, instead of on the next scheduled sync.
  • vitals_alert.qo: sent alongside the threshold-tripping measurement Note, also with sync:true, at most once per alert type every 5 minutes. Both Notes typically arrive in the same immediate cellular session (~15–60 s), or in back-to-back immediate sessions, and skip the normal 15-minute upload timer. The body of each alert Note shows which threshold was crossed (for example, {"alert":"bp_high","systolic_mmhg":168,"diastolic_mmhg":98}).
  • vitals_fault.qo: shouldn't appear on a healthy hub. It means a reading or alert Note was never confirmed: on all three attempts, the Notecard rejected it or didn't answer. A missing answer doesn't prove the Note wasn't stored, since the Notecard may have accepted it and only the I²C reply was lost, so treat each count as a possible loss. The body counts these unconfirmed Notes since the last report (for example, {"dropped_readings":1,"dropped_alerts":0}), but not which vital or what value. A nonzero dropped_alerts means at least one alert may not have reached the care team. §7.7 has the details.
  • commissioning.db: written during a commissioning build (with both ALLOW_UNENROLLED_DEVICES_FOR_DEV=1 and ALLOW_COMMISSIONING_BUILD active) whenever the hub successfully bonds a new device. Each bond_established Note contains the device's ble_addr (colon-separated, MSB-first) and addr_type. Check these Notes after commissioning. Devices with addr_type 0x00 (public) or 0x01 (random static) must be added to ENROLLED_DEVICES in vitals_config.h before the shipping build. The Commission step in §3 Technical Summary and §10 Limitations have the details.

7. Firmware Design

FileRole
post_discharge_vitals_hub.inoArduino entry point: global state, setup(), loop()
vitals_config.hBuild-time configuration: PRODUCT_UID, DEBUG_VITALS, ALLOW_UNENROLLED_DEVICES_FOR_DEV, interval constants, MAC allow-list
ble_central.h / ble_central.cppBLE Central: identity gate, scan/connect/disconnect callbacks, GATT data callbacks, pairing initiation, initBLE()
notecard_helpers.h / notecard_helpers.cppNotecard layer: hub.set configuration, template registration, env-var fetch, Note submission with checked alert delivery
ble_parsers.hStatic inline GATT decoders + physiological plausibility guards

7.1 Installing and flashing

Dependencies:

  • Adafruit nRF52 Arduino board support package. To install it from the Arduino IDE Boards Manager, add the index URL https://adafruit.github.io/arduino-board-index/package_adafruit_index.json under File → Preferences → Additional Boards Manager URLs, then search for "Adafruit nRF52". Select Adafruit Feather nRF52840 Express as the board.
  • Blues Wireless Notecard (note-arduino). Install it from the Arduino Library Manager (search for "Blues Wireless Notecard"), and check github.com/blues/note-arduino/releases for the latest release before flashing.
  • bluefruit.h, which comes with the Adafruit nRF52 board support package, so there's nothing separate to install.

Flashing: Connect a Micro-USB cable to the Feather's Micro-USB port (not the Notecarrier F's USB-C port). Open post_discharge_vitals_hub.ino in the Arduino IDE, select Adafruit Feather nRF52840 Express, select the correct COM/tty port (the Feather enumerates as a serial device over its Micro-USB connector), and click Upload. You don't need an external programmer, because the Feather's onboard UF2 bootloader handles programming directly over USB.

During bench bring-up, open the serial monitor at 115200 baud to watch [BLE] and [VITALS] log lines in real time. This works in a debug build only (DEBUG_VITALS=1 with ALLOW_DEBUG_BUILD, per the build flags below). The default shipping build emits no serial output, so patient data is never printed over USB in a production flash.

Build flags. Several compile-time flags in this project are guarded by a companion acknowledgment flag. Leave out the companion and you get a hard compile-time error, so a development or commissioning build can't accidentally ship to a patient hub. The cleanest way to pass extra flags is with arduino-cli:

# Debug build (verbose serial logging)
arduino-cli compile \
  --fqbn adafruit:nrf52:feather52840 \
  --build-property "compiler.cpp.extra_flags=-DALLOW_DEBUG_BUILD -DDEBUG_VITALS=1" \
  --build-property "compiler.c.extra_flags=-DALLOW_DEBUG_BUILD -DDEBUG_VITALS=1" \
  firmware/post_discharge_vitals_hub

# Commissioning build (identity bypass for initial BLE pairing)
arduino-cli compile \
  --fqbn adafruit:nrf52:feather52840 \
  --build-property "compiler.cpp.extra_flags=-DALLOW_UNENROLLED_DEVICES_FOR_DEV=1 -DALLOW_COMMISSIONING_BUILD" \
  --build-property "compiler.c.extra_flags=-DALLOW_UNENROLLED_DEVICES_FOR_DEV=1 -DALLOW_COMMISSIONING_BUILD" \
  firmware/post_discharge_vitals_hub

In the Arduino IDE, you can add extra flags by creating a platform.local.txt file in the Adafruit nRF52 board-support-package folder with a line like compiler.cpp.extra_flags=-DALLOW_DEBUG_BUILD -DDEBUG_VITALS=1 (see the Arduino platform customization docs). The standard IDE GUI doesn't expose a build-flags field for the nRF52 board package, and that includes the Sketch → Export Compiled Binary menu.

7.2 Module responsibilities

ResponsibilityWhere
Build-time config, DBG macros, MAC allow-listvitals_config.h
Notecard hub configuration and template registrationnotecard_helpers.cpp: notecardConfigure, defineTemplates
Threshold fetch from Notehub env varsnotecard_helpers.cpp: fetchEnvVars (at boot and every 2 minutes via ENV_POLL_MS)
Reading Note emission + checked alert deliverynotecard_helpers.cpp: submitWeight, submitBp, submitSpO2, submitActivity
Dropped-Note report (vitals_fault.qo)notecard_helpers.cpp: reportDroppedNotes (every 2 minutes from loop())
Device identity gate (bonding + MAC allow-list)ble_central.cpp: isIdentifiedDevice
BLE scan + connect + disconnect + pairing event handlingble_central.cpp: bleScanCallback, bleConnectCallback, bleDisconnectCallback, blePairCompleteCallback
GATT indication/notification callbacks + plausibility checksble_central.cpp: weightDataCallback, bpDataCallback, spo2DataCallback, hrDataCallback
BLE characteristic byte parsing + plausibility helpersble_parsers.h: parseSfloat, parseWeightKg, parseBpMmhg, parseSpO2, parseHeartRate, weightKgPlausible, bpPlausible, spo2Plausible, hrPlausible
Buffered reading dispatch and watchdogpost_discharge_vitals_hub.ino: loop()

7.3 BLE scanning and GATT reading strategy

The nRF52840 SoftDevice acts as a BLE Central. bleScanCallback fires for every advertisement packet the hub receives. The firmware calls Bluefruit.Scanner.checkReportForService() against each of the four target service UUIDs and only connects on a match. Once connected, bleConnectCallback requests pairing (or bond re-encryption). When the link is encrypted, bleSecuredCallback runs doDiscoverAndSubscribe(), which calls service.discover(connHandle) to work out which of the four device types just connected. Then it calls char.discover() followed by enableIndicate() (for weight, BP, and SpO2, which use the GATT Indication sub-procedure) or enableNotify() (for heart rate, which uses Notification). The SoftDevice handles GATT ATT acknowledgment automatically. When the device transmits a reading, the matching data callback fills in a buffered struct and sets its valid flag, and the main loop drains that buffer on its next iteration.

The firmware uses one BLE Central connection slot at a time. If the patient picks up the BP cuff while the scale is connected, the scale's connection finishes first (the scale sends one indication and disconnects). The scanner then restarts (through restartOnDisconnect(true)), and the hub picks up the BP cuff on the next scan cycle.

Indication-based devices (weight, BP, SpO2) disconnect on their own after sending a single measurement. The full connection-measure-disconnect cycle typically finishes within 2–5 seconds.

Heart Rate Service devices use Notification instead of Indication, and they stream readings continuously for as long as they stay connected. A device like the Polar H10 will keep sending samples every second indefinitely. The firmware deals with this explicitly. loop() calls Bluefruit.disconnect() after it consumes the first HR notification, and bleScanCallback suppresses further HR Service connection attempts for HR_SAMPLE_INTERVAL_MS (15 minutes) by checking millis() - g_last_hr_sample_ms. That caps the activity.qo Note rate at one sample per 15-minute window while the band is within range, no matter how long the patient wears it.

The Bluetooth SIG assigns the four target service UUIDs:

DeviceService UUIDCharacteristic UUIDSub-procedure
Weight scale0x181D0x2A9DIndication
Blood pressure cuff0x18100x2A35Indication
Pulse oximeter0x18220x2A5EIndication
Activity band0x180D0x2A37Notification

7.4 Characteristic value encoding

The four measurement characteristics don't all use SFLOAT. Between them, they use three different wire encodings.

Blood pressure and SpO2 use IEEE 11073 SFLOAT. The Blood Pressure Measurement (0x2A35) and PLX Spot-Check Measurement (0x2A5E) characteristics encode their numeric fields as IEEE 11073 SFLOAT. That's a 16-bit type where the upper four bits are a signed exponent and the lower twelve bits are a signed mantissa (value = mantissa × 10^exponent). The firmware decodes these in parseSfloat() (in ble_parsers.h). It compares the reserved sentinel values (0x07FF = NaN, 0x0800 = NaN, 0x07FE = +Inf, 0x0802 = -Inf, 0x0801 = Reserved) against the full 16-bit raw SFLOAT encoding before it decodes the mantissa and exponent. Checking only the masked 12-bit mantissa field would wrongly reject valid numbers. For example, 0x17FF has mantissa bits 0x7FF that match the NaN sentinel mask, but its exponent field (0x1) makes it a valid number (mantissa=2047, exponent=1, value=20470). Results are rounded to integers, since 1 mmHg / 1% granularity is clinically sufficient.

Blood Pressure parser scope. Systolic and diastolic values (bytes [1–6], always mandatory) parse correctly for any standards-compliant BP device. The parser decodes the optional pulse_bpm field by walking the optional-field flags. It computes the pulse-rate SFLOAT offset as 7 bytes (mandatory header), plus 7 more bytes if the timestamp field is present (flags bit 1). That handles both the timestamp-included layout (the Omron M4 bench-validation device) and the timestamp-absent layout, which covers all compliant Blood Pressure Measurement (0x2A35) implementations. The User ID (flags bit 3) and Measurement Status (flags bit 4) fields come after the pulse rate in the spec ordering and aren't captured.

PLX Spot-Check Measurement (0x2A5E) parser scope. The mandatory byte layout is [0] Flags (uint8, 1 byte), [1–2] SpO2 SFLOAT (%), and [3–4] pulse rate SFLOAT (bpm), so a minimum valid frame is 5 bytes. The optional fields start at byte [5] and aren't captured. They are Timestamp (7 bytes, flags bit 0), Measurement Status (2 bytes, flags bit 1), Device and Sensor Status (3 bytes, flags bit 2), and Pulse Amplitude Index (SFLOAT 2 bytes, flags bit 3). The Flags field is 1 byte (uint8), not 2, the same width as the Flags field in the Blood Pressure Measurement characteristic. Some GATT tutorials incorrectly widen the PLX Flags to uint16. The bench-validation device (Nonin 3230) implements the SIG-standard layout, and parseSpO2() has been validated against it.

Weight uses uint16 fixed-point. The Weight Measurement characteristic (0x2A9D) encodes the weight as a plain uint16, not SFLOAT. In SI mode the raw value has a resolution of 0.005 kg (5 g), so you divide by 200 to get kilograms. In Imperial mode the resolution is 0.01 lb. An unsuccessful measurement is flagged with the sentinel 0xFFFF. parseWeightKg() reads the uint16 directly, applies the fixed-resolution multiplier, and converts lbs to kg when the Imperial flag (bit 0 of the Flags byte) is set.

Heart rate is a plain uint8/uint16. The Heart Rate Measurement characteristic (0x2A37) carries the heart rate value as a plain unsigned integer. It's a uint8 when flags bit 0 is 0 (the common case for consumer bands) and a uint16 when bit 0 is 1. There's no SFLOAT decoding involved.

7.5 Event payload design

Each of the four reading Notefiles uses a template that stores Notes on the Notecard as fixed-length binary records instead of free-form JSON, which cuts the per-reading wire size by 3–5×. vitals_alert.qo stays untemplated, because its body changes with the alert type.

weight.qo sample:

{
  "weight_kg": 84.35,
  "prev_kg": 82.10
}

bp.qo sample:

{
  "systolic_mmhg": 138,
  "diastolic_mmhg": 86,
  "pulse_bpm": 74
}

spo2.qo sample:

{
  "spo2_pct": 96,
  "pulse_bpm": 71
}

activity.qo sample:

{
  "heart_rate_bpm": 68
}

vitals_alert.qo sample (untemplated, immediate sync):

{
  "alert": "weight_gain",
  "weight_kg": 84.45,
  "delta_kg": 2.35
}

The alert types are weight_gain, bp_high, spo2_low, hr_high, and hr_low. The Notecard adds its metadata envelope (timestamp and device identity) automatically, and it appears alongside the body in Notehub. Location is only included if the device has a configured location source. A wall-powered indoor hub with no GNSS won't have a location field.

7.6 Sync strategy and power

Wall-powered for continuous availability. The hub runs on USB-C power (no battery), so the BLE scanner stays active 24/7. The clinical workflow depends on that. A patient may take a reading at any time (a 3 AM weight check, a midnight BP reading, and so on), and the hub has to be online to capture it immediately. There's no sleep/wake cycle and no polling interval. Measurements are event-driven and go to Notehub without delay, subject to the 15-minute outbound window, or immediately if they trip a threshold.

Notecard cellular sync. The Notecard runs in periodic mode with outbound:15 (minutes) and inbound:60. Readings queue on the Notecard between cellular sessions and upload in a single burst every 15 minutes. When a reading trips a threshold, sync:true goes on both the measurement Note (for example, bp.qo) and the companion vitals_alert.qo Note. Both skip the queue, and they're typically delivered in the same immediate cellular session, or in back-to-back immediate sessions. The 15-minute default is conservative. For most RPM programs, a reading that uploads within 15 minutes of being taken is well inside the care team's response window.

Alert cooldown. Once a vitals_alert.qo Note fires for a given alert type, the firmware suppresses further alert Notes of that type for ALERT_COOLDOWN_MS (5 minutes). That limits how many notifications the care team gets while a patient stays out of range. The threshold-tripping measurement Note still carries sync:true and arrives immediately, so the care team's data record is complete. Only the companion alert Note, which may trigger a page or an automated response, is held back. A reading that stays out of range across several consecutive measurements produces at most one alert every 5 minutes instead of one per reading cycle, which cuts down on alert fatigue. In this POC the 5-minute floor is a firmware constant. A production deployment would expose it as a configurable Notehub environment variable alongside the clinical thresholds.

Radio duty cycle. The nRF52840 SoftDevice duty-cycles the BLE radio automatically, with the scanner running at a 50% duty cycle (a 100-millisecond window every 200-millisecond interval). The BLE scanner accounts for most of the continuous baseline current draw (~5–10 mA). Each cellular session accounts for the short peak-current burst (~250 mA), and even though it's brief (~15–30 seconds), it contributes a sizable share of total energy in each 15-minute sync window.

7.7 Error handling

  • hub.set with response check. notecardConfigure() guards against an empty PRODUCT_UID before sending and checks the response err field through sendChecked(). A failed or rejected configuration gets logged instead of silently swallowed.
  • Checked delivery with retry for all Notes. Alert Notes and routine measurement Notes both use checked enqueue through sendVitalNoteChecked() / sendAlertNote(). Those call requestAndResponse() and make up to ALERT_ENQUEUE_RETRIES (3) attempts, so a transient I²C error doesn't drop a patient reading. Routine readings call sendVitalNoteChecked() with addSync=false, and the normal outbound cadence applies. Threshold-tripping readings call it with addSync=true to trigger an immediate outbound session for that measurement, whatever the alert-cooldown state. Then, if the alert type isn't in its cooldown, sendAlertNote() submits the companion vitals_alert.qo Note, which is also checked and retried. The alert is attempted even when the measurement Note failed, so losing the measurement Note doesn't also cost the care team the alert, and the alert body still carries the value that tripped it.
  • Unconfirmed Notes leave a record. A Note that goes unconfirmed on all three attempts may be lost. The firmware can't tell a rejected Note from one the Notecard stored but whose I²C reply never arrived, so it counts both as possible losses. (For the same reason, a retry after a lost reply can queue the same Note twice.) Every 2 minutes (ENV_POLL_MS), reportDroppedNotes() tries to send the counts as a vitals_fault.qo Note (dropped_readings, dropped_alerts) with sync:true. The counts are kept until the Notecard accepts that Note, so a failure during a Notecard fault is still reported once the Notecard responds again. The report says how many readings and alerts went unconfirmed, not which vitals or what values. If the Notecard never recovers, nothing reaches Notehub at all, which is why a production hub needs a heartbeat (see §10). Serial logging exists only in debug builds, so on a shipping hub this Note is the only record of a possible loss.
  • Configuration calls checked. hub.set and note.template calls use sendChecked(), so a rejected template registration gets logged instead of silently swallowed.
  • Physiological plausibility. Data callbacks in ble_central.cpp apply plausibility guards (defined in ble_parsers.h) before setting a reading's valid flag. Frames outside human-survival bounds get logged and discarded. That covers impossible BP relationships, SpO2 values below 50%, HR outside 20–300 bpm, and weight outside 1–500 kg. The guards keep malformed frames and noncompliant devices from producing impossible readings or triggering spurious alerts.
  • GATT payload guards. BLE callbacks check the minimum len before dereferencing any byte. Measurement-unsuccessful sentinels (0xFFFF for weight, and NaN/reserved SFLOAT for blood pressure and SpO2) make the parsing helpers return −1.0f or NAN, and the callbacks discard non-positive values.
  • Env-var validation. fetchEnvVars() checks for a NULL response, a non-empty err field, and a non-empty string before calling atof(). Related threshold pairs (systolic/diastolic, hr_high/hr_low) are cross-checked for sensible ordering before they're applied.

7.8 Key code snippet 1: Notecard periodic sync with 15-minute outbound

The firmware sends hub.set as a checked request that guards against an empty PRODUCT_UID and inspects the Notecard's response err field through sendChecked(). OUTBOUND_INTERVAL_MIN (15) and INBOUND_INTERVAL_MIN (60) come from vitals_config.h, so queued readings upload every 15 minutes and the Notecard fetches env vars every 60:

void notecardConfigure() {
    if (PRODUCT_UID[0] == '\0') {
        DBG_PRINTLN("[CFG] PRODUCT_UID is empty — hub will not associate with a Notehub project");
        return;
    }
    bool ok = false;
    J *req = notecard.newRequest("hub.set");
    if (req) {
        JAddStringToObject(req, "product",  PRODUCT_UID);
        JAddStringToObject(req, "mode",     "periodic");
        JAddNumberToObject(req, "outbound", OUTBOUND_INTERVAL_MIN);
        JAddNumberToObject(req, "inbound",  INBOUND_INTERVAL_MIN);
        ok = sendChecked(req);
    }
    // ...

7.9 Key code snippet 2: immediate-sync alert on threshold trip

sync:true tells the Notecard to open a session immediately instead of waiting for the next scheduled outbound window. A care team that needs to act on a dangerous SpO2 within minutes can't wait for the next quarter-hour. submitSpO2() builds the alert body, and sendAlertNote() wraps it in a note.add to vitals_alert.qo with sync:true, making up to three checked attempts. The alert is attempted whether or not the spo2.qo measurement Note was accepted.

static bool sendAlertNote(J *body) {
    bool ok = false;
    for (uint8_t attempt = 1; attempt <= ALERT_ENQUEUE_RETRIES; ++attempt) {
        J *req = notecard.newRequest("note.add");
        // ...
        JAddStringToObject(req, "file", "vitals_alert.qo");
        JAddBoolToObject  (req, "sync", true);
        // ...
    }
    // ...
}

// ...

void submitSpO2(int16_t spo2_pct, int16_t pulse) {
    // ...
    if (threshold_tripped) {
        // ...
        sendVitalNoteChecked("spo2.qo", mbody, /*addSync=*/true);

        if (alert) {
            J *ab = JCreateObject();
            JAddStringToObject(ab, "alert",    "spo2_low");
            JAddNumberToObject(ab, "spo2_pct", spo2_pct);
            if (sendAlertNote(ab)) { g_last_spo2_alert_ms = millis(); }
            // ...

7.10 Key code snippet 3: BLE service discovery and indication subscribe

doDiscoverAndSubscribe() runs this once the link is encrypted (it's called from bleSecuredCallback). The service discover() call walks the connected peripheral's ATT database. On a match, setIndicateCallback() registers the data callback, and enableIndicate() writes the CCCD (Client Characteristic Configuration Descriptor) to turn on indications. Blood Pressure Measurement uses Indication, so the cuff sends each measurement and the hub acknowledges it. If either step fails, the hub disconnects to free its single Central slot.

static bool doDiscoverAndSubscribe(uint16_t connHandle) {
    if (s_weightSvc.discover(connHandle)) {
        // ...
    } else if (s_bpSvc.discover(connHandle)) {
        DBG_PRINTLN("[BLE] Blood pressure cuff — subscribing");
        s_bpChar.setIndicateCallback(bpDataCallback);
        if (!s_bpChar.discover() ||
            !s_bpChar.enableIndicate()) {
            DBG_PRINTLN("[BLE] BP: subscription failed — disconnecting");
            Bluefruit.disconnect(connHandle);
            return false;
        }
        g_oneshot_conn_handle = connHandle;

    } else if (s_pulseOxSvc.discover(connHandle)) {
        // ...

7.11 Key code snippet 4: SFLOAT decode in ble_parsers.h

The IEEE 11073 SFLOAT type is a 16-bit packed float used for the blood pressure and SpO2 fields. parseSfloat() sign-extends both the 4-bit exponent and the 12-bit mantissa, which is the step most BLE tutorial snippets get right. Sentinel rejection is the less obvious part. Reserved SFLOAT values are specific full 16-bit encodings, and they must be compared against the raw value before any decoding. If you check only the masked 12-bit mantissa field, you'll wrongly reject valid numbers whose lower 12 bits match a sentinel pattern but whose exponent is nonzero. For example, 0x17FF has mantissa bits 0x7FF that match the NaN sentinel 0x07FF, but its exponent 0x1 makes it a valid number (mantissa=2047, exponent=1, value=20470). Weight and heart rate don't use this decoder, and §7.4 covers their encoding formats.

static inline float parseSfloat(uint16_t raw) {
    // Compare the full 16-bit raw value against each reserved sentinel before
    // decoding mantissa/exponent.  Checking only the masked 12-bit mantissa
    // field would incorrectly reject valid encodings whose mantissa bits happen
    // to match a sentinel pattern but whose exponent is nonzero (e.g. 0x17FF
    // is a valid number with exponent=1, mantissa=2047, value=20470).
    if (raw == 0x07FFu || raw == 0x0800u ||
        raw == 0x07FEu || raw == 0x0802u ||
        raw == 0x0801u) {
        return NAN;
    }
    // Sign-extend mantissa: bits [11:0] → int16
    int16_t mantissa = (int16_t)(raw & 0x0FFFu);
    if (mantissa & 0x0800) {
        mantissa |= (int16_t)0xF000;   // propagate sign bit
    }
    // Exponent: sign-extend bits [15:12] to a full int8
    int8_t exponent = (int8_t)((raw >> 12) & 0x0F);
    if (exponent & 0x08) {
        exponent |= (int8_t)0xF0;      // propagate sign bit
    }
    return (float)mantissa * powf(10.0f, (float)exponent);
}

8. Data Flow

Data flow: patient BLE event → GATT parse (SFLOAT for BP/SpO2, uint16 for weight, uint8/16 for HR) + plausibility guard → threshold check against env-var rules → threshold-tripping path: measurement note + vitals_alert.qo both with sync:true → immediate cellular session (~15–60 s); normal path: reading Notefile queued → 15-min outbound sync → Notehub → care-coordinator alert route and analytics/EHR route; env vars flow back from Notehub on inbound 60-min cadence

Collected. Every time a patient uses a BLE health device: weight (kg), blood pressure (systolic/diastolic mmHg and pulse bpm), SpO2 (percent) and pulse rate, and heart rate (bpm from the activity band). The hub doesn't poll on a fixed schedule. Readings are event-driven and captured whenever the device transmits.

Transmitted.

  • weight.qo, bp.qo, spo2.qo, activity.qo: queued when a reading arrives and uploaded in the next 15-minute outbound sync window, unless the reading trips a threshold. In that case the measurement Note also gets sync:true and uploads immediately. These are template-encoded, so each Note is a compact binary record on the Notecard.
  • vitals_alert.qo: emitted alongside a threshold-tripping measurement Note, also with sync:true, unless an alert of the same type went out in the last 5 minutes (ALERT_COOLDOWN_MS). The measurement Note and the alert Note typically arrive in the same immediate cellular session (~15–60 s after the reading), or in back-to-back immediate sessions, rather than on the next scheduled sync.
  • vitals_fault.qo: sent with sync:true only after a reading or alert Note goes unconfirmed on every enqueue attempt. It carries dropped_readings and dropped_alerts counts, and the firmware retries it every 2 minutes until the Notecard accepts it.

Routed. All six Notefiles land in Notehub, and from there they go wherever the project's routes send them. The recommended split sends the reading Notefiles to long-term analytics or EHR staging, and vitals_alert.qo and vitals_fault.qo to an on-call or care coordinator notification channel (SMS gateway, webhook, EHR inbox message, and so on).

Alerts trigger on:

  • weight_gain: the current weight reading exceeds the previous weight reading in the same boot session by ≥ weight_delta_kg. Body: alert, weight_kg, delta_kg. (The first reading of a session never alerts, and the delta state doesn't persist across power cycles. See Limitations.)
  • bp_high: systolic ≥ bp_systolic_high or diastolic ≥ bp_diastolic_high. Body: alert, systolic_mmhg, diastolic_mmhg.
  • spo2_low: SpO2 < spo2_low. Body: alert, spo2_pct.
  • hr_high: heart rate from the activity band (Heart Rate Service, 0x180D) > hr_high. Body: alert, heart_rate_bpm. Pulse values from the BP cuff or pulse oximeter aren't evaluated against this threshold.
  • hr_low: heart rate from the activity band (Heart Rate Service, 0x180D) < hr_low and > 0. Body: alert, heart_rate_bpm. Same scope: activity band only.

Alert deduplication. When a reading trips a threshold, the measurement Note always carries sync:true and uploads immediately. The companion vitals_alert.qo Note is also rate-limited by ALERT_COOLDOWN_MS (5 minutes per alert type). If the same alert type fired within the last 5 minutes, the alert Note is suppressed, but the measurement Note still syncs. The care team gets a bounded number of actionable notifications, and the raw data record stays complete.

9. Validation and Testing

Expected steady-state. On a normal day, a post-discharge patient weighs in once in the morning, takes BP twice (morning and evening) and SpO2 once or twice, and wears the activity band off and on. Activity band heart rate samples are capped at one per HR_SAMPLE_INTERVAL_MS (15 minutes), so a patient who wears the band for two hours yields at most 8 HR Notes in that time. For a recovering patient, the hub produces roughly 5–15 reading Notes a day across the four Notefiles, zero alert Notes, and no vitals_fault.qo. The first set of Notes should show up in Notehub within 15 minutes of the first measurement after the hub comes online.

Simulating a threshold trip. The fastest way to test alert delivery is to set bp_systolic_high to 100 and bp_diastolic_high to 60 in the Fleet environment, so any normal BP reading trips bp_high. Set both, because the firmware rejects the pair whenever diastolic isn't below systolic, and the default diastolic is also 100. After the next inbound sync pulls the new values, take a BP measurement and confirm that a bp.qo Note and a vitals_alert.qo Note both appear in Notehub within a cellular session-establishment window (typically 15–60 s). Set them back to 160 and 100 when you're done. Deleting the variables isn't enough, because the firmware keeps the last accepted values until the hub restarts.

Verifying BLE connectivity. Open the Arduino serial monitor at 115200 baud (debug build only: set DEBUG_VITALS=1 and ALLOW_DEBUG_BUILD per §7.1, because the default shipping build emits no serial output). Each scan hit, connection, and characteristic subscription prints a [BLE] line, and each reading prints a [VITALS] line. If no devices show up, make sure the BLE health device is in a measurement-ready state (most BLE health devices only advertise right after a measurement is initiated) and that it implements the standard Bluetooth SIG service UUID for its category.

Power validation with Mojo. Put the Mojo between a bench LiPo and the Notecarrier F's battery connector, with the USB ports unplugged (see §5, step 3). The trace has a continuous baseline with a recurring cellular burst on top of it:

StateExpected drawNotes
Continuous baseline (host + BLE scanning)~5–10 mAThe nRF52840 SoftDevice runs at a 50% scan duty cycle (+4 dBm). The Notecard's idle contribution (~8–18 µA) adds almost nothing to this baseline.
Cellular session burst (~15–30 s, once every 15 min)~250 mA average, with brief spikes to ~500 mA at peak LTE Cat-1 bis TX. The ~2 A figure only applies in GSM mode (2G fallback, select regions). See the MBGLW datasheet.LTE Cat-1 bis data transfer. Also fires on any threshold-tripping measurement Note or alert Note with sync:true.

The BLE scanner dominates the continuous baseline. The cellular burst is brief, but it contributes a sizable share of total energy per sync window, at roughly 30 s of ~250 mA (LTE Cat-1 bis) every 15 minutes. Both are well within what a standard 5V/2A USB phone charger can supply. Notecard current figures come from the MBGLW datasheet and the low-power design guide. Keep in mind that the ~8–18 µA Notecard idle figure is a module-level datasheet spec. The Mojo measures the whole powered stack at the battery input, which will read higher because of the nRF52840 and the Notecarrier F's regulator. When you bench-validate, focus on the trace shape and sync-burst cadence, and don't expect the Notecard datasheet idle figure to show up directly at the battery input.

A healthy Mojo trace for this hub shows a steady ~5–10 mA baseline (continuous BLE scanning) with a 15–30-second burst at ~250 mA (LTE Cat-1 bis) once every 15 minutes for the scheduled cellular sync. You'll also see extra ~250 mA bursts whenever a threshold-tripping measurement or alert Note carries sync:true. Where the network falls back to GSM, the cellular burst can spike briefly to ~2 A during transmit. That's normal MBGLW behavior on GSM, but you shouldn't see it in LTE Cat-1 bis–only deployments. If cellular bursts are missing or come far less often than every 15 minutes, check that PRODUCT_UID is set and that the Notecard has coverage (look for _session.qo events in Notehub).

Troubleshooting

Here are the issues that actually come up during first bring-up and deployment.

SymptomLikely causeWhat to check
Device never appears in Notehub's Devices tab.PRODUCT_UID is empty or wrong, or the cellular antenna isn't routed.Make sure PRODUCT_UID exactly matches the Notehub project, and confirm the u.FL antenna pigtail is attached to the Notecard. Check for _session.qo events. If there aren't any, there's no cellular connection.
_session.qo events arrive but no reading Notes ever appear.No BLE health device is in range, or the device isn't advertising its standard service UUID.Open the serial monitor at 115200 baud and watch for [BLE] Scanner started (debug build only, see the §7.1 build flags). Bring a BLE health device within a few meters and start a measurement, since most devices only advertise during an active measurement, not continuously. Confirm the device uses the standard Bluetooth SIG service UUID (not a proprietary profile).
BLE device connects but the reading struct is never populated.The device sends a measurement-unsuccessful sentinel (0xFFFF for weight, or NaN or reserved SFLOAT for blood pressure or SpO2), or the characteristic byte layout differs from the SIG specification.Check the [BLE] log lines (debug build only, see the §7.1 build flags) for the service discovery step. If discovery succeeds but the data callback never sets valid, add a Serial.printf in the callback to print the raw bytes. Compare them against the device's BLE specification, or use a BLE sniffer app (nRF Connect for Mobile works well).
Alerts fire on every reading even though values look normal.A threshold set clinically too tight, stale cached env values, or a scope override shadowing the intended value.In Notehub, check that each threshold env var holds a sensible value for the patient population. The firmware's validateEnvFloat rejects out-of-range and non-numeric values (including "0" and blank strings). Each var has explicit bounds (for example, bp_systolic_high ∈ [60, 260], hr_low ∈ [20, 100], weight_delta_kg ∈ [0.5, 20]), and a rejected value falls back to the previously accepted threshold instead of being applied. Notehub applies env vars hierarchically. Project scope is the baseline, fleet scope overrides it, and device scope overrides both. A device-level value silently shadows a fleet or project default, so check all three scopes when a threshold behaves unexpectedly.
A vitals_fault.qo Note appears.A reading or alert Note was rejected, or got no answer from the Notecard, on all three attempts. A Note that got no answer may still have been stored. Common causes are a Notecard that isn't fully seated, an I²C fault, or a Notecard without main power (§5, step 4).A nonzero dropped_alerts means at least one alert may not have reached the care team, and the report doesn't say which vital or value, so handle it under your program's missed-alert procedure. Then reseat the Notecard and check the hub's USB-C power. A debug build (§7.1) logs each failed attempt, with the Notecard's err text when there is one.
Weight readings are off by a factor of ~2.2.The device is transmitting in Imperial (lbs), but the flags byte isn't being parsed correctly.Bit 0 of the Flags byte (byte 0) sets SI vs. Imperial. Log data[0] in weightDataCallback and check it. If the device always sends Imperial, the parseWeightKg helper already converts lbs → kg when the flag is set.
No cellular sync for more than 15 minutes.The Notecard is in a coverage gap, or the firmware is stuck before calling submitWeight / submitBp / etc.From the blues.dev In-Browser Terminal, run hub.status for the Notehub connection state and card.wireless for signal strength. If coverage is good but syncs are missing, confirm the main loop() is running (add a serial heartbeat print every 30 seconds).
Mojo bench trace shows no cellular bursts at all.hub.set wasn't applied, or the Notecard is in minimum mode from a prior configuration.Send {"req":"hub.set","mode":"periodic","outbound":15,"inbound":60} from the blues.dev In-Browser Terminal to reset the sync mode. Then power-cycle the hub. The sketch calls notecardConfigure() in setup(), which re-applies the configuration.

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 + BLE setup.

10. Limitations and Next Steps

warning

Reminder: this is a proof-of-concept reference design, not a cleared medical device. It is not intended for diagnostic, emergency, or life-sustaining use. Any production deployment must address PHI/HIPAA compliance, secure data routing and storage, auditability, data retention policies, and applicable regulatory review before handling real patient data.

The hub is scoped to the post-discharge window on purpose. It serves one patient from one wall outlet, speaks the standard Bluetooth SIG health profiles, and uses the smallest commissioning workflow that works. Per-patient baselines, multi-device concurrency, OTA firmware, and proprietary activity-band data are all real product features, but they belong in a follow-on design instead of complicating this reference design.

Simplified for the POC

Every simplification below was a deliberate scope choice. Once a real post-discharge program is running the hub, a production deployment will close each one with added concurrency, a configurable knob, or a hardened security path. As the warning above says, this is a proof-of-concept reference design, not a cleared medical device.

One BLE connection at a time. The nRF52840's Central role supports multiple concurrent connections, but the sketch only opens one. If a patient manages to trigger two devices at once (unlikely with health devices that connect, measure, and disconnect quickly), the second connection attempt waits until the first device disconnects. Adding multi-connection support is straightforward with the Adafruit nRF52 library.

Alert cooldown is a fixed firmware constant, not remotely configurable. The hub suppresses repeated vitals_alert.qo Notes of the same type for 5 minutes (ALERT_COOLDOWN_MS), and you can't adjust that duration per patient without reflashing the firmware. A production deployment would expose it as a Notehub environment variable alongside the clinical thresholds, so care coordinators could tighten or relax the alert rate for individual patients.

Weight-delta alerting, not absolute. The weight_gain alert fires on the change from the previous reading, not on absolute weight. On first boot there's no previous reading, so a delta alert isn't possible. A daily-comparison window (today vs. the same time yesterday) is a more dependable clinical metric. It requires persisting the previous day's reading, which you could do by storing it in a local-only .dbx database Notefile (for example, weight_state.dbx), which survives restarts of both the host and the Notecard.

Heart rate only from activity band. The Heart Rate Service (0x180D) provides heart rate, but step count isn't part of the standard service. It lives in proprietary, manufacturer-specific GATT services that differ from one activity band brand to the next. The firmware only captures heart rate from that service.

Blood Pressure Measurement fields user_id and measurement_status aren't captured. parseBpMmhg correctly decodes systolic, diastolic, and pulse rate for any compliant 0x2A35 layout. Only the timestamp field (flags bit 1) affects the pulse-rate offset. When the timestamp is present, the parser adds 7 bytes to the mandatory 7-byte header, so it handles both timestamp-present (Omron M4) and timestamp-absent layouts correctly. The User ID (flags bit 3) and Measurement Status (flags bit 4) fields come after pulse rate in the spec ordering. The firmware doesn't use them and discards their values.

BLE bonding requires a commissioning step, and public/static-address devices also require an allow-list entry. The hub is fail-closed by default: unrecognized devices are ignored. Before the hub ships to a patient, you need a commissioning build with both -DALLOW_UNENROLLED_DEVICES_FOR_DEV=1 and -DALLOW_COMMISSIONING_BUILD to pair that patient's specific devices and store their bond keys. After commissioning, check the bond_established Notes in commissioning.db on Notehub. Any device whose addr_type is 0x00 (public) or 0x01 (random static) must also be added to ENROLLED_DEVICES in vitals_config.h. Bonding alone isn't enough for those devices, because the hub can't identify them through IRK resolution. Remove both commissioning flags before deploying to patients. The MAC allow-list is a compile-time constant, so updating it takes a reflash.

BLE pairing uses encrypted but unauthenticated bonding. The commissioning step uses Just Works pairing (setMITM(false)) because most consumer health devices lack input/output capabilities. That gives you an encrypted BLE connection, but it offers no man-in-the-middle protection during the initial key exchange. For production deployments that handle patient data, the clinical and security teams should define a controlled commissioning procedure (for example, pairing only in a secured kitting environment at the point of device preparation) and evaluate device and vendor choices that support authenticated pairing modes (Numeric Comparison or Passkey Entry) where input/output capabilities exist. They should also review the full threat model for the deployment setting before processing real patient data.

No offline-reading catchup. If the hub is unplugged for several days and then plugged back in, it has no way of knowing which readings it missed. Readings taken while the hub was offline are simply absent from the record.

Activity band steps not captured. The standard Heart Rate Service (0x180D) doesn't carry step count. Activity bands that expose a proprietary step-count characteristic need a device-specific firmware extension.

Production Next Steps

Before this proof-of-concept is ready for a real patient population, firmware delivery, baseline modeling, and supply-rail visibility all need hardening.

Investigate host firmware OTA. Notecard Outboard Firmware Update targets hosts whose bootloader the Notecard can drive over UART (for example, the STM32 System Memory bootloader). The Adafruit Feather nRF52840's UF2 bootloader uses USB mass-storage enumeration, which doesn't fit that model. Before OTA firmware updates can be a supported feature, a production deployment would need either a custom UART-accessible bootloader on the nRF52840 or another delivery path (for example, Nordic Bluetooth DFU triggered by an environment-variable flag delivered through the Notecard).

Per-patient baseline learning. Store a 7-day rolling average weight in a local-only .dbx Notefile, and alert when a single day's reading is more than weight_delta_kg above that rolling average instead of just above the previous reading.

Heartbeat Note. Send a heartbeat.qo every 24 hours even when no readings arrived, so the care team can tell "patient not using devices" apart from "hub offline."

Low-voltage notification. Notecard's card.voltage response monitors the supply rail, so a voltage_low alert can page the care coordinator if the hub is losing power or the USB supply is marginal.

Remotely configurable alert cooldown. Expose ALERT_COOLDOWN_MS as a Notehub environment variable, so care coordinators can tune the alert rate per patient without a reflash.

11. Summary

The 82-year-old heart failure patient sets the hub on the nightstand and forgets it's there. The discharge coordinator who mailed it sees weight, blood pressure, SpO2, and heart rate show up in Notehub the first time a device is used, without a password, an app, or a support call. If any reading crosses a clinical threshold, the coordinator gets paged within minutes. A Feather nRF52840 does the work, talking to the BLE health-device profiles on one side and a cellular Notecard on the other. Care coordinators retune per-patient thresholds from Notehub environment variables, with no firmware change and no truck roll. Some patients don't have home WiFi, can't configure a tablet, or are recovering at a family member's house. For them, this is the only hub architecture that reliably delivers the clinical outcome the program was built to produce.

On This Page

  • 1. Project Overview
  • 2. System Architecture
  • 3. Technical Summary
  • 4. Hardware Requirements
  • 5. Wiring and Assembly
  • 6. Notehub Setup
    • What to look for in Notehub
  • 7. Firmware Design
    • 7.1 Installing and flashing
    • 7.2 Module responsibilities
    • 7.3 BLE scanning and GATT reading strategy
    • 7.4 Characteristic value encoding
    • 7.5 Event payload design
    • 7.6 Sync strategy and power
    • 7.7 Error handling
    • 7.8 Key code snippet 1: Notecard periodic sync with 15-minute outbound
    • 7.9 Key code snippet 2: immediate-sync alert on threshold trip
    • 7.10 Key code snippet 3: BLE service discovery and indication subscribe
    • 7.11 Key code snippet 4: SFLOAT decode in ble_parsers.h
  • 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