phcsync

package
v0.0.0-...-3515dcf Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 1, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Clock

type Clock interface {
	SetFreqOffset(float64) error
	FreqOffset() (float64, error)
	MaxFreqOffset() float64
	AdjTime(d time.Duration) (phctime.Era, error)
}

Clock interface represents a PHC (PTP Hardware Clock) that can be adjusted.

type Config

type Config struct {
	Reset    ResetConfig      `toml:"reset" comment:"Reset mode parameters"`
	Converge ConvergingConfig `toml:"converge" comment:"Converging mode parameters"`
	Track    TrackingConfig   `toml:"track" comment:"Tracking mode parameters"`
}

Config contains tunable parameters for the Controller.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with sensible default values.

func (*Config) Validate

func (cfg *Config) Validate() error

type Controller

type Controller struct {
	// contains filtered or unexported fields
}

Controller coordinates PHC synchronization.

func NewController

func NewController(
	clock Clock,
	sampler Sampler,
	gm *ptpgm.Grandmaster,
	cfg Config,
	leapSecond ptime.LeapSecond,
	edgesPerPulse int,
	lg *slog.Logger,
) (*Controller, error)

NewController creates a new Controller instance. The Config must be validated before calling this function.

func (*Controller) Close

func (c *Controller) Close()

Close gracefully shuts down the controller.

func (*Controller) LeapSecond

func (c *Controller) LeapSecond(ls ptime.LeapSecond)

LeapSecond handles leap second information updates.

func (*Controller) Mode

func (c *Controller) Mode() Mode

Mode returns the current operating mode of the controller.

func (*Controller) Pause

func (c *Controller) Pause()

Pause handles pause events from the timestamp worker. This is called when the network interface loses carrier. It resets sync state and transitions back to reset mode.

func (*Controller) PulseEdge

func (c *Controller) PulseEdge(edge PulseEdge)

PulseEdge handles edge timestamp events from the PHC.

func (*Controller) SetTimeMsgBuffer

func (c *Controller) SetTimeMsgBuffer(buf TimeMsgBuffer)

SetTimeMsgBuffer sets the time message buffer for the controller. This must be called exactly once after construction, before any PulseEdge or TimeMessage calls.

func (*Controller) Tick

func (c *Controller) Tick(now time.Time)

Tick handles regular tick events (0.25s intervals).

func (*Controller) TimeMessage

func (c *Controller) TimeMessage()

TimeMessage handles notification that a time message occurred.

type ConvergingConfig

type ConvergingConfig struct {
	// Kp is the proportional gain for the PI servo used during converging mode.
	// Higher values make the servo more responsive but may cause oscillation.
	Kp float64 `toml:"kp" check:">0.0,<10.0" comment:"PI servo proportional gain"`

	// Ki is the integral gain for the PI servo used during converging mode.
	// This accumulates error over time to eliminate steady-state offset.
	Ki float64 `toml:"ki" check:">=0.0,<10.0" comment:"PI servo integral gain"`

	// MedianWindow is the number of samples in the sliding window for computing the median
	// of absolute offsets. The median is used to track convergence progress: when it stops
	// decreasing and stabilizes below OffsetLimit, converging mode exits to tracking mode.
	MedianWindow int `toml:"medianWindow" check:">=3,<100" comment:"Number of samples for median window"`

	// OffsetLimit is the maximum acceptable absolute offset in nanoseconds for declaring
	// convergence complete. Converging mode exits to tracking when both conditions hold:
	// (1) the median of absolute offsets has not decreased for StableWindow consecutive samples,
	// and (2) every sample since the minimum median was observed has absolute offset <= OffsetLimit.
	// If any sample exceeds this limit, the stability counter resets.
	OffsetLimit int64 `toml:"offsetLimit" check:">0,<=10000" comment:"Max offset to declare converged (ns)"`

	// StableWindow is the number of consecutive samples for which the minimum median must
	// remain stable (not decrease) before exiting converging mode. This ensures the offset
	// has truly stabilized rather than just momentarily dipping below the threshold.
	StableWindow int `toml:"stableWindow" check:">=1,<100" comment:"Number of stable samples before exit"`

	// BadSampleLimit is the maximum number of consecutive missing samples before transitioning
	// back to reset mode. Missing samples indicate loss of PPS signal or time messages.
	BadSampleLimit int `toml:"badSampleLimit" check:">=1,<100" comment:"Max consecutive bad samples before reset"`

	// StepCompensate enables compensation for ADJ_SETOFFSET delay at the beginning of
	// converging mode. ADJ_SETOFFSET is implemented in the kernel by calling the driver
	// to read the current PHC time, adding the offset, then calling the driver again to
	// write back the adjusted time. This read-modify-write sequence takes a few microseconds,
	// causing the clock to lag behind by that amount. If enabled, the offset from the first
	// pulse in converging mode is used to apply a compensation step.
	StepCompensate bool `toml:"stepCompensate" comment:"Compensate for ADJ_SETOFFSET delay"`
}

