lvtime
Lossless LabVIEW 128-bit timestamp (1904 epoch) to and from datetime, UTC, sub-microsecond preserved.
Install
pip install lvtime
Usage
from datetime import datetime, timezone
from lvtime import LVTimestamp, to_datetime, from_datetime, read_timestamp, write_timestamp
# A LabVIEW absolute time is a signed 64-bit second count since 1904-01-01 UTC
# plus an unsigned 64-bit fraction in units of 2**-64 s.
ts = LVTimestamp(seconds=3810888000, frac=1 << 63) # 2024-10-04 12:00:00.5 UTC
ts.to_datetime() # datetime(2024, 10, 4, 12, 0, 0, 500000, tzinfo=UTC)
# Build one from an aware datetime (naive input is rejected).
LVTimestamp.from_datetime(datetime(2024, 10, 4, tzinfo=timezone.utc))
# Read and write the 16-byte big-endian on-disk form (TDMS / "Flatten To String").
raw = write_timestamp(ts) # 16 bytes: 8 signed seconds + 8 unsigned fraction
read_timestamp(raw) == ts # True, every bit preserved
# The plain functions take and return the integer pair directly.
to_datetime(3810888000, 1 << 63)
seconds, frac = from_datetime(datetime(2024, 10, 4, tzinfo=timezone.utc))
LVTimestamp is the lossless carrier: it keeps the full 2**-64 s fraction, so two
instants a single fraction unit apart stay distinct even though they collapse to the
same microsecond when you ask for a datetime.
Unix time and float seconds
ts.to_unix() # 1728043200.5, float seconds since 1970-01-01 UTC
LVTimestamp.from_unix(1728043200.5)
ts.to_float() # 3810888000.5, seconds since 1904 (LabVIEW coerce-to-double)
LVTimestamp.from_float(3810888000.5)
Floats hold about 52 bits of mantissa, so these views are convenient but lossy below the microsecond. The integer pair stays exact.
Exact values with Decimal
When you need the real value and not a float, convert through Decimal (or plain int
or a str). This is the only way to reproduce NI's own documented bytes for a value
like -5.2 s, which a float cannot represent:
from decimal import Decimal
from lvtime import LVTimestamp, to_decimal, from_decimal
from_decimal("-5.2") # (-6, 14757395258967641293) == {0xFFFFFFFFFFFFFFFA, 0xCCCCCCCCCCCCCCCD}
LVTimestamp.from_decimal("-5.2").hex() # 'fffffffffffffffacccccccccccccccd', the exact NI bytes
LVTimestamp(10, 1 << 63).to_decimal() # Decimal('10.5'), exact seconds since 1904
to_decimal(0, 1) # Decimal('5.42...E-20'), one 2**-64 fraction unit, held exactly
from_decimal refuses a float on purpose, so a lossy value cannot slip in unnoticed;
reach for from_float when a float is really what you have. There are to_unix_decimal
and from_unix_decimal for the Unix-epoch view, and LVTimestamp.hex() /
LVTimestamp.from_hex() for the hex string you usually hold the flattened form as.
Endianness and batches
ts.to_bytes("little") # byte-reversed 16-byte form
LVTimestamp.from_bytes(raw, "little")
from lvtime import read_timestamps, write_timestamps
blob = write_timestamps([ts, LVTimestamp(0, 0)]) # back-to-back 16-byte records
read_timestamps(blob) # [LVTimestamp(...), LVTimestamp(0, 0)]
Command line
Installing the package puts an lvtime command on the path (also runnable as
python -m lvtime):
$ lvtime decode 00000000e32589408000000000000000
2024-10-04T12:00:00.500000+00:00
$ lvtime decode --format fields 00000000e32589408000000000000000
seconds=3810888000 frac=9223372036854775808 iso=2024-10-04T12:00:00.500000+00:00 unix=1728043200.5
$ lvtime encode 2024-10-04T12:00:00.5+00:00
00000000e32589408000000000000000
$ lvtime decode --format decimal 00000000e32589408000000000000000
epoch1904=3810888000.5000... unix=1728043200.5000...
$ echo 00000000e32589408000000000000000 | lvtime decode
2024-10-04T12:00:00.500000+00:00
decode also takes --format json, --format decimal (exact seconds, no float) and
--byteorder little; encode reads the ISO datetime from an argument or stdin and treats
a naive datetime as UTC. lvtime --version prints the version, and a bad input goes to
stderr with exit code 1. See lvtime --help.
How it works
A LabVIEW absolute time is 128 bits of fixed point: a signed 64-bit count of whole
seconds since 1904-01-01 00:00:00 UTC followed by an unsigned 64-bit fraction in
units of 2**-64 s. On disk ("Flatten To String", TDMS timestamp channels) the two fields
are stored big-endian, most significant byte first, for 16 bytes total. Converting to a
datetime means scaling the fraction to microseconds and adding to the 1904 epoch;
converting back maps the microsecond field to the nearest fraction unit. The full integer
pair is the only representation that keeps every bit.
Two caveats worth knowing
The NI timestamp overview
lists example values. Two of them trip people up, and lvtime is built around the byte
values rather than the labels:
- The 0.8 fraction only survives in fixed point. 0.8 s is exactly
0xCCCCCCCCCCCCCCCDin 2**-64 units (so -5.2 s is{0xFFFFFFFFFFFFFFFA, 0xCCCCCCCCCCCCCCCD}). Build that same fraction from a Cdoubleand it collapses to0xCCCCCCCCCCCCC000, losing the low bits, which NI's own page notes.from_float(-5.2)reproduces the lossy0xCCCCCCCCCCCCC000;from_decimal("-5.2")reproduces the exact0xCCCCCCCCCCCCCCCD. Use the Decimal path when the bytes have to match. - NI's page mislabels the 2002 row by a day. It labels
{3092601600, 0}(0xB8555B00) as "01/01/2002 00:00:00 UTC", but 3092601600 seconds after the 1904 epoch is 2001-12-31 00:00:00 UTC, one day earlier.lvtimedecodes the byte value, so it returns the correct 2001-12-31.
Why
LabVIEW stores absolute time as a 128-bit fixed-point value: a signed 64-bit count of whole seconds since 1904-01-01 00:00:00 UTC and an unsigned 64-bit fraction in units of 2**-64 s (NI, LabVIEW Timestamp Overview). Two things trip people up every time:
- The 1904 epoch. The Unix offset is exactly
2082844800seconds. Hand-rolled code that re-derives or mistypes this constant lands "off by 300 years" or by a timezone, a recurring complaint on the NI and LAVA forums. - The precision. The format resolves far below a nanosecond. Round-tripping through a
C
doubleor adatetimethrows most of that away (about a 0.5 microsecond step through a double). npTDMS's default read path surfaces timestamps as NumPydatetime64truncated to microseconds, which loses the fraction; it does expose the lossless pair withraw_timestamps=True, but only for timestamps read from inside a TDMS file.
Inside a TDMS file, then, npTDMS already covers the lossless case. What is missing is a
standalone codec for the raw 16-byte form that is not part of a TDMS file: Flatten To
String blobs, protocol payloads, anything where you hold the bytes directly. The LabVIEW-side
toolkits (OpenG, VIPM/JKI) run inside LabVIEW and ship no Python reader. lvtime fills that
gap: a tiny, typed, zero-dependency (stdlib only) library that keeps the exact integer pair
with the correct signedness, reads and writes the 16-byte on-disk form (big- or little-endian)
independent of any file container, and converts to a UTC-aware datetime only when you ask
(rounded to the microsecond, which is all datetime can hold).
Scope
lvtime handles the absolute-time value itself: the integer pair, the 16-byte record, the
datetime conversion, and the Unix and float-seconds views. It does not parse whole TDMS or
flattened-data files; it is the piece you reach for once you have the 16 raw bytes in hand, or
the seconds-and-fraction pair you need to turn into a real instant. LabVIEW follows the
Gregorian calendar and ignores leap seconds, and so does this library.
License
MIT. See LICENSE.
Links
- Website: https://lianes8studio.com
- GitHub: https://github.com/JaviLianes8
- LinkedIn: https://www.linkedin.com/in/jlianes/
Metadata
Release files for lvtime 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lvtime-0.3.0.tar.gz | 19.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lvtime-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.1 kB
Release files / lvtime-0.3.0.tar.gz
| Download URL | lvtime-0.3.0.tar.gz |
|---|---|
| Size | 19.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e5932524ccbba4b375904d7e2b59ddcc29d5ffae372e9e81d4eb25a3712f1432
|
|
BLAKE2b-256 checksum How to use checksums |
6a98942c4469b751a8d4371c4f64aeb2bce1cc8b5e6dc5ad2ea607b92d763f1b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Release files / lvtime-0.3.0-py3-none-any.whl
| Download URL | lvtime-0.3.0-py3-none-any.whl |
|---|---|
| Size | 13.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c1eb0c75fcc544cf07c4100af9ef251c633068d39426211427eb4e38e63e7a51
|
|
BLAKE2b-256 checksum How to use checksums |
001802f5fdd73ce70694fae0110e8d39c1344f7847196846716990d37f6dc6c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|