npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

enx-wasm-parser

v0.5.0

Published

Parse Teledyne RDI PD0/ENX acoustic data files via Rust/WebAssembly, off the main thread

Readme

enx-wasm-parser

A Rust/WebAssembly npm package that parses Teledyne RDI PD0/ENX binary acoustic Doppler current profiler (ADCP) files entirely off the main thread, converts GPS coordinates to local 3D Cartesian space at 64-bit precision, and exposes a clean TypeScript API.


Features

  • Pure Wasm core — all binary parsing happens in Rust compiled to WebAssembly; no native Node addons, no server round-trips
  • Off main thread — runs inside a Web Worker automatically; the UI never blocks
  • Zero-copy transfers — the input ArrayBuffer is transferred (not copied) to the worker; the binary output Float32Array is transferred back
  • Bottom-track velocity correction — when the file contains a Bottom Track block (0x0600), the boat's own velocity is automatically subtracted from each depth bin to produce true absolute water velocity; uncorrected pings are still returned (with btCorrectionApplied: false) so no data is lost
  • Side-lobe contamination removal — bins within 2 cell-lengths of the measured riverbed are automatically discarded; this is the standard ADCP physics cutoff that causes valid bin count to vary dynamically with water depth, consistent with DOLfYN and Teledyne's own reference software
  • Correct depth formula — bin range is computed as bin1_dist + i × cell_size using the firmware-computed bin1_dist field (Fixed Leader bytes 32–33), which already incorporates speed-of-sound and profiling-mode corrections; consistent with DOLfYN
  • Instrument config in metadata — every parse result includes numCells, cellSizeM, bin1DistM, theoreticalMaxDepthM, and coordSys from the Fixed Leader so callers can verify configuration without re-reading the file
  • Strict error handling — any malformed ensemble (bad magic bytes or checksum mismatch) throws immediately; no silent data corruption
  • Missing-data aware — Teledyne's -32768 sentinel is detected per bin; corrupt bins are dropped and counted in metadata
  • Two output modes — structured JSON for inspection/debugging, or a flat interleaved Float32Array ready for WebGL upload
  • f64 geo-precision — GPS→Cartesian conversion is done in 64-bit Rust before downcasting to f32 for output

Supported File Types

| Extension | Description | |---|---| | .enx | VmDas ensemble file (includes GPS nav block) | | .ens | VmDas ensemble file | | .sta | VmDas short-term average | | .lta | VmDas long-term average | | .pd0 | Raw WorkHorse PD0 binary (GPS nav block may be absent) |

Note: Raw .enr files do not contain the VmDas navigation block (0x2000). Pings without GPS data are skipped — they cannot be placed in Cartesian space.


Output Format

Binary mode (outputFormat: 'binary')

Returns a Float32Array with 7 floats per valid depth bin, across all pings, interleaved sequentially:

[ X, Y, Z, U, V, W, Intensity,   X, Y, Z, U, V, W, Intensity, ... ]

| Field | Unit | Description | |---|---|---| | X | meters | East offset from anchor | | Y | meters | Negative depth (−5 m = 5 m below surface) | | Z | meters | North offset from anchor | | U | m/s | Eastward velocity (BT-corrected when bottom track was present) | | V | m/s | Northward velocity (BT-corrected when bottom track was present) | | W | m/s | Vertical velocity (BT-corrected when bottom track was present) | | Intensity | counts (0–255) | Mean echo intensity across 4 beams |

JSON mode (outputFormat: 'json')

Returns a serialized JSON string of the form:

[
  {
    "timestamp": "2023-07-14T12:34:56.00Z",
    "x": 12.3,
    "z": -45.6,
    "bt_correction_applied": true,
    "bt_east_m_s": 1.234,
    "bt_north_m_s": -0.456,
    "bt_vertical_m_s": 0.012,
    "bins": [
      { "y": -5.0, "u": 0.123, "v": -0.045, "w": 0.002, "intensity": 87.25 }
    ]
  }
]

u/v/w in each bin are absolute water velocities when bt_correction_applied is true. When false (no bottom track block in that ensemble), they are instrument-relative and include the boat's motion.


Installation

npm install enx-wasm-parser

Or clone and build from source (see Building from Source).


Usage

import { ENXParser } from 'enx-wasm-parser';

const parser = new ENXParser();

// Get an ArrayBuffer from a file input, fetch, or fs.readFile
const fileBuffer: ArrayBuffer = /* ... */;

const result = await parser.parse(fileBuffer, {
  outputFormat: 'binary',   // 'binary' | 'json'  (default: 'json')
  anchor: {
    lat: 45.27,             // WGS-84 decimal degrees
    lon: -66.05,
  },
});

// Binary mode
const floats = result.data as Float32Array;
const totalBins = floats.length / 7;
console.log(`${totalBins} valid depth bins`);

// JSON mode
const pings = JSON.parse(result.data as string);
console.log(pings[0]);