ConvergingConfig contains tunable parameters for converging mode.

type Mode

type Mode int

Mode represents the mode in which the Controller is operating.

const (
	ModeInvalid Mode = iota
	ModeReset
	ModeConverging
	ModeTracking
	NModes
)

func (Mode) InSync

func (m Mode) InSync() bool

InSync returns true if the mode represents a synchronized state

func (Mode) String

func (m Mode) String() string

type MultiSampler

type MultiSampler struct {
	// contains filtered or unexported fields
}

MultiSampler fans out Sample calls to multiple samplers

func NewMultiSampler

func NewMultiSampler(samplers ...Sampler) *MultiSampler

NewMultiSampler creates a new MultiSampler that fans out to multiple samplers

func (*MultiSampler) Sample

func (m *MultiSampler) Sample(data Sample)

Sample implements Sampler by calling Sample on all samplers

func (*MultiSampler) Samplers

func (m *MultiSampler) Samplers() iter.Seq[Sampler]

Samplers returns an iterator over the samplers

type PulseEdge

type PulseEdge struct {
	Timestamp phctime.Time   // PHC clock timestamp for the pulse edge
	TRead     phctime.Sample // PHC and system time immediately after the timestamp event was read
}

type PulseType

type PulseType struct {
	EdgesPerPulse int
	PulseWidth    time.Duration
}

PulseType describes the pulse characteristics.

type ResetConfig

