Skip to content

Repository files navigation

meditech-cardiomera-decoder

License: MIT Python CI

An open-source decoder and reverse-engineered format specification for Holter ECG recordings produced by the Meditech CardioMera (FC1) ambulatory recorder (Meditech Kft, Budapest). It reads the raw SD-card files ECG.DAT and FC1_PRG.DAT, reconstructs the multi-channel ECG, and exports EDF+ that opens in any standard ECG viewer (EDFbrowser, WFDB tools, etc.).

Reconstructed 3-lead ECG (synthetic sample)

Reconstructed 3-lead ECG. This sample is generated from synthetic data (tests/synthetic.py) — no patient data is included anywhere in this repo.

The vendor's own software (CardioVisions) is Windows-only and there is no public documentation of the on-card file format. This project documents that format and provides a cross-platform (Python) reader so the data is not locked to one application.

Status: the byte-level layout here was reverse-engineered from a real recording and validated by reconstructing physiologically-correct ECG (see Verification). The physical acquisition parameters (resolution, rate, channels, calibration) are independently confirmed by the manufacturer's device manual (see References). Contributions and cross-checks against other CardioMera recordings are very welcome.


⚠️ Disclaimers

Medical. This software is not a medical device and produces no diagnosis. The optional arrhythmia script gives only a crude, rhythm-based estimate (it does not analyze QRS morphology and cannot distinguish ventricular vs supraventricular ectopy vs artifact). Always rely on a clinician and certified Holter analysis software for any clinical decision.

Privacy (PHI). FC1_PRG.DAT stores the patient name and ID in plaintext, and ECG.DAT is the patient's ECG. Treat both as protected health information. The provided .gitignore keeps all *.DAT, *.edf, *.npz and image/PDF exports out of the repository. Do not commit real recordings. All examples in this README are redacted / synthetic.


