Skip to main content

Cookbook

Compact, copy-paste recipes for the Java library. All of them assume the JPMS module com.optimeas.osf on the module path (Java 21) and import the public types from the com.optimeas.osf package:

import com.optimeas.osf.*;
import java.nio.file.Path;

Error handling is trimmed to the minimum. The read and write APIs report failures via the unchecked OsfException (see Error handling); for the class-level overview see Architecture, Reading and Writing.

Inspect a file (metadata + channel list)​

loadFromFile detects OSF4, OSF5 and compressed OSFZ (gzip/zlib) transparently from the leading bytes — the same call works for .osf and .osfz.

DataManager mgr = DataManager.loadFromFile(Path.of(path)); // .osf or .osfz

System.out.println("version: " + mgr.version()); // OSF4 / OSF5
System.out.println("creator: " + mgr.metadata().getOrDefault("creator", "-"));
System.out.println("created: " + mgr.metadata().getOrDefault("created_utc", "-"));
System.out.println("channels: " + mgr.channels().size());

for (DataChannel ch : mgr.channels()) {
System.out.printf(" [%d] %s (%d samples, unit: %s)%n",
ch.index(), ch.name(), ch.sampleCount(),
ch.physicalUnit() == null ? "-" : ch.physicalUnit());
}

Get one channel as double values with timestamps​

asDoubles() widens every numeric data type to double; timestampsNs() returns the parallel timestamp array — reconstructed from start time and sample rate for equidistant channels, explicit for timestamped channels. Both arrays have the same length.

DataChannel ch = mgr.channelByName("Motor.Speed").orElseThrow();

long[] ts = ch.timestampsNs();
double[] v = ch.asDoubles(); // throws OsfException.UnsupportedType if not numeric

for (int i = 0; i < v.length; i++) {
long tNs = ts[i];
double value = v[i];
// … process tNs, value
}

channelByName returns an Optional<DataChannel>; alternatively channelByIndex(int) looks up by the metablock index.

Iterate generically over mixed data types​

A switch over dataType() makes export tooling datatype-agnostic. Each as…() accessor throws OsfException.UnsupportedType when it does not match the channel — the switch prevents that.

long[] ts = ch.timestampsNs();
switch (ch.dataType()) {
case DOUBLE, FLOAT, INT8, INT16, INT32, INT64,
UINT8, UINT16, UINT32, UINT64, BOOL -> {
double[] v = ch.asDoubles();
for (int i = 0; i < v.length; i++) row(ts[i], v[i]);
}
case GPS_LOCATION -> {
GpsLocation[] g = ch.asGps();
for (int i = 0; i < g.length; i++) row(ts[i], g[i].latitude(), g[i].longitude());
}
case STRING -> {
String[] s = ch.asStrings();
for (int i = 0; i < s.length; i++) row(ts[i], s[i]);
}
case BINARY -> {
byte[][] b = ch.asBinaries();
for (int i = 0; i < b.length; i++) row(ts[i], b[i].length + " bytes");
}
default -> { /* UNSUPPORTED: channel loaded, but values not projectable */ }
}

Minimal CSV export​

import java.io.BufferedWriter;
import java.nio.file.Files;

DataChannel ch = mgr.channelByName(name).orElseThrow();
long[] ts = ch.timestampsNs();
double[] v = ch.asDoubles();

try (BufferedWriter w = Files.newBufferedWriter(Path.of("channel.csv"))) {
w.write("timestamp_ns,value\n");
for (int i = 0; i < v.length; i++) {
w.write(ts[i] + "," + v[i] + "\n");
}
}

A wide table (one column per channel, one row per timestamp) is produced by the osf-cli tool with osf convert --to csv — see Tools.

Convert OSF4 → OSF5 (also OSFZ input)​

BlockWriter.fromManager reconstructs a writer from a loaded file — its metadata, channel definitions and every sample. The output is always OSF5, regardless of the source format; samples stay bit-exact.

DataManager mgr = DataManager.loadFromFile(Path.of("old_osf4.osf")); // or .osfz
BlockWriter.fromManager(mgr).writeToFile(Path.of("new_osf5.osf"));

For OSFZ output write the BlockWriter into a GZIPOutputStream:

import java.util.zip.GZIPOutputStream;

try (var os = new GZIPOutputStream(Files.newOutputStream(Path.of("new.osfz")))) {
BlockWriter.fromManager(mgr).writeTo(os);
}

Write a new file with analysis data​