type ResetConfig struct {
	// PulseWindow is the number of pulses to collect for alignment analysis during reset mode.
	// A larger window provides more data for statistical checks but delays the initial clock step.
	PulseWindow int `toml:"pulseWindow" check:">=3,<100" comment:"Number of pulses for alignment analysis"`

	// StepThreshold is the minimum absolute offset in nanoseconds required to perform a clock step.
	// If the measured offset is smaller than this threshold, reset mode transitions directly to
	// converging mode without stepping the clock.
	StepThreshold int64 `toml:"stepThreshold" check:">=0,<1_000_000" comment:"Min offset to trigger clock step (ns)"`

	// PulseVariation is the maximum acceptable variation between consecutive pulse intervals,
	// expressed in parts per billion (PPB). This checks clock stability: if the variation
	// between the shortest and longest interval exceeds this limit, the pulses are rejected.
	// The variation is computed as: (maxInterval/minInterval - 1.0) * 1e9.
	PulseVariation float64 `toml:"pulseVariation" check:">=5,<1_000_000" comment:"Max pulse interval variation (ppb)"`

	// ExpectedDelay is the expected pulse-to-message delay in seconds.
	// This represents the typical delay between when a PPS pulse occurs and when the GPS
	// receiver sends the corresponding time message. Most GPS receivers send messages
	// 50-250ms after the pulse.
	ExpectedDelay float64 `toml:"expectedDelay" check:">=0.0,<1.0" comment:"Expected pulse-to-message delay (s)"`

	// DelayConfidenceWindow specifies what fraction of the maximum possible delay window
	// to accept, expressed as a proportion (0.0 to 1.0). The accepted window has width
	// DelayConfidenceWindow*maxWindow and includes ExpectedDelay. If centering that window
	// on ExpectedDelay would make the lower bound negative, it is shifted up so the lower
	// bound is 0 while keeping the window width the same.
	DelayConfidenceWindow float64 `toml:"delayConfidenceWindow" check:">0.0,<=1.0" comment:"Fraction of delay window to accept [0,1]"`

	// DelayVariation is the maximum acceptable spread between pulse-to-message delays,
	// expressed as a proportion of the maximum window (1.0 second). This checks consistency:
	// all delays should be similar. The spread is computed as: (maxDelay - minDelay) / 1.0.
	// Should be significantly smaller than DelayConfidenceWindow to ensure tight clustering.
	DelayVariation float64 `toml:"delayVariation" check:">0.0,<1.0" comment:"Max delay spread as fraction of window"`

	// PulseWidthDetectLimit is the maximum pulse width in seconds that can be automatically
	// detected in dual-edge mode to determine which edge is leading. Pulse widths greater
	// than this value (and by symmetry, less than 1.0 - this value) are too close to 50%
	// duty cycle for reliable auto-detection from timing alone. When detection fails, both
	// edge lists are kept and alignment with time messages is used. Note: pulse widths
	// greater than 0.5 seconds must be explicitly configured via gps.pulseWidth.
	PulseWidthDetectLimit float64 `toml:"pulseWidthDetectLimit" check:">=0.1,<0.5" comment:"Max auto-detectable pulse width (s)"`

	// DriftRateLimit is the maximum drift rate in PPB that reset mode will accept when
	// validating a candidate step against the persisted sample from tracking mode.
	// If the implied drift rate exceeds this limit, the step is rejected and the system
	// remains in reset mode. This prevents re-locking to a bad phase after GPS issues.
	// Set to 0 to disable drift rate checking.
	DriftRateLimit float64 `toml:"driftRateLimit" check:">=0,<1_000_000_000" comment:"Max drift rate to accept step (PPB)"`
}

ResetConfig contains tunable parameters for reset mode.

func (ResetConfig) DelayBounds

func (cfg ResetConfig) DelayBounds(maxWindow float64) (minAcceptable, maxAcceptable float64)

DelayBounds returns the acceptable delay range in seconds for a given maxWindow. The range has width DelayConfidenceWindow*maxWindow, includes ExpectedDelay, and never extends below 0 seconds.

type Sample

type Sample struct {
	Kind      SampleKind    // Determines validity of other fields
	Ref       ptime.Time    // GPS reference time (different from system time)
	Offset    time.Duration // PHC/GPS offset (valid for SampleOK and SampleOutlier, 0 for SampleMissing)
	Freq      float64       // Current frequency adjustment in PPB (always valid)
	FreqDelta float64       // Change in frequency adjustment in PPB (valid for SampleOK, 0 for SampleOutlier)
	Mode      Mode          // Current synchronization mode (always valid)
	Era       phctime.Era   // For clock step tracking and logging (always valid)
	EdgeIndex uint64        // Tracks which edge produced this sample (odd/even)
	Sys       time.Time     // Estimated monotonic system time of pulse
}

Sample contains all information about a synchronization sample

func (*Sample) SysSample

func (s *Sample) SysSample() phctime.Sample

SysSample returns a phctime.Sample pairing the GPS reference time with the estimated system time of the pulse.

type SampleKind

type SampleKind int

SampleKind determines the validity and type of a synchronization sample

const (
	SampleMissing SampleKind = iota // Missing sample (PPS signal not received)
	SampleOK                        // Valid sample within acceptable limits
	SampleOutlier                   // Sample that is an outlier but still measured
)

type Sampler

type Sampler interface {
	// Sample reports a clock synchronization sample of any kind
	Sample(data Sample)
}