Features

  • Integrity report (recorded vs pre-allocated blocks, corrupt-block check).
  • Lossless reconstruction of the integrated signal from the stored deltas.
  • Baseline-drift handling with a pluggable strategy (leaky integrator by default; Butterworth high-pass / moving-average alternatives documented).
  • EDF+ export in real microvolts (using the manufacturer's 4 µV/count gain).
  • Optional rhythm-based ectopic-burden estimate with a per-hour breakdown.

Install

python3 -m pip install -r requirements.txt
# numpy, scipy, pyEDFlib, matplotlib

Usage

Put ECG.DAT and FC1_PRG.DAT in the working directory, then:

# Decode + export EDF+ (cardiomera.edf) with an integrity report
python3 meditech_decode.py

# Rough rhythm-based ectopic-burden estimate (writes arrhythmia_events.npz)
python3 meditech_arrhythmia.py

Both tools take CLI flags so they work on any CardioMera recording, not just the default 3-channel / 300 Hz layout:

python3 meditech_decode.py path/to/ECG.DAT path/to/FC1_PRG.DAT \
    -o out.edf --channels 3 --rate 300 --gain 4.0 --baseline butter
python3 meditech_decode.py --report-only            # metadata + integrity only

python3 meditech_arrhythmia.py path/to/ECG.DAT --rate 300 --detect-channel 2
python3 meditech_decode.py -h                        # full option list

Run the test suite (uses synthetic data — no real recording needed):

python3 tests/test_smoke.py        # or: pytest -q

Programmatic use:

import meditech_decode as m
meta  = m.parse_prg("FC1_PRG.DAT")          # patient/device/timestamps
integ = m.analyze_integrity("ECG.DAT")      # block accounting
sig   = m.reconstruct("ECG.DAT")            # (samples, 3) int32 ADC counts
sig   = m.correct_baseline(sig)             # remove slow drift for int16/EDF
m.export_edf(sig, meta, "cardiomera.edf")   # EDF+ in microvolts

File format specification

Everything below was derived by inspection of a real recording and confirmed by reconstructing clean ECG; treat it as a community spec, not a vendor document. All multi-byte integers are little-endian.

1. Files on the card

A programmed CardioMera card holds (at least) two files, listed in an embedded FAT-style directory inside FC1_PRG.DAT:

File Size (example) Purpose
FC1_PRG.DAT 2048 bytes Programming/configuration + patient data
ECG.DAT N × 1024 bytes Raw multi-channel ECG

The FC1 prefix is the model code for CardioMera (per the Meditech device manual), so FC1_PRG.DAT = "FC1 programming file".

2. ECG.DAT — the signal container

ECG.DAT is a flat array of 1024-byte blocks. The file is pre-allocated to a fixed number of blocks when recording starts; when the recorder powers off, the remaining blocks stay all-zero. So:

file size  = TOTAL_BLOCKS * 1024
recorded   = blocks [0 .. last non-zero block]
unused tail= all-zero blocks (NOT lost data — never written)

Do not treat the zero tail as corruption; it is normal unused buffer.

2.1 Block layout (1024 bytes)

offset  size  field
------  ----  ---------------------------------------------------------------
  0      1    tag        block type / flags: 0x1d, 0x19 or 0x0d
                         (does NOT change the data layout — data always @13)
  1      1    0x00       constant
  2      3    counter    24-bit LE, increments by +20 per block
                         (a device clock/index; NOT a sample count)
  5      8    reserved   housekeeping; typically 01 02 00 .. (varies slightly)
 13    1011   payload    3 interleaved channels of int8 deltas:
                         c0,c1,c2, c0,c1,c2, ...  (337 samples per channel)

Key facts, and why:

  • Header is 13 bytes for every block. Data starts at byte 13 regardless of the tag value. This is proven empirically: using offset 13 yields clean ECG for 0x1d, 0x19 and 0x0d blocks alike, and 1024 − 13 = 1011 = 3 × 337 divides evenly into 3 channels, so the interleave phase is preserved exactly across every block boundary.
  • The tag byte is a type/flag, not the header length. (0x1d=29, 0x19=25, 0x0d=13 as numbers, but the header is 13 in all cases.) Its exact meaning is not fully decoded; it does not affect sample extraction.
  • The counter increments by +20 per block and is a device clock/index — it does not equal the sample count (each block holds 337 samples/channel). A handful of blocks show a different step at segment boundaries; these are harmless and cancel out.

2.2 Sample encoding — int8 delta

Each payload byte is a signed 8-bit delta (int8). The reconstructed signal is the per-channel cumulative sum:

deltas  = payload.reshape(-1, 3).astype(int8)       # (337*nblocks, 3)
signal  = cumsum(deltas, axis=0)                    # ADC counts

This is confirmed by the byte histogram, which is a clean Laplacian centered on 0 (value 0 most common, then ±1, ±2, … symmetrically) — the signature of delta/differential coding, not raw samples. (A raw-int16 interpretation was tested and rejected: it is noisy and overflows the range.)

Baseline drift. A global cumulative sum is exact but accumulates a small DC bias (~+36 counts/block), so over a multi-day record it drifts into the millions and exceeds the int16 range EDF requires. The diagnostic content is in the AC component and is fully preserved; remove the slow drift with any standard ECG high-pass. correct_baseline() implements a leaky integrator (y[n]=α·y[n−1]+Δ, α≈0.999, ≈0.05 Hz) by default; a 0.05 Hz Butterworth high-pass gives the best ST/T fidelity.

2.3 Worked example — first block

Header + start of the first payload of a real ECG.DAT (the payload bytes are just ECG deltas, not identifying):

00000000: 1d 00 be 14 00 00 01 3f 02 00 00 00 45 1e 12 00 dd 1f f7 03
          ^^ ^^ ^^^^^^^^ ^^          ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ ...
          |  |  |        |           |                  └ payload: int8 deltas
          |  |  |        |           └ reserved / housekeeping (bytes 5..12)
          |  |  |        └ counter byte 3 (high)
          |  |  └ counter = 0x0014be = 5310 (LE, bytes 2..4)
          |  └ 0x00
          └ tag = 0x1d

Decoding: bytes 13..1023int8 → reshape to (337, 3)cumsum → the first 337 samples of each of the 3 channels.

3. FC1_PRG.DAT — configuration & patient data

A fixed 2048-byte structure. Notable fields (offsets from a real, redacted example; byte values for PHI fields replaced with JOHN DOE / 0 / X):

Offset Type / len Field
0x100 packed date (7 bytes) Year u16, then month, day, hour, min, sec (u8)
0x201 len-prefixed string Software name, e.g. CardioClip01 (len byte 0x0c)
0x20d packed date-ish Secondary timestamp block
0x243 len-prefixed string Patient name (len byte precedes, e.g. 0x0d)
0x28b len-prefixed string Patient name (second copy)
0x2af len-prefixed string Patient ID (len byte 0x09)
0x2cf double (TDateTime) Delphi timestamp (days since 1899-12-30)
0x2d7 double (TDateTime) Delphi timestamp
0x400 8.3 dir entries FAT-style directory: CARDIOMERA, FC1_PRG DAT, ECG DAT
0x610 ASCII string Device serial, e.g. 2019FCxxxxxxx
~0x158 ASCII ATDT modem dial strings (legacy telemetry)

Timestamps. Two encodings coexist:

  • A packed date at 0x100: EA 07 07 01 0A 14 26 → year 0x07EA=2026, month 07, day 01, hour 0x0A=10, minute 0x14=20, second 0x26=38 → 2026-07-01 10:20:38.
  • Delphi TDateTime doubles (days since 1899-12-30). These sit near 0x2cf/0x2d7 and give the recording/setup times. Note these are written at programming time and capture the start; there is no explicit end timestamp — compute the end from the decoded length (recorded_blocks × 337 / sample_rate).

Redacted dump of the key regions:

00000100: ea 07 07 01 0a 14 26 00  ...    packed date 2026-07-01 10:20:38
00000200: 0c 43 61 72 64 69 6f 43 6c 69 70 30 31 ea 07 07   .CardioClip01...
00000240: 00 00 0d 4a 4f 48 4e 20 44 4f 45 00 ...           patient name (REDACTED)
000002a0: ... 09 30 30 30 30 30 30 30 30 30 ...             patient ID (REDACTED)
000002c0: 00 .. 3a 58 26 ae 8d 8f e6 40  70 e7 38 9b 8d 8f e6 40   two TDateTime doubles
00000400: 43 41 52 44 49 4f 4d 45 52 41 20 ...  CARDIOMERA  (FAT-style directory)
00000420: 46 43 31 5f 50 52 47 20 44 41 54 20 ...  FC1_PRG DAT
00000440: 45 43 47 20 20 20 20 20 44 41 54 20 ...  ECG     DAT
00000610: 32 30 31 39 46 43 58 58 58 58 58 58 58   2019FCXXXXXXX  (serial, REDACTED)

4. Acquisition parameters & calibration

Confirmed by the Meditech device manual (see References):

Parameter Value
ECG channels up to 3 bipolar (or 5 unipolar); this recording: 3
A/D resolution 12 bit
Sampling (acq.) 1200 Hz or 600 Hz
Storage rate 600 / 300 / 150 Hz (selectable); this recording: 300 Hz
Dynamic range 16 mV peak-to-valley
Sensitivity 4 µV≈ 4 µV per ADC count (16 mV / 4096 = 2¹²)
Media / duration SD/MMC card, up to 96 h

Microvolt calibration. physical_µV ≈ ADC_count × 4 µV. The decoder uses ADC_GAIN_UV = 4.0 and writes EDF in µV. Caveat: the manual gives the hardware LSB; whether ECG.DAT stores exactly raw counts (vs a scaled value) should ideally be verified against a known 1 mV calibration pulse if one is present in the recording.

Active channel count and storage rate for a given recording are selectable and are almost certainly encoded somewhere in FC1_PRG.DAT; the exact field is not yet decoded, so this tool uses the empirically-determined values (3 ch, 300 Hz). The 300 Hz figure is corroborated physiologically (resting HR of 65–80 bpm falls out only at 300 Hz; 600 Hz would imply ~150 bpm, 150 Hz ~38 bpm).

5. Known unknowns / help wanted

  • Exact semantics of the block tag byte (0x1d / 0x19 / 0x0d).
  • Meaning of the +20 per-block counter's unit and the reserved header bytes 5–12.
  • Location of the channel-count and storage-rate fields inside FC1_PRG.DAT.
  • Confirmation that samples are raw counts (calibration against a known pulse).
  • Whether other CardioMera model codes (card(X)plore, CardiUp) share this block layout.

Verification

The format was validated end-to-end, not just asserted:

  • Delta encoding confirmed by the Laplacian byte histogram (0 ≫ ±1 ≫ ±2 …).
  • 3-channel interleave confirmed by a smoothness metric: reconstructing as 3 channels yields smooth per-channel waveforms; 1/2/4 channels do not.
  • Physiological ECG recovered — sharp QRS complexes with visible P and T waves, and a resting heart rate of 65–80 bpm at 300 Hz.
  • Integrity: on the sample recording, every recorded block had a valid tag and no zero/dropout blocks inside the recorded region.

Converting / viewing

The exported cardiomera.edf is standard EDF+ and opens in:

References

License

MIT. Not affiliated with or endorsed by Meditech Kft. All trademarks belong to their respective owners.

About

Open-source decoder & reverse-engineered file-format spec for Meditech CardioMera (FC1) Holter ECG recordings — parse ECG.DAT / FC1_PRG.DAT and export EDF+. Cross-platform Python alternative to CardioVisions.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages