Time Synchronization

Belay includes an automatic time synchronization feature that aligns the device's internal timer with the host computer's clock. This enables fairly accurate timestamping of events on the device relative to host time.

When to Use Time Synchronization

For most applications, host-side timestamps are simpler and sufficient:

import time
import belay

device = belay.Device("/dev/ttyUSB0", auto_sync_time=False)


@device.task
def read_sensor():
    return sensor.read()


# Simple approach - timestamp on the host when data arrives
value = read_sensor()
timestamp = time.time()  # Accurate enough for most use cases

Only use Belay's time-sync feature if you need:

  • High time accuracy (< 5ms) between device events and timestamps

  • Timestamps for data collected and buffered on-device

  • Precise correlation between multiple devices

Testing Your Connection Latency

Use the belay latency CLI command to measure your device/connection round-trip time:

belay latency /dev/ttyUSB0

If your measured latency is acceptable for your application (e.g., 10-50ms for USB), host-side timestamps are the simpler choice with zero overhead.

How It Works

When time synchronization is enabled (the default), Belay periodically synchronizes the device's time.ticks_ms() timer with the host computer's clock. This synchronization happens automatically on device creation, and piggybacks on every device communication.

The synchronization uses a simple offset calculation:

device_time_ms = time.ticks_ms()  # On device time in milliseconds
host_time_s = time.time()  # On host time in seconds
offset = host_time_s - (device_time_ms / 1000.0)

This offset is stored and used to convert device timestamps to host time:

device_time_ms = device("time.ticks_ms()")
host_time = (device_time_ms / 1000.0) - device.time_offset
print(f"Device event occurred at: {time.ctime(host_time)}")

Using return_time with Tasks

The simplest way to get accurate timestamps for task results is using the return_time parameter on @device.task:

import belay

device = belay.Device("/dev/ttyUSB0")


@device.task(return_time=True)
def read_sensor():
    return sensor.read()


# Returns (value, datetime) tuple
value, timestamp = read_sensor()
print(f"Sensor reading: {value} at {timestamp}")

When return_time=True, the task returns a tuple of (result, datetime.datetime) where the datetime represents the estimated midpoint time when the task executed on the device, converted to host time.

The timestamp is captured by measuring device time immediately before and after evaluating the expression, then averaging the two. This device-time is then converted to host-time using the time-offset-synchronization. This provides a good estimate of when the actual computation occurred, accounting for any time spent in the operation itself.

This also works with generator tasks - each yielded value becomes a (value, datetime) tuple:

@device.task(return_time=True)
def stream_readings(count):
    for _ in range(count):
        yield sensor.read()


for value, timestamp in stream_readings(10):
    print(f"{timestamp}: {value}")

You can also use return_time directly with device() calls:

# Get a value with its timestamp
result, timestamp = device("sensor.read()", return_time=True)

If time synchronization is not available (e.g., auto_sync_time=False and no manual sync_time() call), using return_time=True will raise a ValueError.

Enabling and Disabling

Time synchronization is enabled by default. You can disable it when creating a Device:

import belay

# With time synchronization (default)
device = belay.Device("/dev/ttyUSB0")

# Without time synchronization
device = belay.Device("/dev/ttyUSB0", auto_sync_time=False)

You can also disable it for the CLI latency command:

# With time synchronization (default)
belay latency /dev/ttyUSB0

# Without time synchronization
belay latency /dev/ttyUSB0 --without-timing

The only pragmatic downside to the default auto_sync_time=True is that it can slow down initial startup by a few dozen milliseconds.

Example

For a complete working example with code samples and detailed usage patterns, see Example 12: Time Synchronization.