Écriture
La bibliothèque écrit exclusivement de l'OSF5 — même lorsque la source
était un fichier OSF4 ou OSFZ. Deux classes writer du paquet
com.optimeas.osf couvrent deux profils d'utilisation très différents :
StreamingWriter | BlockWriter | |
|---|---|---|
| Usage | Enregistrement embarqué | Analyse, conversion, export |
| Mémoire | limitée (un tampon de bloc par canal, libéré après l'émission) | collecte tous les échantillons en RAM |
| Durabilité | FileChannel.force(true) par bloc — tolérant aux coupures de courant | pas de force ; le fichier est produit à la fin |
| Cible | chemin de fichier (Path) | Path ou OutputStream quelconque (mémoire, socket) |
sizeOfLengthValue | fixe dès la déclaration du canal (le métabloc est déjà sur disque) | relèvement automatique de 2 à 4 pour les canaux variables |
| Cycle de vie | create() → canaux → begin() → écriture → close() | new → collecte → writeToFile() / writeTo() |
| Émission multiple | non (un fichier par instance, Closeable) | oui (writeTo* appelable à volonté) |
Les deux partagent les mêmes types de canaux, la même arithmétique de
chunking et les mêmes familles d'écriture (équidistant, numérique timestamped,
GPS, String/Binary). Pour les mêmes canaux, échantillons,
sizeOfLengthValue et created_utc, ils produisent des fichiers OSF5
identiques à l'octet près.
Déclarer des canaux — ChannelDef
Un canal n'est pas construit directement, mais déclaré via une méthode
add…Channel du writer, qui crée en interne une ChannelDef et renvoie
l'index de canal (séquentiel à partir de 0) utilisé par tous les appels
d'écriture. La ChannelDef (un record) décrit le canal tel qu'il figure
dans le métabloc :
public record ChannelDef(
int index, // Kanalindex (0..65535), vom Block-Strom referenziert
String name, // vollqualifizierter Kanalname (Pflicht)
DataType dataType, // aufgelöster Datentyp (Pflicht)
ChannelType channelType, // Datenform: SCALAR/VECTOR/MATRIX/BINARY
int sizeOfLengthValue, // Breite des Längenpräfix: 2 oder 4
long timeIncrementNs, // äquidistante Periode in ns; 0 = timestamped
String physicalUnit, // physikalische Einheit oder null
Map<String,String> attributes) { } // z. B. displayname, comment, reference
Deux modes de déclaration par writer :
// Timestamped-Kanal (jedes Sample trägt seinen eigenen Zeitstempel):
int rpm = writer.addTimestampedChannel("motor.drehzahl", DataType.DOUBLE, 2);
int evt = writer.addTimestampedChannel("ereignis", DataType.STRING, 2,
"1/min", Map.of("displayname", "Ereignis"));
// Äquidistanter Kanal (feste Rate; nur der Segmentstart trägt einen Zeitstempel):
int sig = writer.addEquidistantChannel("beschleunigung", DataType.DOUBLE, 4,
1000.0 /* Hz */);
Sont rejetés avec IllegalArgumentException : un nom vide, un type de
données null ou UNSUPPORTED, un sizeOfLengthValue ≠ 2/4, un canal
équidistant avec un type autre que FLOAT/DOUBLE, ainsi qu'une fréquence
d'échantillonnage non positive ou non finie. Pour le StreamingWriter, tout
appel add…Channel après begin() déclenche une OsfException (la phase de
configuration est alors terminée). Le channeltype est toujours normalisé en
scalar à l'écriture — l'équidistance n'est portée que par le
timeincrement, et non par le channeltype.
Bien choisir sizeOfLengthValue
Le champ de longueur de chaque bloc fait 2 ou 4 octets de large et limite la taille des blocs (~64 Ko ou ~2 Go). Règles pratiques :
- Canaux String/Binary avec de grands échantillons (images, audio,
blobs) : pour le
StreamingWriter, déclarer impérativement4— il ne peut plus modifier la valeur aprèsbegin(). LeBlockWriterrelève automatiquement de 2 à 4 pour un canal variable qu'il peut dimensionner lui-même (relèvement automatique à l'émission) ; pour lui,2comme valeur de départ est toujours correct. - Les canaux numériques ne sont jamais modifiés — ils sont au contraire
répartis sur davantage de blocs. Sur le
StreamingWriter,4épargne aux canaux à haute fréquence le découpage en de nombreux petits blocs soumis chacun à un fsync. - Sinon, rester sur la valeur par défaut
2(blocs plus compacts).
StreamingWriter — embarqué, tolérant aux pannes
import com.optimeas.osf.*;
try (StreamingWriter w = StreamingWriter.create(Path.of("aufzeichnung.osf"))) {
w.setMetadata("creator", "logger-fw/3.2"); // Metadaten vor begin()
w.setMetadata("tag", "pruefstand-7");
int rpm = w.addTimestampedChannel("motor.drehzahl", DataType.DOUBLE, 2);
int gps = w.addTimestampedChannel("fahrzeug.gps", DataType.GPS_LOCATION, 2);
w.begin(); // Header + Metablock auf Platte, fsync
while (running) {
w.writeSample(rpm, nowNs(), readRpm()); // je Block: kodieren, schreiben, fsync
}
} // close() emittiert Restblöcke + fsync
Garanties et comportement :
- Chaque
writeSamplerevenu est sur disque sous forme de bloc complet (FileChannel.force(true)= fsync). Après une coupure de courant, le fichier reste lisible jusqu'au dernier bloc confirmé ; le reader best effort restitue chaque bloc avant la coupure et signale un reste tronqué viaReaderStats.truncationSeen()(voir Lecture). - Préambule différé ou immédiat :
begin()écrit une fois la ligne d'en-tête magique (OSF5 <len>\n) et le métabloc JSON, et les soumet à fsync ; s'il n'est pas appelé explicitement, cela se fait automatiquement au premier échantillon. Ensuite, le métabloc n'est plus jamais touché — les setters de métadonnées n'ont d'effet qu'avant. created_utcest horodaté automatiquement lors debegin()s'il n'est pas défini (ISO-8601 UTC, par ex.2026-07-11T08:30:00Z).- Les canaux sont attachés à une famille de blocs : écrire le même canal
une fois en timestamped et une fois en équidistant provoque une
OsfException. close()est idempotent et émet les éventuels blocs restants en tampon ; en tant queCloseable, le writer appartient à un try-with-resources. Il n'est pas thread-safe — sérialiser les accès de l'extérieur.
Familles d'écriture
// Timestamped numerisch — Einzel- und Batch-Überladungen:
w.writeSample(ch, tsNs, 3.14); // double
w.writeSample(ch, tsNs, 42L); // beliebiger Integer-Kanal
w.writeSamples(ch, tsArray, werteArray); // parallele Arrays (Bulk)
// GPS (eigene Überladung):
w.writeSample(ch, tsNs, new GpsLocation(lat, lon, alt));
// String / Binary (ein Sample pro Block; OSF5: kein 0x00-Terminator):
w.writeSample(ch, tsNs, "Ereignis: Tür offen");
w.writeSample(ch, tsNs, jpegBytes); // byte[]
// Äquidistant (nur float/double; Rate stammt aus addEquidistantChannel):
w.startEquidistantSegment(ch, t0Ns, daten); // öffnet ein Segment
w.appendEquidistantSamples(ch, weitere); // verlängert das offene Segment
writeSample(int, long, long) dessert tout canal entier (int8…int64,
uint8…uint64) ; la valeur est rétrécie à la largeur du canal lors de
l'encodage. Chaque appel de lot et de segment est automatiquement découpé en
chunks selon la capacité de bloc du canal — un fsync par bloc émis. Un nouvel
appel startEquidistantSegment clôt le précédent et ouvre volontairement un
nouveau segment ; les lacunes entre segments sont le moyen conforme à la
spécification de représenter les pauses d'enregistrement.
BlockWriter — collecter et émettre
BlockWriter w = new BlockWriter();
w.setMetadata("creator", "analyse-tool/1.0");
int ch = w.addTimestampedChannel("messwert", DataType.DOUBLE); // Auto-Bump-Form
int sig = w.addEquidistantChannel("signal", DataType.DOUBLE, 4, 100.0);
w.startEquidistantSegment(sig, t0Ns, samples);
w.writeSample(ch, tsNs, 42.0);
w.writeToFile(Path.of("ergebnis.osf"));
var mem = new java.io.ByteArrayOutputStream(); // oder ein beliebiger OutputStream
w.writeTo(mem); // dieselbe Instanz erneut emittieren
- La famille
writeSample/startEquidistantSegmentreflète celle du writer en streaming (mêmes types, même validation), mais ne collecte qu'en mémoire ; le découpage en blocs conformes à la spécification n'intervient qu'à l'émission. writeToFile/writeTopeuvent être appelés plusieurs fois — la même instance peut être écrite simultanément dans un fichier et sur le réseau.- Relèvement automatique : un canal variable déclaré avec la forme à deux
arguments
addTimestampedChannel(name, type)démarre àsizeOfLengthValue = 2et est relevé à4pour l'émission si son plus grand échantillon dépasse le champ de longueur u16. La forme à trois arguments avec valeur explicite est respectée et rejette au contraire un échantillon trop grand (comme le StreamingWriter, qui ne peut pas relever). Les canaux numériques ne sont jamais relevés. - Pas de fsync — la durabilité incombe à l'appelant.
channelCount()etchannelIndex("name")aident lorsque les index ne sont pas conservés (channelIndexrenvoie-1pour un nom inconnu).
Valeurs par défaut automatiques des métadonnées
Les deux writers écrivent les entrées définies par setMetadata(key, value)
telles quelles dans l'objet osf.file du métabloc. La seule intervention
automatique :
| Champ | Comportement si non défini |
|---|---|
created_utc | toujours horodaté (heure UTC actuelle, YYYY-MM-DDTHH:MM:SSZ) — via putIfAbsent, une valeur déjà présente reste intacte |
tous les autres (creator, tag, …) | écrits uniquement s'ils sont définis ; jamais en tant que null |
Il n'y a donc pas de valeur par défaut imposée pour creator ou tag — ce
qui n'est pas défini n'apparaît pas dans le métabloc.
Profil d'intégrité à l'écriture (crc)
Les deux writers peuvent produire en option le profil d'intégrité OSF5 au
niveau crc — il est désactivé par défaut (IntegrityProfile.NONE) :
w.setIntegrity(IntegrityProfile.CRC32C); // vor begin() bzw. writeTo
Lorsqu'il est activé, le writer écrit
- un jeton
crc32cdans la ligne d'en-tête magique, qui porte le CRC du métabloc, et - pour chaque bloc de données, un CRC32C de trame ajouté (4 octets,
compté dans le champ de longueur du bloc). Pour le
StreamingWriter, le CRC de trame est rendu durable avec le mêmeforce(true)que le bloc lui-même.
La somme de contrôle est java.util.zip.CRC32C (native au JDK). Les 4
octets de CRC de trame prennent sur le budget de charge utile de chaque bloc,
de sorte qu'un peu moins d'échantillons tiennent par bloc — l'arithmétique de
chunking en tient compte automatiquement. La manière dont le reader vérifie
ces valeurs en mode fail-closed est décrite sous Lecture et
Gestion des erreurs.
Le niveau de signature (IntegrityProfile.ED25519) n'est pas pris en
charge par le writer et est rejeté avec une OsfException lors de l'écriture
du préambule.
Aller-retour et conversion
Réécrire un DataManager chargé — c'est aussi la conversion
OSF4 → OSF5 ou OSFZ → OSF5 :
DataManager mgr = DataManager.loadFromFile(Path.of("alt.osf")); // auch OSF4 / OSFZ
BlockWriter.fromManager(mgr).writeToFile(Path.of("neu.osf")); // immer OSF5
BlockWriter.fromManager(mgr) construit un writer à partir des canaux typés
et des échantillons du manager ; pour filtrer ou renommer avant l'écriture, on
continue de travailler sur le writer renvoyé.
Sont conservés : noms de canaux, types de données, valeurs d'échantillons (à
l'octet près), limites de segments et métadonnées du fichier — y compris
created_utc, car une valeur chargée est déjà définie et n'est pas
horodatée à nouveau. N'est pas repris : le sizeOfLengthValue (le writer
commence à 2 et relève les canaux variables si nécessaire) ; l'index de canal
est réattribué selon la position dans la liste.
Ce que les writers ne font volontairement pas
- Pas de sortie OSF4 — OSF5 est le seul format d'écriture.
- Pas de sortie OSFZ — la bibliothèque lit de manière transparente les fichiers empaquetés en gzip, mais ne compresse pas elle-même ; la compression est une étape en aval.
- Pas d'horodatages relatifs — le format temporel relatif est un héritage de lecture d'OSF4 ; les writers émettent des horodatages absolus.
- Pas de validation des horodatages — la monotonie n'est pas exigée par la spécification et n'est pas imposée.
- Pas de signature — le niveau Ed25519 est rejeté.
- Pas de sécurité des threads — sérialiser de l'extérieur les accès à une instance de writer.
Les détails de trame et de chunking sur lesquels reposent les deux writers sont décrits au chapitre Éléments internes ; l'architecture globale et les outils se trouvent sous Architecture, Outils et Build. Le Livre de recettes rassemble des exemples pour débuter ; la définition de format de référence est fournie par le chapitre Format OSF, la vue d'ensemble par l'implémentation Java.
Ce document est publié sous licence CC BY 4.0. Attribution : optiMEAS GmbH et optiMEAS Switzerland GmbH.