Zum Hauptinhalt springen

Kochbuch

Kompakte, kopierfertige Rezepte für die Java-Bibliothek. Alle setzen das JPMS-Modul com.optimeas.osf auf dem Modulpfad voraus (Java 21) und importieren die öffentlichen Typen aus dem Paket com.optimeas.osf:

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

Fehlerbehandlung ist auf das Minimum gekürzt. Die Lese- und Schreib-APIs melden Fehler über die ungeprüfte OsfException (siehe Fehlerbehandlung); für den Gesamtüberblick über die Klassen siehe Architektur, Lesen und Schreiben.

Datei inspizieren (Metadaten + Kanalliste)

loadFromFile erkennt OSF4, OSF5 und komprimiertes OSFZ (gzip/zlib) transparent an den ersten Bytes — derselbe Aufruf für .osf und .osfz.

DataManager mgr = DataManager.loadFromFile(Path.of(pfad)); // .osf oder .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, Einheit: %s)%n",
ch.index(), ch.name(), ch.sampleCount(),
ch.physicalUnit() == null ? "-" : ch.physicalUnit());
}

Einen Kanal als double-Werte mit Zeitstempeln holen

asDoubles() verbreitert jeden numerischen Datentyp auf double; timestampsNs() liefert das parallele Zeitstempel-Array — bei äquidistanten Kanälen aus Startzeit und Abtastrate rekonstruiert, bei Timestamped-Kanälen explizit. Beide Arrays sind gleich lang.

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

long[] ts = ch.timestampsNs();
double[] v = ch.asDoubles(); // wirft OsfException.UnsupportedType, wenn nicht numerisch

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

channelByName liefert ein Optional<DataChannel>; alternativ greift channelByIndex(int) über den Metablock-Index zu.

Über gemischte Datentypen generisch iterieren

Ein switch über dataType() macht Export-Werkzeuge datentyp-agnostisch. Jeder as…()-Zugriff wirft OsfException.UnsupportedType, wenn er nicht zum Kanal passt — der switch verhindert das.

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: Kanal geladen, aber Werte nicht projizierbar */ }
}

Minimaler 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("kanal.csv"))) {
w.write("timestamp_ns,value\n");
for (int i = 0; i < v.length; i++) {
w.write(ts[i] + "," + v[i] + "\n");
}
}

Eine breite Tabelle (eine Spalte je Kanal, eine Zeile je Zeitstempel) erzeugt das osf-cli-Werkzeug mit osf convert --to csv — siehe Werkzeuge.

OSF4 → OSF5 konvertieren (auch OSFZ-Eingabe)

BlockWriter.fromManager rekonstruiert einen Writer aus einer geladenen Datei — Metadaten, Kanaldefinitionen und alle Samples. Die Ausgabe ist immer OSF5, unabhängig vom Quellformat; die Samples bleiben bitgenau.

DataManager mgr = DataManager.loadFromFile(Path.of("alt_osf4.osf")); // oder .osfz
BlockWriter.fromManager(mgr).writeToFile(Path.of("neu_osf5.osf"));

Für OSFZ-Ausgabe den BlockWriter in einen GZIPOutputStream schreiben:

import java.util.zip.GZIPOutputStream;

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

Neue Datei mit Analyse-Daten schreiben

Der BlockWriter sammelt alle Samples im Speicher und schreibt die Datei in einem Durchgang. Kanäle werden zuerst deklariert (der Rückgabewert ist der Kanal-Index), dann gefüllt.

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

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

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

w.writeToFile(Path.of("ergebnis.osf")); // created_utc wird automatisch gesetzt

Für äquidistante Analyse-Reihen (feste Rate, nur float/double) statt Timestamped:

int spektrum = w.addEquidistantChannel("ergebnis.psd", DataType.DOUBLE, 2, 1000.0); // 1 kHz
w.startEquidistantSegment(spektrum, startNs, block1); // double[]
w.appendEquidistantSamples(spektrum, block2); // hängt an dasselbe Segment an