// Metadata
console.log(result.metadata);
// {
//   totalPings: 1200,
//   validPings: 1198,
//   droppedCorruptBins: 14,
//   pingsWithoutGps: 0,           // >0 for raw PD0 files (no GPS block)
//   pingsWithBtCorrection: 1198,  // 0 means file has no bottom track data
//   durationMs: 42.3,
//   // Instrument configuration (from Fixed Leader, constant per deployment):
//   numCells: 80,
//   cellSizeM: 0.5,
//   bin1DistM: 1.67,              // range to centre of bin 1 from transducer
//   theoreticalMaxDepthM: 41.17,  // bin1DistM + (numCells-1) × cellSizeM
//   coordSys: 'earth'             // 'beam' | 'inst' | 'ship' | 'earth'
//                                 //  ⚠ if not 'earth', u/v/w are NOT east/north/up
// }

// Release the worker when done
parser.dispose();

File input example (browser)

document.querySelector('input[type=file]').addEventListener('change', async (e) => {
  const file = (e.target as HTMLInputElement).files![0];
  const buffer = await file.arrayBuffer();

  const parser = new ENXParser();
  const result = await parser.parse(buffer, {
    outputFormat: 'binary',
    anchor: { lat: 45.27, lon: -66.05 },
  });

  console.log(result.metadata);
});

Important: ArrayBuffer is transferred zero-copy to the worker. After calling parse(), the original buffer reference becomes detached (neutered). If you need to re-read the bytes, clone the buffer before passing it: buffer.slice(0).


API Reference

new ENXParser()

Creates a new parser instance. The Web Worker is not spawned until the first parse() call.


parser.parse(buffer, options): Promise<ParseResult>

Parses the binary file and returns a promise that resolves with the result.

Parameters:

| Name | Type | Required | Description | |---|---|---|---| | buffer | ArrayBuffer | Yes | The raw ENX/PD0 file bytes | | options.anchor | { lat: number, lon: number } | Yes | Geographic anchor point for Cartesian conversion | | options.outputFormat | 'json' \| 'binary' | No | Output format (default: 'json') |

Returns: Promise<ParseResult>

interface ParseResult {
  data: Float32Array | string;  // Float32Array for binary, JSON string for json
  metadata: {
    totalPings: number;              // all ensembles found in the file
    validPings: number;              // ensembles with all required acoustic blocks present
    droppedCorruptBins: number;      // bins dropped due to -32768 sentinel or side-lobe cutoff
    pingsWithoutGps: number;         // pings with no VmDas GPS block (raw PD0 files); x/z = null
    pingsWithBtCorrection: number;   // pings where boat velocity was subtracted; 0 = no BT data in file
    durationMs: number;              // parse wall-clock time in milliseconds
    // Instrument configuration (Fixed Leader — constant for the deployment):
    numCells: number;                // depth cells per ping
    cellSizeM: number;               // cell size in metres
    bin1DistM: number;               // range to centre of bin 1 from transducer face (metres)
    theoreticalMaxDepthM: number;    // bin1DistM + (numCells-1) × cellSizeM
    coordSys: 'beam' | 'inst' | 'ship' | 'earth'; // velocity coordinate system
  };
}

Throws: If the file fails magic-byte or checksum validation, the promise rejects with an Error describing the failure location.


parser.dispose()

Terminates the background Web Worker and rejects any pending parse() promises. Call this when the parser is no longer needed to free memory.


Building from Source

Prerequisites

| Tool | Install | |---|---| | Rust + Cargo | https://rustup.rs | | wasm-pack | cargo install wasm-pack | | Node.js ≥ 18 | https://nodejs.org |

Steps

# 1. Clone
git clone https://github.com/AmirhosseinAzimyzadeh/enx-wasm-parser
cd enx-wasm-parser

# 2. Build the Wasm module
npm run build:wasm
# Outputs to pkg/

# 3. Install JS dependencies
npm install

# 4. Type-check the TypeScript
npx tsc --noEmit

# 5. Run the example dev server
npm run dev
# → http://localhost:5173

Build scripts

| Script | What it does | |---|---| | npm run build:wasm | Runs wasm-pack build --target bundler --out-dir pkg | | npm run build:ts | Compiles TypeScript to dist/ | | npm run build | Runs both of the above in sequence | | npm run dev | Starts the Vite dev server for the example app |


Project Structure

