GlucoseIQ

CGM Connectors

Normalize Dexcom Share, Libre LinkUp, and Nightscout payloads into one analytics-ready contract.

The connector module translates vendor payloads into NormalizedCGMReading. The normalized result extends GlucoseReading, so the same value can feed analyzeGlucose, AGP builders, live helpers, renderers, or your own pipeline.

import {
  normalizeDexcomEntries,
  normalizeLibreEntries,
  normalizeNightscoutEntries,
} from '@glucoseiq/core/connectors'

Normalization, not authenticated transport

These functions do not sign in to a vendor, hold credentials, refresh tokens, make network requests, poll, retry, backfill, cache, or persist data. Fetch payloads with the authorized API or client appropriate to your application, then pass those payloads to GlucoseIQ for deterministic normalization.

The normalized contract

Every adapter returns the same shape:

interface NormalizedCGMReading {
  readonly value: number
  readonly unit: 'mg/dL' | 'mmol/L'
  readonly timestamp: string
  readonly trend:
    | 'rapidRising'
    | 'rising'
    | 'slightlyRising'
    | 'flat'
    | 'slightlyFalling'
    | 'falling'
    | 'rapidFalling'
    | 'unknown'
  readonly source: 'dexcom' | 'libre' | 'nightscout' | 'unknown'
  readonly vendorId?: string
  readonly nativeUnit?: 'mg/dL' | 'mmol/L'
  readonly dedupKey?: string
}

Batch normalizers sort successful readings chronologically. Unknown or unsupported trend values become unknown instead of inventing a direction.

dedupKey is a deterministic key you can use in your own cache or persistence layer. The normalizers create the key; they do not remove duplicate readings for you.

Dexcom Share

Dexcom entries use Value, Trend, and WT. WT may be a Dexcom Date(epochMs) wrapper or an ISO timestamp.

import { normalizeDexcomEntry } from '@glucoseiq/core/connectors'

const reading = normalizeDexcomEntry({
  Value: 142,
  Trend: 'FortyFiveUp',
  WT: '/Date(1704096000000)/',
  ST: 'record-42',
})

reading.value     // 142
reading.unit      // 'mg/dL'
reading.trend     // 'slightlyRising'
reading.source    // 'dexcom'
reading.vendorId  // 'record-42'
reading.dedupKey  // 'dexcom:record-42'

Use parseDexcomDate when you only need the date conversion and normalizeDexcomTrend when normalizing a trend outside a full entry.

Libre LinkUp

Libre normalization respects the account display unit. When ValueInMgPerDl exists, it is preferred for the normalized value; an account using mmol/L is recorded in nativeUnit.

import { normalizeLibreEntry } from '@glucoseiq/core/connectors'

const reading = normalizeLibreEntry({
  Value: 6.7,
  ValueInMgPerDl: 121,
  GlucoseUnits: 0,
  TrendArrow: 4,
  Timestamp: '2025-01-01T08:00:00Z',
})

reading.value       // 121
reading.unit        // 'mg/dL'
reading.nativeUnit  // 'mmol/L'
reading.trend       // 'rising'
reading.dedupKey    // 'libre:2025-01-01T08:00:00.000Z'

If ValueInMgPerDl is absent, GlucoseUnits: 0 keeps Value in mmol/L and GlucoseUnits: 1 uses mg/dL. Payloads with neither field default to mg/dL for backward compatibility.

Libre trend arrows map from 1 (rapidly falling) through 5 (rapidly rising); 3 is flat. normalizeLibreTrend returns unknown for a missing or out-of-range value.

Nightscout

Nightscout adapters accept the /api/v1/entries SGV shape. They prefer a valid dateString, fall back to the epoch-millisecond date, and preserve _id as vendorId.

import { normalizeNightscoutEntry } from '@glucoseiq/core/connectors'

const reading = normalizeNightscoutEntry({
  sgv: 108,
  date: 1735718400000,
  direction: 'Flat',
  type: 'sgv',
  _id: 'nightscout-7',
})

reading.timestamp  // '2025-01-01T08:00:00.000Z'
reading.trend      // 'flat'
reading.source     // 'nightscout'
reading.dedupKey   // 'nightscout:nightscout-7'

normalizeNightscoutDirection recognizes the common Dexcom-style direction strings. Other strings normalize to unknown.

Keep valid readings from a mixed batch

The strict batch functions throw if an entry cannot be normalized. Use the safe variants when one malformed record should not reject the rest of the batch:

import { safeNormalizeDexcomEntries } from '@glucoseiq/core/connectors'

const result = safeNormalizeDexcomEntries([
  { Value: 120, Trend: 'Flat', WT: 'Date(1700000000000)' },
  { Value: 130, Trend: 'Flat', WT: 'not-a-date' },
  { Value: 140, Trend: 'SingleUp', WT: 'Date(1700000600000)' },
])

result.readings.length // 2
result.errors
// [{ index: 1, message: 'Unable to parse Dexcom date: not-a-date' }]

safeNormalizeLibreEntries and safeNormalizeNightscoutEntries use the same SafeNormalizeResult contract. Successful readings are sorted by timestamp; each error reports the original input index and message.

Plan around source capabilities

Capability descriptors expose source behavior without performing transport:

import {
  DEXCOM_CAPABILITIES,
  LIBRE_CAPABILITIES,
  NIGHTSCOUT_CAPABILITIES,
} from '@glucoseiq/core/connectors'

DEXCOM_CAPABILITIES.updateIntervalSec // 300
LIBRE_CAPABILITIES.trendVocabulary    // 'coarse'
NIGHTSCOUT_CAPABILITIES.clockModel    // 'relay'
SourceTierTypical updateStale afterTrend vocabularyHistory depthClock
Dexcom1300 s600 sfull1 daydirect
Libre160 s300 scoarse14 daysdirect
Nightscout2300 s900 sfull90 daysrelay

Tier 1 describes a full-fidelity vendor feed; tier 2 describes a community or relay source. Treat these values as declarative defaults for your scheduling, freshness, and gap policies—not as a polling engine or a guarantee from the upstream service.

A practical ingestion boundary

Keep transport and analysis separate in application code:

import { analyzeGlucose } from '@glucoseiq/core'
import { safeNormalizeNightscoutEntries } from '@glucoseiq/core/connectors'

async function buildReport() {
  const response = await fetch('/api/cgm/nightscout')
  if (!response.ok) throw new Error(`CGM request failed: ${response.status}`)

  const payload = await response.json()
  const { readings, errors } = safeNormalizeNightscoutEntries(payload)

  return {
    report: analyzeGlucose(readings),
    normalizationErrors: errors,
  }
}

Your server route owns authorization and network policy. GlucoseIQ owns the pure, testable translation into a vendor-neutral analytics contract.

On this page