Ausfallsichere Embedded-Aufzeichnungsschleife

Der StreamingWriter schreibt Präambel und jeden fertigen Block sofort und ruft nach jedem Block force(true) (fsync). Durabilität ist damit pro Block: Nach einem Stromausfall liest der DataManager die Datei bis zum letzten vollständigen Block und setzt stats().truncationSeen() für die angeschnittenen Reststbytes. Er implementiert Closeabletry-with-resources schreibt Restblöcke, forced und schließt.

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 tuer = w.addTimestampedChannel("tuer", DataType.BOOL, 2); // Event-Kanal
w.begin(); // Präambel früh festschreiben (sonst lazy beim ersten Sample)

while (laeuft) {
long now = jetztNs();

if (neuerMesswert) w.writeSample(temp, now, wert);
if (tuerGeaendert) w.writeSample(tuer, now, offen);

// Optional: w.flush() erzwingt die noch offenen Teilblöcke sofort.
warteAufNaechstenTick();
}
} // close(): Restblöcke schreiben, force, schließen

Der StreamingWriter fixiert sizeoflengthvalue je Kanal und kann es nicht nachträglich anheben — Kanäle mit großen variablen Samples (siehe unten) also gleich mit 4 deklarieren.

Bilder/Blobs als Binary-Kanal

Große variable Samples (JPEGs, Rohpuffer) überschreiten leicht das 2-Byte- Längenfeld, deshalb den Kanal mit sizeoflengthvalue = 4 anlegen. Über die Attribute-Map lässt sich ein mimetype in den Metablock schreiben. Variable Samples werden nie gebündelt — ein Block pro Sample.

// Schreiben (StreamingWriter — sov=4 wegen Sample-Größe):
int kamera = w.addTimestampedChannel(
"kamera.snapshots", DataType.BINARY, 4,
null, java.util.Map.of("mimetype", "image/jpeg"));

w.writeSample(kamera, tsNs, jpegBytes); // byte[]
// Lesen:
DataChannel ch = mgr.channelByName("kamera.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];
// … jpeg speichern/decodieren
}

Der BlockWriter kennt die maximale Sample-Größe vorab: dort genügt die Kurzform addTimestampedChannel(name, DataType.BINARY), die sizeoflengthvalue bei Bedarf automatisch von 2 auf 4 anhebt.

Integritätsprofil crc schreiben und prüfen

Beide Writer können das Level crc erzeugen: eine CRC32C über den Metablock im Magic-Header plus eine Frame-CRC32C je Block.

BlockWriter w = new BlockWriter();
w.setIntegrity(IntegrityProfile.CRC32C);
// … Kanäle + Samples …
w.writeToFile(Path.of("gesichert.osf"));

Beim Lesen verifiziert der DataManager fail-closed: eine falsche Metablock-CRC wirft OsfException.MetablockCrcMismatch, defekte Blöcke werden übersprungen und gezählt.

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

System.out.println(s.verificationStatus()); // "crc_valid" oder "invalid"
if (s.blocksCrcFailed() > 0) {
System.out.println("ACHTUNG: " + s.blocksCrcFailed() + " Blöcke mit CRC-Fehler");
}

Lese-Statistik ausgeben

ReaderStats s = mgr.stats();
System.out.printf("Blöcke gelesen: %d%n", s.blocksRead());
if (s.truncationSeen()) {
System.out.println("ACHTUNG: Datei war abgeschnitten");
}
if (s.compressed()) {
System.out.println("Quelle war OSFZ (" + s.compressionFormat() + ")");
}

Details zu den Interna (Blockstrom, Assembler, Chunking) stehen unter Interna; Bau und Maven-Koordinaten unter Bauen. Die Übersicht aller Werkzeuge liefert die Java-Startseite.

Dieses Dokument ist lizenziert unter CC BY 4.0. Namensnennung: optiMEAS GmbH und optiMEAS Switzerland GmbH.