Sampler handles clock synchronization samples of all types

type TimeMsgBuffer

type TimeMsgBuffer interface {
	// GetPostTimeMessages retrieves n time messages.
	// It returns the reference time of the last message and
	// the read times of all the messages in chronological order.
	// The read times must have a valid monotonic part.
	// The messages must be for consecutive seconds;
	// the reference time of each time message is one greater than the previous one.
	// The last message must not be stale i.e. there must not be a time message of the same type with a later reference time.
	// The messages must be the same GNSS message type, which must be of a type that follows the time pulse.
	// If n such messages are not available, the slice will be empty and lastSec will be zero.
	GetPostTimeMessages(n int) (lastSec ptime.Time, tRead []time.Time)
	// GetPulseCorrection retrieves the pulse offset correction (PulseOffset) for a given reference time.
	// The returned correction satisfies: true_time_of_second = pulse_time + correction
	// Returns (correction, true) if available, (0, false) otherwise.
	GetPulseCorrection(refTime ptime.Time) (time.Duration, bool)
	WaitForPulseCorrection(refTime ptime.Time) bool
}

TimeMsgBuffer is an interface for the Controller to access time messages from the receiver.

type TrackingConfig

type TrackingConfig struct {
	// Kp is the proportional gain for the PI servo used during tracking mode.
	// Lower than converging mode for stability during normal operation.
	Kp float64 `toml:"kp" check:">0.0,<10.0" comment:"PI servo proportional gain"`

	// Ki is the integral gain for the PI servo used during tracking mode.
	// Lower than converging mode to prevent overcorrection during stable tracking.
	Ki float64 `toml:"ki" check:">0.0,<10.0" comment:"PI servo integral gain"`

	// MADThreshold is the minimum absolute offset in nanoseconds for a sample to be
	// considered for MAD-based outlier detection. Samples with |offset| < MADThreshold
	// are always accepted. Samples with |offset| >= MADThreshold are evaluated using
	// MAD statistics to determine if they are outliers. This ensures samples within the
	// normal range of tracking jitter are not treated as outliers.
	MADThreshold int64 `toml:"madThreshold" check:">0,<=1_000_000" comment:"Min offset to use MAD for outlier detection (ns)"`

	// MADWindow is the number of samples in the sliding window for MAD-based outlier detection.
	// The window stores offset history used to compute the median and median absolute deviation.
	// Larger windows provide more robust outlier detection but respond more slowly to changing
	// conditions.
	MADWindow int `toml:"madWindow" check:">=3,<100" comment:"Number of samples for MAD window"`

	// MADMultiple is the multiple of MAD (Median Absolute Deviation) used as the outlier threshold.
	// A sample is classified as an outlier if its offset is more than MADMultiple * MAD away from
	// the median offset. Higher values make outlier detection more conservative (fewer rejections).
	MADMultiple float64 `toml:"madMultiple" check:">0.0,<1000.0" comment:"MAD multiple for outlier threshold"`

	// MADMinSamples is the minimum number of samples required in the MAD window before MAD-based
	// outlier detection is active. Until this threshold is reached, only the OutlierThreshold
	// is checked. This ensures MAD statistics are based on sufficient data.
	MADMinSamples int `toml:"madMinSamples" check:">=3,<100" comment:"Min samples before MAD detection active"`

	// OutlierThreshold is the absolute offset in nanoseconds above which a sample is unconditionally treated
	// as an outlier. Above this threshold, MAD detection is bypassed and the sample is rejected outright.
	// During MAD warmup, this is the only outlier check performed.
	OutlierThreshold int64 `toml:"outlierThreshold" check:">0,<=1_000_000" comment:"Unconditional outlier threshold (ns)"`

	// PulseWidthTolerance is the tolerance in nanoseconds for filtering trailing edges in
	// dual-edge mode based on temporal spacing. An edge is considered a trailing edge (and
	// ignored) if it arrives within PulseWidthTolerance of the expected trailing edge time
	// (lastEdgeTime + PulseWidth). This helps distinguish leading from trailing edges when
	// alignment alone is ambiguous.
	PulseWidthTolerance int64 `toml:"pulseWidthTolerance" check:">0,<=10000" comment:"Tolerance for trailing edge filter (ns)"`

	// AlignTolerance is the tolerance in nanoseconds for offset from top of second to
	// immediately accept an edge as a leading edge. Edges with |offset| <= AlignTolerance
	// (where offset is timestamp rounded to nearest second) are assumed to be leading edges
	// and accepted without further checks. This is the primary discriminator for
	// well-synchronized clocks.
	AlignTolerance int64 `toml:"alignTolerance" check:">0,<=10000" comment:"Tolerance for leading edge detection (ns)"`

	// BadSampleRunLimit is the maximum number of consecutive bad samples (missing or outlier)
	// allowed while remaining in tracking mode. Exceeding this limit triggers a reset.
	BadSampleRunLimit int `toml:"badSampleRunLimit" check:">=1,<1000" comment:"Max consecutive bad samples before reset"`

	// OutlierRatioLimit is the maximum ratio of MAD window samples that can be outliers
	// before triggering a reset. Only counts samples admitted to the MAD window and later
	// classified as outliers by MAD detection; extreme outliers rejected by OutlierThreshold
	// are not counted. This prevents the median from being corrupted by accumulated outliers.
	OutlierRatioLimit float64 `toml:"outlierRatioLimit" check:">0.0,<=1.0" comment:"Max outlier ratio in MAD window"`

	// BadSampleWindow is the size of the sliding window for tracking bad sample frequency.
	// Used with BadSampleRatioLimit to detect intermittent failures.
	BadSampleWindow int `toml:"badSampleWindow" check:">=1,<1000" comment:"Window size for bad sample frequency"`

	// BadSampleRatioLimit is the maximum ratio of bad samples allowed in the bad sample
	// window before triggering a reset. Detects intermittent failures even when not consecutive.
	BadSampleRatioLimit float64 `toml:"badSampleRatioLimit" check:">0.0,<=1.0" comment:"Max bad sample ratio in window"`

	// AvgFreqTimeConstant is the time constant in seconds for the exponential moving
	// average of frequency adjustments. This average represents the baseline frequency
	// correction (without phase correction) and is used when samples are missing.
	// Larger values track baseline frequency more smoothly but respond more slowly to
	// oscillator drift. The EMA is updated as: avgFreq = alpha*freq + (1-alpha)*avgFreq
	// where alpha = 1 - exp(-sampleInterval/timeConstant). Set to 0 to disable this
	// feature (no frequency adjustment on missing samples).
	AvgFreqTimeConstant float64 `toml:"avgFreqTimeConstant" check:">=0.0,<1000.0" comment:"EMA time constant for avg frequency (s)"`

	// IgnoreSawtoothCorrection, when true, disables the use of pulse offset corrections
	// from PrePulse messages. This is primarily for testing to verify that sawtooth
	// correction improves synchronization accuracy. Default: false (use corrections).
	IgnoreSawtoothCorrection bool `toml:"ignoreSawtoothCorrection" comment:"Ignore sawtooth corrections"`

	// PulseCorrectionTimeout is maximum time in seconds to wait for a PostPulse correction
	// message after the pulse occurs.
	// The upper limit is set the 1 second minus the tick interval, which is 0.25s.
	PulseCorrectionTimeout float64 `toml:"pulseCorrectionTimeout" check:">0,<0.75" comment:"Max wait for pulse correction msg (s)"`

	// PersistThreshold is the minimum time in seconds that must be spent in tracking mode
	// before the current sample is persisted for drift rate validation in reset mode.
	// This ensures we only trust the reference sample after being synchronized for a while.
	PersistThreshold float64 `toml:"persistThreshold" check:">=0,<86400" comment:"Min time in tracking before sample persists (s)"`
}

TrackingConfig contains tunable parameters for tracking mode.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL