Skip to content

Java port of calibration algorithm for Android CGM apps#5

Merged
ErikDeBruijn merged 28 commits into
mainfrom
feat/java-calibration
Mar 17, 2026
Merged

Java port of calibration algorithm for Android CGM apps#5
ErikDeBruijn merged 28 commits into
mainfrom
feat/java-calibration

Conversation

@ErikDeBruijn

Copy link
Copy Markdown
Owner

Summary

Complete Java port of the CareSens Air CGM calibration algorithm, designed as a standalone library for integration into Android CGM apps such as xDrip+, Jugluco, and AndroidAPS.

This port was prompted by a request from Jon Ande (jamorham), the lead developer of xDrip+, who asked about converting the C implementation to Java for native integration without binary library dependencies.

What this adds

Java calibration library (java/)

A pure Java implementation of the full 14-step calibration pipeline that converts raw ADC sensor readings into calibrated glucose values (mg/dL), matching the behavior of the proprietary i-SENS libCALCULATION.so library.

Source files (17):

File Purpose
CareSensCalibrator.java Public facade — the main entry point for integrators
SensorConfig.java Immutable sensor factory parameters with Builder pattern
CalibrationResult.java Immutable result object (glucose, trend, errors)
BlePacketParser.java Parses raw BLE C5 notification bytes (84 bytes)
CalibrationAlgorithm.java Internal: 14-step calibration pipeline
SignalProcessing.java Internal: LOESS, Hampel, SG smoothing, IRLS, FIR
CheckError.java Internal: 7 error detectors (err1/2/4/8/16/32/128)
MathUtils.java Internal: 28 math/statistics functions
LoessKernel.java Internal: 4050 precomputed LOESS regression coefficients
OracleBinaryReader.java Test utility: reads oracle binary verification files
model/*.java (7 files) Data model classes (AlgorithmState, DeviceInfo, etc.)

Test files (9), 323 tests total:

File Tests Purpose
MathUtilsTest.java 80 All math functions incl. NaN, overflow, edge cases
SignalProcessingTest.java 42 LOESS pipeline, SG smoothing, regression, error helpers
CheckErrorTest.java 51 All 7 error detectors with state machine verification
CalibrationAlgorithmTest.java 63 Pipeline steps, Kalman convergence, safety guards
CareSensCalibratorTest.java 39 Public API, state persistence, input validation
BlePacketParserTest.java 10 Packet parsing, defensive copies, error handling
OracleBinaryReaderTest.java 23 Binary file parsing across all 5 sensor lots
OracleVerificationTest.java 7 Full pipeline vs. oracle (2000 readings, 5 lots)
EndToEndIntegrationTest.java 10 BLE bytes → glucose, state persistence, error paths

C implementation improvements (pre-existing branch work)

The branch also includes C-side improvements that were developed during the oracle verification phase:

  • Trendrate computation
  • SG smoothing fixes
  • lot_type=2 support
  • Real sensor data tooling
  • Multiple error detector implementations (err1/2/4/8/16/32/128)
  • Oracle infrastructure updates

Oracle verification results

The Java implementation was verified against 2000 oracle readings generated by the proprietary ARM binary across 5 sensor lots:

Lot eapp Profile Glucose Errcode Stage Trendrate Debug fields
lot0 0.10067 Normal 100% ✓ 100% ✓ 100% ✓ 100% ✓ 99.97%
lot1 0.15 Extreme 100% ✓ 100% ✓ 100% ✓ 100% ✓ 100%
lot2 0.05 Low-eapp 100% ✓ 100% ✓ 100% ✓ 100% ✓ 99.98%
lot3 0.10067 Hypo (45 mg/dL) 100% ✓ 100% ✓ 100% ✓ 100% ✓ 99.91%
lot4 0.10067 Hyper (380+ mg/dL) 100% ✓ 100% ✓ 100% ✓ 100% ✓ 99.97%

All safety-critical outputs match 100%. Debug intermediate fields match 99.995% (263,987 / 264,000). The 13 remaining mismatches are in a single internal diagnostic field (err2_crt_current[1]) that does not affect any patient-facing output.

Known limitation: SG smooth_result_glucose

The Savitzky-Golay smoothed glucose output (smooth_result_glucose[0..5]) has ~391 mismatches per lot starting at seq 10. Analysis of the ARM binary disassembly revealed that the proprietary implementation uses a causal convolution with a data-dependent kernel that differs from the C reimplementation's weighted average approach. This is a limitation of the C reimplementation itself, faithfully reproduced in the Java port. This does not affect the primary result_glucose value, which matches 100%.

Public API usage

// 1. Configure from BLE advertisement data
SensorConfig config = new SensorConfig.Builder()
    .eapp(0.10067f)
    .vref(1.2f)
    .slope100(3.5226f)
    .basicWarmup(24)
    .err345Seq2(5)
    .wSgX100(new int[]{80, 130, 90, 80, 110, 90, 80})
    .build();

// 2. Create calibrator
CareSensCalibrator calibrator = new CareSensCalibrator(config);

// 3. On each BLE C5 notification (~every 5 minutes)
BlePacketParser.ParsedReading reading = BlePacketParser.parse(bleBytes);
CalibrationResult result = calibrator.processReading(
    reading.getSequenceNumber(),
    reading.getTimestamp(),
    reading.getAdcSamples(),
    reading.getTemperature());

if (result.isValid()) {
    double glucose = result.getGlucoseMgdl();    // or getGlucoseMmol()
    double trend = result.getTrendRateMgdlPerMin();
}

// 4. Persist state across app restarts
byte[] state = calibrator.saveState();
CareSensCalibrator restored = CareSensCalibrator.restoreState(state, config);

Medical safety considerations

This library produces glucose values that people with diabetes use to dose insulin. Incorrect values can cause dangerous hypoglycemia or hyperglycemia. The following safety measures are in place:

  • Oracle verification: every safety-critical output field matches the reference implementation across 2000 readings covering normal, hypoglycemic, and hyperglycemic profiles
  • Division-by-zero guards: all critical division paths (glucose computation, trendrate) are guarded against zero denominators
  • NaN/Infinity protection: isTrendAvailable() rejects NaN and Infinity; extreme temperature guard prevents near-zero slopeRatioTemp
  • Input validation: SensorConfig.Builder validates required fields; processReading() validates ADC array length
  • Immutable results: CalibrationResult and SensorConfig are immutable with defensive array copies
  • State versioning: serialized state includes a version header for forward compatibility
  • 50-reading Kalman convergence test: verifies the Holt-Kalman filter converges and produces finite glucose values over extended sequences

Design decisions

  1. Pure Java, zero Android dependencies — works as a JAR in any JVM environment. BLE transport is the host app's responsibility.
  2. Java 8 source compatibility — maximum Android device coverage.
  3. Package-private internals — only CareSensCalibrator, SensorConfig, CalibrationResult, and BlePacketParser are public. All algorithm internals are hidden.
  4. No build tool requirementbuild.sh auto-detects JDK and downloads JUnit. Also includes build.gradle for Gradle-based Android projects.
  5. Field names follow C originals (camelCased) — makes cross-referencing with the C implementation straightforward for verification.
  6. Generic library, not app-specific — designed for xDrip+, Jugluco, AndroidAPS, or any custom app.

Integration documentation

  • java/README.md — Quick start, API reference, build instructions, Android integration guide
  • java/INTEGRATION.md — xDrip+ integration analysis, specific file locations, data flow, generic design principles

Test plan

  • All 323 unit/integration tests pass
  • Oracle verification: 100% glucose match across 5 sensor lots (2000 readings)
  • End-to-end test: BLE bytes → parsed reading → calibrated glucose
  • State persistence: save/restore produces identical glucose continuity
  • Edge cases: NaN, Infinity, zero denominators, extreme temperatures, negative ADC
  • Code review: medical safety issues identified and fixed
  • Real sensor data validation (requires physical CareSens Air device)

Port all 7 C structs to Java model classes (CgmInput, AlgorithmOutput,
DeviceInfo, AlgorithmState, DebugOutput, CalibrationLog, CalibrationList)
and the 90x45 precomputed LOESS kernel coefficient table.
All 28 math utility functions ported from math_utils.c with NaN-aware
behavior matching C implementation exactly. 74 JUnit 5 tests covering
edge cases, NaN handling, and regression verification.
All 13 signal processing functions ported including the complex LOESS
pipeline (Hampel -> IRLS -> running median -> FIR -> trimmed mean),
Savitzky-Golay smoothing, regression calibration, and error helpers.
CheckError: all 7 error detectors (err1/2/4/8/16/32/128) ported with
51 TDD tests. Decomposed into well-named static methods.

OracleBinaryReader: reads packed binary oracle files (output_t, debug_t,
cgm_input_t) for field-by-field verification. 23 tests validating
against actual oracle data from 5 sensor lots.

Total: 194 tests passing.
Main calibration pipeline ported from calibration.c including all
static helpers: ADC conversion, IIR filter, drift correction,
temperature compensation, Holt-Kalman bias correction, trendrate.

All constants match C at full precision. 244 total tests passing.
Tests Java calibration against 2000 oracle readings (5 lots x 400).
Results: lots 0/3/4 glucose match 100%, lots 1/2 need fixes.
SG smoothing initialization and error edge cases identified.
- Lot1 (eapp=0.15): add eapp>=0.12 rejection with errcode=64
- Lot2 (eapp=0.05): use lot1 temp formula for all lot types,
  enable drift correction for all lot types (matches oracle)
- SG smoothing: fix output buffer positions [3..8] not [0..5],
  add zero-guard for unfilled convolution windows

All 2000 oracle readings now produce bit-identical glucose values.
250 tests passing.
INTEGRATION.md: public API design (CareSensCalibrator facade), packaging
as pure Java JAR via JitPack, xDrip+ integration points, generic design
for use by any Android CGM app.

SG smoothing: ARM binary analysis reveals the C reimplementation uses a
different convolution formula than the proprietary binary. This is a
known limitation in the C source, not a Java port issue. Primary
result_glucose values are 100% correct across all 2000 oracle readings.
Clean public API for any Android CGM app:
- SensorConfig: immutable sensor params with Builder pattern
- CalibrationResult: immutable result with glucose, trend, errors
- CareSensCalibrator: initialize, processReading, save/restore state

No Android dependencies. 29 new tests including oracle verification
through the facade. 279 total tests passing.
Quick start, API reference, build instructions, Android integration
guide, and architecture overview for the CareSensCalibrator facade.
- Internal classes (MathUtils, SignalProcessing, CheckError, etc.) made
  package-private; only public API facade classes remain public
- Replace 7 manual array shift loops with System.arraycopy
- Replace fully-qualified java.util.Arrays with import
- Simplify checkBoundary conditional return
- Remove unused import

All 279 tests still passing. No behavioral changes.
OracleVerificationTest: assert all safety-critical fields (errcode,
cal_available_flag, current_stage, trendrate) in addition to glucose.
Add detailed per-field mismatch reporting.

build.sh: robust standalone build script with JDK auto-detection,
JUnit auto-download, and compile/test/all subcommands.

build.gradle: target Java 8 for maximum Android compatibility.
- err1: compute i_sse_d_mean before epoch reset, write th_diff at n=1
- err4: fix min_diff to track signed breakthrough vs running minimum
- err2: fix crt_current[1] lagged glucose >= threshold

Debug match: 263987/264000 (99.995%). All patient-facing outputs
(glucose, errcode, trendrate, stage) match 100% across all 2000
oracle readings.
- Trendrate: add zero-denominator guards preventing NaN/Infinity
- SensorConfig: deep-copy DeviceInfo in Builder.build() for immutability
- CalibrationResult: guard isTrendAvailable() against NaN/Infinity
- SensorConfig: validate vref and slope100 independently
- CalibrationAlgorithm: reject slope100 <= 0 to prevent division by zero
- CareSensCalibrator: add version header to serialized state format

10 new tests, 290 total passing.
Parses 84-byte BLE C5 characteristic notifications into components
ready for CareSensCalibrator.processReading(). Includes temperature
conversion, uint16 ADC extraction, and immutable ParsedReading result.
10 new tests, 300 total passing.
Full flow: BLE bytes -> BlePacketParser -> CareSensCalibrator -> glucose.
Tests multi-reading sequences, state persistence, error handling, and
extreme values. 10 new tests, 310 total passing.
- Guard slopeRatioTemp near-zero division (extreme temperature)
- Add 50-reading Kalman convergence test
- Add oracle data existence check (prevents silent CI skip)
- Test mathRound overflow, NaN in statistics, negative ADC values,
  trendrate glucose boundary conditions (40.0, 500.0)

323 tests passing.
…cation

README: reflect both C and Java implementations, updated oracle results
table, Java quick start example, updated project structure.

CI: GitHub Actions workflow with 3 jobs:
- Java unit tests (323 tests)
- Oracle verification (downloads oracle data from release asset)
- C tests (cmake + ctest)
- Root settings.gradle includes java/ as :opencaresens-air module
- jitpack.yml configures JDK 11 for JitPack builds
- README: downstream integrations section with JitPack usage and
  notification list for major updates
@ErikDeBruijn ErikDeBruijn merged commit 95c816f into main Mar 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant