Documentation
¶
Index ¶
- type Clock
- type Config
- type Controller
- func (c *Controller) Close()
- func (c *Controller) LeapSecond(ls ptime.LeapSecond)
- func (c *Controller) Mode() Mode
- func (c *Controller) Pause()
- func (c *Controller) PulseEdge(edge PulseEdge)
- func (c *Controller) SetTimeMsgBuffer(buf TimeMsgBuffer)
- func (c *Controller) Tick(now time.Time)
- func (c *Controller) TimeMessage()
- type ConvergingConfig
- type Mode
- type MultiSampler
- type PulseEdge
- type PulseType
- type ResetConfig
- type Sample
- type SampleKind
- type Sampler
- type TimeMsgBuffer
- type TrackingConfig
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.
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) 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.
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
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
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.