Source code for osi_utilities.tracefile.txth_writer
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026, Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
"""Text human-readable (.txth) trace file writer.
Writes OSI trace files in Google protobuf TextFormat.
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import IO
from google.protobuf import text_format
from google.protobuf.message import EncodeError, Message
from osi_utilities.tracefile.writer import TraceFileWriter
logger = logging.getLogger(__name__)
[docs]
class TXTHTraceFileWriter(TraceFileWriter):
"""Writer for text human-readable OSI trace files (.txth).
Messages are stored in Google protobuf TextFormat, one after another.
"""
def __init__(self) -> None:
self._file: IO[str] | None = None
self._path: Path | None = None
self._written_count = 0
[docs]
def open(self, path: Path) -> bool:
"""Open a .txth trace file for writing.
Args:
path: Path to the output file. Must have .txth extension.
Returns:
True on success, False on failure.
"""
if self._file is not None:
logger.error("Opening file '%s', writer has already a file opened", path)
return False
if path.suffix.lower() != ".txth":
logger.error("Text trace files must have .txth extension, got '%s'", path.suffix)
return False
try:
self._file = open(path, "w", encoding="utf-8") # noqa: SIM115
self._path = path
self._written_count = 0
return True
except OSError as e:
logger.error("Failed to open file '%s' for writing: %s", path, e)
return False
[docs]
def write_message(self, message: Message, topic: str = "") -> bool:
"""Write a protobuf message in TextFormat.
Args:
message: The protobuf message to write.
topic: Ignored for single-channel text files.
Returns:
True on success, False on failure.
"""
if self._file is None:
logger.error("Writer is not open")
return False
try:
text = text_format.MessageToString(message)
self._file.write(text)
self._written_count += 1
return True
except (OSError, EncodeError) as e:
logger.error("Failed to write message: %s", e)
return False
[docs]
def close(self) -> None:
"""Close the trace file."""
if self._file is not None:
self._file.close()
logger.info("Wrote %d messages to '%s'", self._written_count, self._path)
self._file = None
@property
def written_count(self) -> int:
"""Number of messages written so far."""
return self._written_count