The BlockWriter accumulates every sample in memory and emits the file in one pass. Channels are declared first (the return value is the channel index), then filled.

BlockWriter w = new BlockWriter();
w.setMetadata("creator", "my-tool/1.0");

int fftPeak = w.addTimestampedChannel("result.fft_peak", DataType.DOUBLE);

for (var e : results) {
w.writeSample(fftPeak, e.timestampNs(), e.peakHz());
}

w.writeToFile(Path.of("result.osf")); // created_utc is injected automatically

For equidistant analysis series (fixed rate, float/double only) instead of timestamped:

int spectrum = w.addEquidistantChannel("result.psd", DataType.DOUBLE, 2, 1000.0); // 1 kHz
w.startEquidistantSegment(spectrum, startNs, block1); // double[]
w.appendEquidistantSamples(spectrum, block2); // extends the same segment

Crash-safe embedded recording loop​

The StreamingWriter writes the preamble and each completed block immediately and calls force(true) (fsync) after every block. Durability is therefore per block: after a power loss the DataManager reads the file up to the last whole block and sets stats().truncationSeen() for the trailing partial bytes. It implements Closeable — try-with-resources emits any remaining blocks, forces and closes.

try (StreamingWriter w = StreamingWriter.create(Path.of("/data/rec_0001.osf"))) {
w.setMetadata("creator", "logger-fw/3.2");

int temp = w.addTimestampedChannel("temp", DataType.DOUBLE, 2, "degC", null);
int door = w.addTimestampedChannel("door", DataType.BOOL, 2); // event channel
w.begin(); // pin the preamble early (otherwise lazy on the first sample)

while (running) {
long now = nowNs();

if (newSample) w.writeSample(temp, now, value);
if (doorChanged) w.writeSample(door, now, open);

// Optional: w.flush() forces the still-open partial blocks now.
waitForNextTick();
}
} // close(): emit remaining blocks, force, close

The StreamingWriter fixes sizeoflengthvalue per channel and cannot bump it later — so declare channels with large variable samples (see below) with 4 up front.

Images/blobs as a binary channel​

Large variable samples (JPEGs, raw buffers) easily exceed the 2-byte length field, so declare the channel with sizeoflengthvalue = 4. An attributes map lets you write a mimetype into the metablock. Variable samples are never batched — one block per sample.

// Writing (StreamingWriter — sov=4 because of the sample size):
int camera = w.addTimestampedChannel(
"camera.snapshots", DataType.BINARY, 4,
null, java.util.Map.of("mimetype", "image/jpeg"));

w.writeSample(camera, tsNs, jpegBytes); // byte[]
// Reading:
DataChannel ch = mgr.channelByName("camera.snapshots").orElseThrow();
long[] ts = ch.timestampsNs();
byte[][] blobs = ch.asBinaries();

for (int i = 0; i < blobs.length; i++) {
long t = ts[i];
byte[] jpeg = blobs[i];
// … store/decode jpeg
}

The BlockWriter knows the maximum sample size up front: there the short form addTimestampedChannel(name, DataType.BINARY) is enough — it auto-bumps sizeoflengthvalue from 2 to 4 when needed.

Write and verify the crc integrity profile​

Both writers can emit level crc: a CRC32C over the metablock in the magic header plus a frame CRC32C per block.

BlockWriter w = new BlockWriter();
w.setIntegrity(IntegrityProfile.CRC32C);
// … channels + samples …
w.writeToFile(Path.of("secured.osf"));

On read the DataManager verifies fail-closed: a wrong metablock CRC throws OsfException.MetablockCrcMismatch, corrupt blocks are skipped and counted.

DataManager mgr = DataManager.loadFromFile(Path.of("secured.osf"));
ReaderStats s = mgr.stats();

System.out.println(s.verificationStatus()); // "crc_valid" or "invalid"
if (s.blocksCrcFailed() > 0) {
System.out.println("WARNING: " + s.blocksCrcFailed() + " blocks failed their CRC");
}
ReaderStats s = mgr.stats();
System.out.printf("blocks read: %d%n", s.blocksRead());
if (s.truncationSeen()) {
System.out.println("WARNING: the file was truncated");
}
if (s.compressed()) {
System.out.println("source was OSFZ (" + s.compressionFormat() + ")");
}

Internals (block stream, assembler, chunking) are covered under Internals; build and Maven coordinates under Building. The overview of all tools is on the Java landing page.

This document is licensed under CC BY 4.0. Attribution: optiMEAS GmbH and optiMEAS Switzerland GmbH.