enx-wasm-parser/
├── src/                        # Rust source
│   ├── lib.rs / lib.md         # wasm-bindgen entry point
│   ├── parser.rs / parser.md   # ensemble loop + checksum
│   ├── header.rs / header.md   # 0x7F 0x7F magic + offset table
│   ├── fixed_leader.rs         # block 0x0000 — num_cells, cell_size, bin1_dist, coord_sys
│   ├── variable_leader.rs      # block 0x0080 — timestamp
│   ├── velocity.rs             # block 0x0100 — u/v/w per bin
│   ├── echo_intensity.rs       # block 0x0300 — signal strength
│   ├── vmdas_nav.rs            # block 0x2000 — GPS lat/lon
│   ├── bottom_track.rs         # block 0x0600 — boat velocity + slant range to bed
│   ├── coords.rs               # GPS→Cartesian (f64) + bin depth
│   ├── output.rs               # JSON / Float32Array serialization
│   └── error.rs                # ParseError type
├── ts/                         # TypeScript source
│   ├── index.ts / index.md     # ENXParser public API class
│   ├── worker.ts / worker.md   # Web Worker entry point
│   └── types.ts                # Shared interfaces
├── example/                    # Vite demo app
│   ├── index.html
│   ├── main.ts
│   └── vite.config.ts
├── pkg/                        # wasm-pack output (gitignored)
├── Cargo.toml
├── package.json
├── tsconfig.json
├── vite.config.ts
└── RESEARCH.md                 # Binary format spec + design decisions

Every Rust and TypeScript module has a companion .md file documenting its exact purpose, binary layout, and data flow.


How It Works

Parse pipeline

File ArrayBuffer
  └─► Web Worker (off main thread)
        └─► Rust/Wasm: parse_enx()
              ├─ scan for 0x7F 0x7F ensemble boundaries
              ├─ validate 16-bit checksum per ensemble
              ├─ read Header → offset table
              ├─ parse Fixed Leader    → num_cells, cell_size, bin1_dist, coord_sys
              ├─ parse Variable Leader → timestamp
              ├─ parse Velocity block  → u/v/w per bin (drop -32768 sentinel bins)
              ├─ parse Echo Intensity  → mean of 4 beams per bin
              ├─ parse VmDas Nav       → GPS lat/lon (i32 × 1e-7)
              ├─ parse Bottom Track    → boat ENU velocity + slant range to bed
              ├─ subtract BT velocity  → true water velocity (when all 3 components valid)
              ├─ bin depth = bin1_dist + i × cell_size  (range from transducer, metres)
              ├─ drop bins where depth > bt_range − 2 × cell_size  (side-lobe zone)
              ├─ GPS → Cartesian (f64 equirectangular)
              └─ serialize → Float32Array or JSON
        └─► transfer result back to main thread
  └─► Promise resolves

Coordinate system

The anchor point (user-supplied lat/lon) becomes the origin (0, 0, 0).

  • X — East, meters
  • Y — negative depth, meters (Y = −5 means 5 m below surface)
  • Z — North, meters

Conversion uses the equirectangular approximation with midpoint-latitude correction, accurate to sub-meter for survey distances under ~500 km.

Depth calculation

Bin depth is the range from the transducer face to the centre of the bin (not absolute depth below the water surface):

depth_m = bin1_dist_m + bin_index × cell_size_m

bin1_dist_m comes from Fixed Leader bytes 32–33 — a firmware-computed value that already incorporates speed-of-sound corrections and profiling-mode adjustments. This is consistent with DOLfYN and Teledyne's own documentation. The older approximation (blank_distance + (i + 0.5) × cell_size) is not used.

Side-lobe contamination removal

ADCP beams have a finite opening angle (~20°). As the acoustic pulse approaches the riverbed, side-lobe energy reflects off the bottom before the main lobe does, contaminating the deepest bins with bed-echo rather than water velocity. The parser automatically discards any bin where:

depth_m > bt_range_m − 2 × cell_size_m

bt_range_m is the mean slant range to the bed measured by the Bottom Track block. The 2-cell margin matches the BOTTOM_OFFSET_BINS = 2 constant used in Teledyne's reference software. This filter causes valid bin count to vary dynamically per ping as the water depth changes — this is expected and correct.

Missing-data handling

Teledyne RDI uses the value -32768 as a "no valid data" sentinel for the East velocity component. When the parser encounters this value in a bin:

  1. The entire bin (all four velocity components + intensity) is discarded
  2. droppedCorruptBins in the metadata is incremented
  3. Surrounding bins in the same ping are unaffected

Error Handling

The parser is intentionally strict:

| Condition | Result | |---|---| | File does not start with 0x7F 0x7F | Promise rejects with fatal error | | Ensemble checksum mismatch | Promise rejects with fatal error | | Required block missing or malformed | Promise rejects with fatal error | | Velocity bin = -32768 (missing data) | Bin silently dropped, droppedCorruptBins incremented | | Bin depth > bt_range − 2 × cell_size | Bin silently dropped (side-lobe contamination zone), droppedCorruptBins incremented | | Bottom Track block absent | Bins kept; no side-lobe cutoff applied; velocities uncorrected (btCorrectionApplied: false) | | Bottom Track velocity component = -32768 | BT correction skipped for that ensemble; partial correction would introduce systematic error | | VmDas nav block absent in an ensemble | Ping included with x/z = null; pingsWithoutGps incremented |


License

MIT