diff --git a/cmd/ptpcheck/cmd/ts2phc.go b/cmd/ptpcheck/cmd/ts2phc.go new file mode 100644 index 00000000..9d3f1e11 --- /dev/null +++ b/cmd/ptpcheck/cmd/ts2phc.go @@ -0,0 +1,119 @@ +/* +Copyright (c) Facebook, Inc. and its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/facebook/time/phc" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + srcDeviceTS2PHCFlag string + dstDeviceTS2PHCFlag string + intervalTS2PHCFlag time.Duration + firstStepTS2PHCFlag time.Duration + srcPinTS2PHCFlag uint +) + +func init() { + RootCmd.AddCommand(ts2phcCmd) + ts2phcCmd.Flags().StringVarP(&srcDeviceTS2PHCFlag, "source", "s", "/dev/ptp_tcard", "Source for Time of Day (ToD) data. Only PHC devices are supported at the moment") + ts2phcCmd.Flags().StringVarP(&dstDeviceTS2PHCFlag, "destination", "d", "eth0", "PHC to be synchronized. The clock may be identified by its character device (like /dev/ptp0) or its associated network interface (like eth0).") + ts2phcCmd.Flags().DurationVarP(&intervalTS2PHCFlag, "interval", "i", time.Second, "Interval between syncs in nanosseconds") + ts2phcCmd.Flags().DurationVarP(&firstStepTS2PHCFlag, "first_step", "f", 20*time.Microsecond, "The maximum offset, specified in seconds, that the servo will correct by changing the clock frequency instead of stepping the clock. This is only applied on the first update. When set to 0.0, the servo will not step the clock on start. The default is 0.00002 (20 microseconds).") + ts2phcCmd.Flags().UintVarP(&srcPinTS2PHCFlag, "pin", "p", phc.DefaultTs2PhcIndex, "output pin number of the PPS signal on source device. Defaults to Pin 3") +} + +func ts2phcRun(srcDevicePath string, dstDeviceName string, interval time.Duration, stepth time.Duration, srcPinIndex uint) error { + ppsSource, err := getPPSSourceFromPath(srcDevicePath, srcPinIndex) + if err != nil { + return fmt.Errorf("error opening source phc device: %w", err) + } + dstDevice, err := phcDeviceFromName(dstDeviceName) + if err != nil { + return fmt.Errorf("error opening target phc device: %w", err) + } + ppsSink, err := phc.PPSSinkFromDevice(dstDevice) + if err != nil { + return fmt.Errorf("error setting target device as PPS sink: %w", err) + } + pi, err := phc.NewPiServo(interval, stepth, dstDevice) + if err != nil { + return fmt.Errorf("error getting servo: %w", err) + } + ticker := time.NewTicker(interval) + + for range ticker.C { + eventTime, err := phc.PollLatestPPSEvent(ppsSink) + if err != nil { + log.Errorf("Error polling PPS Sink: %v", err) + continue + } else if eventTime.IsZero() { + continue + } + log.Debugf("PPS event at %+v", eventTime.UnixNano()) + if err := phc.PPSClockSync(pi, ppsSource, eventTime, dstDevice); err != nil { + log.Errorf("Error syncing PHC: %v", err) + } + } + return nil +} + +func getPPSSourceFromPath(srcDevicePath string, pinIndex uint) (*phc.PPSSource, error) { + srcDeviceFile, err := os.Open(srcDevicePath) + if err != nil { + return nil, fmt.Errorf("error opening source device: %w", err) + } + ppsSource, err := phc.ActivatePPSSource(phc.FromFile(srcDeviceFile), pinIndex) + if err != nil { + return nil, fmt.Errorf("error activating PPS Source for PHC: %w", err) + } + + return ppsSource, nil +} + +// phcDeviceFromName returns a PHC device from a device name, which can be either an interface name or a PHC device path +func phcDeviceFromName(dstDeviceName string) (*phc.Device, error) { + devicePath, err := phc.IfaceToPHCDevice(dstDeviceName) + if err != nil { + log.Infof("Provided device name is not an interface, assuming it is a PHC device path") + devicePath = dstDeviceName + } + // need RW permissions to issue CLOCK_ADJTIME on the device + dstFile, err := os.OpenFile(devicePath, os.O_RDWR, 0) + if err != nil { + return nil, fmt.Errorf("error opening destination device: %w", err) + } + dstDevice := phc.FromFile(dstFile) + return dstDevice, nil +} + +var ts2phcCmd = &cobra.Command{ + Use: "ts2phc", + Short: "Sync PHC with external timestamps", + Run: func(_ *cobra.Command, _ []string) { + ConfigureVerbosity() + if err := ts2phcRun(srcDeviceTS2PHCFlag, dstDeviceTS2PHCFlag, intervalTS2PHCFlag, firstStepTS2PHCFlag, srcPinTS2PHCFlag); err != nil { + log.Fatal(err) + } + }, +} diff --git a/phc/device.go b/phc/device.go index a25d4bd6..ea4fd880 100644 --- a/phc/device.go +++ b/phc/device.go @@ -53,6 +53,9 @@ var iocPinSetfunc2 = ioctl.IOW(ptpClkMagic, 16, unsafe.Sizeof(rawPinDesc{})) // ioctlPTPPeroutRequest2 is an IOCTL req corresponding to PTP_PEROUT_REQUEST2 in linux/ptp_clock.h var ioctlPTPPeroutRequest2 = ioctl.IOW(ptpClkMagic, 12, unsafe.Sizeof(PTPPeroutRequest{})) +// ioctlExtTTSRequest2 is an IOCTL req corresponding to PTP_EXTTS_REQUEST2 in linux/ptp_clock.h +var ioctlExtTTSRequest2 = ioctl.IOW(ptpClkMagic, 11, unsafe.Sizeof(PTPExtTTSRequest{})) + // Ifreq is the request we send with SIOCETHTOOL IOCTL // as per Linux kernel's include/uapi/linux/if.h type Ifreq struct { @@ -136,6 +139,30 @@ type PTPPeroutRequest struct { On PTPClockTime // "On" time of the signal. Must be lower than the period. Valid only if (flags & PTP_PEROUT_DUTY_CYCLE) is set. } +// Bits of the ptp_extts_request.flags field: +const ( + PTPEnableFeature uint32 = 1 << 0 // Enable feature + PTPRisingEdge uint32 = 1 << 1 // Rising edge + PTPFallingEdge uint32 = 1 << 2 // Falling edge + PTPStrictFlags uint32 = 1 << 3 // Strict flags + PTPExtOffset uint32 = 1 << 4 // External offset +) + +// PTPExtTTSRequest as defined in linux/ptp_clock.h +type PTPExtTTSRequest struct { + index uint32 + flags uint32 + rsv [2]uint32 +} + +// PTPExtTTS as defined in linux/ptp_clock.h +type PTPExtTTS struct { + T PTPClockTime /* Time when event occurred. */ + Index uint32 /* Which channel produced the event. Corresponds to the 'index' field of the PTP_EXTTS_REQUEST and PTP_PEROUT_REQUEST ioctls.*/ + Flags uint32 /* Event flags */ + Rsv [2]uint32 /* Reserved for future use. */ +} + // PTPClockTime as defined in linux/ptp_clock.h type PTPClockTime struct { Sec int64 /* seconds */ diff --git a/phc/helper_386.go b/phc/helper_386.go index a2e14f63..fdc7fc76 100644 --- a/phc/helper_386.go +++ b/phc/helper_386.go @@ -24,6 +24,10 @@ import ( "golang.org/x/sys/unix" ) -func timeToTimespec(time time.Time) (ts unix.Timespec) { - return unix.Timespec{Sec: int32(time.Unix()), Nsec: int32(time.Nanosecond())} +func timeToTimespec(t time.Time) unix.Timespec { + return unix.Timespec{Sec: int32(t.Unix()), Nsec: int32(t.Nanosecond())} +} + +func ptpClockTimeToTime(t PTPClockTime) time.Time { + return time.Unix(int64(t.Sec), int64(t.NSec)) } diff --git a/phc/helper_64bit.go b/phc/helper_64bit.go index 77327c12..7ead39b9 100644 --- a/phc/helper_64bit.go +++ b/phc/helper_64bit.go @@ -24,6 +24,10 @@ import ( "golang.org/x/sys/unix" ) -func timeToTimespec(time time.Time) (ts unix.Timespec) { - return unix.Timespec{Sec: time.Unix(), Nsec: int64(time.Nanosecond())} +func timeToTimespec(t time.Time) unix.Timespec { + return unix.Timespec{Sec: t.Unix(), Nsec: int64(t.Nanosecond())} +} + +func ptpClockTimeToTime(t PTPClockTime) time.Time { + return time.Unix(t.Sec, int64(t.NSec)) } diff --git a/phc/phc.go b/phc/phc.go index daea8f85..2d7a9052 100644 --- a/phc/phc.go +++ b/phc/phc.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "strings" + "syscall" "time" "unsafe" @@ -274,6 +275,10 @@ func (dev *Device) setPTPPerout(req PTPPeroutRequest) error { return dev.ioctl(ioctlPTPPeroutRequest2, unsafe.Pointer(&req)) } +func (dev *Device) extTTSRequest(req PTPExtTTSRequest) error { + return dev.ioctl(ioctlExtTTSRequest2, unsafe.Pointer(&req)) +} + // FreqPPB reads PHC device frequency in PPB (parts per billion) func (dev *Device) FreqPPB() (freqPPB float64, err error) { return freqPPBFromDevice(dev) } @@ -282,3 +287,7 @@ func (dev *Device) AdjFreq(freqPPB float64) error { return clockAdjFreq(dev, fre // Step steps the PHC clock by given duration func (dev *Device) Step(step time.Duration) error { return clockStep(dev, step) } + +func (dev *Device) Read(buffer []byte) (int, error) { + return syscall.Read(int(dev.Fd()), buffer) +} diff --git a/phc/pps_source.go b/phc/pps_source.go index 97a78fe1..9770491d 100644 --- a/phc/pps_source.go +++ b/phc/pps_source.go @@ -17,12 +17,17 @@ limitations under the License. package phc import ( + "encoding/binary" + "errors" "fmt" "log" "os" + "syscall" "time" + "unsafe" "github.com/facebook/time/servo" + "golang.org/x/sys/unix" ) // PPSSource represents a PPS source @@ -30,6 +35,7 @@ type PPSSource struct { PHCDevice DeviceController state PPSSourceState peroutPhase int + PPSPinIndex uint } // PPSSourceState represents the state of a PPS source @@ -47,12 +53,14 @@ const ( ptpPeroutDutyCycle = (1 << 1) ptpPeroutPhase = (1 << 2) defaultTs2PhcChannel = 0 - defaultTs2PhcIndex = 0 + DefaultTs2PhcIndex = 3 defaultPulseWidth = uint32(500000000) // should default to 0 if config specified. Otherwise -1 (ignore phase) defaultPeroutPhase = int32(-1) //nolint:all // ppsStartDelay is the delay in seconds before the first PPS signal is sent - ppsStartDelay = 2 + ppsStartDelay = 2 + defaultPollerInterval = 40 * time.Millisecond + PPSPollMaxAttempts = 10 ) // ServoController abstracts away servo @@ -73,18 +81,42 @@ type DeviceController interface { File() *os.File AdjFreq(freq float64) error Step(offset time.Duration) error + Read(buf []byte) (int, error) + Fd() uintptr + extTTSRequest(req PTPExtTTSRequest) error +} + +// PPSPoller represents a device which can be polled for PPS events +type PPSPoller interface { + pollPPSSink() (time.Time, error) +} + +// FrequencyGetter is an interface for getting PHC frequency and max frequency adjustment +type FrequencyGetter interface { + MaxFreqAdjPPB() (float64, error) + FreqPPB() (float64, error) +} + +// PPSSink represents a device which is a sink of PPS signals +type PPSSink struct { + PinDesc rawPinDesc + Polarity uint32 + PulseWidth uint32 + Device DeviceController + CollectedEvents []*time.Time + pollDescriptor unix.PollFd } // ActivatePPSSource configures the PHC device to be a PPS timestamp source -func ActivatePPSSource(dev DeviceController) (*PPSSource, error) { +func ActivatePPSSource(dev DeviceController, pinIndex uint) (*PPSSource, error) { // Initialize the PTPPeroutRequest struct peroutRequest := PTPPeroutRequest{} - err := dev.setPinFunc(defaultTs2PhcIndex, PinFuncPerOut, defaultTs2PhcChannel) + err := dev.setPinFunc(pinIndex, PinFuncPerOut, defaultTs2PhcChannel) if err != nil { log.Printf("Failed to set PPS Perout on pin index %d, channel %d, PHC %s. Error: %s. Continuing bravely on...", - defaultTs2PhcIndex, defaultTs2PhcChannel, dev.File().Name(), err) + pinIndex, defaultTs2PhcChannel, dev.File().Name(), err) } ts, err := dev.Time() @@ -93,13 +125,14 @@ func ActivatePPSSource(dev DeviceController) (*PPSSource, error) { } // Set the index and period - peroutRequest.Index = defaultTs2PhcIndex + // nolint:gosec + peroutRequest.Index = uint32(defaultTs2PhcChannel) peroutRequest.Period = PTPClockTime{Sec: 1, NSec: 0} // Set flags and pulse width pulsewidth := defaultPulseWidth - // TODO: skip this block if pulsewidth > 0 once pulsewidth is configurable + // TODO: skip this block if pulsewidth unset once pulsewidth is configurable peroutRequest.Flags |= ptpPeroutDutyCycle peroutRequest.On = PTPClockTime{Sec: int64(pulsewidth / nsPerSec), NSec: pulsewidth % nsPerSec} @@ -118,7 +151,7 @@ func ActivatePPSSource(dev DeviceController) (*PPSSource, error) { } } - return &PPSSource{PHCDevice: dev, state: PPSSet}, nil + return &PPSSource{PHCDevice: dev, state: PPSSet, PPSPinIndex: pinIndex}, nil } // Timestamp returns the timestamp of the last PPS output edge from the given PPS source @@ -129,7 +162,6 @@ func (ppsSource *PPSSource) Timestamp() (*time.Time, error) { } currTime, err := ppsSource.PHCDevice.Time() - if err != nil { return nil, fmt.Errorf("error getting time (clock_gettime) on %s", ppsSource.PHCDevice.File().Name()) } @@ -139,20 +171,7 @@ func (ppsSource *PPSSource) Timestamp() (*time.Time, error) { currTime = currTime.Add(-time.Duration(ppsSource.peroutPhase)) sourceTs := timeToTimespec(currTime) - /* - * As long as the kernel doesn't support a proper API for reporting - * back a precise perout timestamp, we have to assume that the current - * time on the PPS source is still within +/- half a second of the last - * perout output edge, and hence, we can deduce the current second - * (nanossecond is omitted) of this edge at the emitter based on the - * emitter's current time. We support only PHC sources, so we can ignore - * the NMEA source edge case described in ts2phc.c - */ - //nolint:unconvert - if int64(sourceTs.Nsec) > int64(nsPerSec/2) { - sourceTs.Sec++ - sourceTs.Nsec = 0 - } + sourceTs.Nsec = 0 //nolint:unconvert currTime = time.Unix(int64(sourceTs.Sec), int64(sourceTs.Nsec)) currTime = currTime.Add(time.Duration(ppsSource.peroutPhase)) @@ -160,22 +179,44 @@ func (ppsSource *PPSSource) Timestamp() (*time.Time, error) { return &currTime, nil } -// PPSClockSync adjusts the frequency of the destination device based on the PPS from the ppsSource -func PPSClockSync(pi ServoController, ppsSource Timestamper, dstDevice DeviceController) error { - srcTimestamp, err := ppsSource.Timestamp() +// NewPiServo returns a servo.PiServo object configure for synchronizing the given device +func NewPiServo(interval time.Duration, stepth time.Duration, device FrequencyGetter) (*servo.PiServo, error) { + servoCfg := servo.DefaultServoConfig() + if stepth != 0 { + // allow stepping clock on first update + servoCfg.FirstUpdate = true + servoCfg.FirstStepThreshold = int64(stepth) + } + + freq, err := device.FreqPPB() if err != nil { - return fmt.Errorf("error getting timestamp from PPS source: %w", err) + return nil, err } - dstTimestamp, err := dstDevice.Time() + pi := servo.NewPiServo(servoCfg, servo.DefaultPiServoCfg(), -freq) + pi.SyncInterval(interval.Seconds()) + + maxFreq, err := device.MaxFreqAdjPPB() if err != nil { - return fmt.Errorf("error getting time from destination device: %w", err) + maxFreq = float64(DefaultMaxClockFreqPPB) } - phcOffset := dstTimestamp.Sub(*srcTimestamp) + pi.SetMaxFreq(maxFreq) + + return pi, nil +} + +// PPSClockSync adjusts the frequency of the destination device based on the PPS from the ppsSource +func PPSClockSync(pi ServoController, ppsSource Timestamper, dstEventTimestamp time.Time, dstDevice DeviceController) error { + srcTimestamp, err := ppsSource.Timestamp() // seconds set, nanosseconds set to 0 + if err != nil { + return fmt.Errorf("error getting timestamp from PPS source: %w", err) + } + + phcOffset := dstEventTimestamp.Sub(*srcTimestamp) //nolint:gosec - freqAdj, servoState := pi.Sample(int64(phcOffset), uint64(dstTimestamp.UnixNano())) // unix nano is never negative + freqAdj, servoState := pi.Sample(int64(phcOffset), uint64(dstEventTimestamp.UnixNano())) // unix nano is never negative - log.Printf("%s offset %10d s%d freq %+7.0f", dstDevice.File().Name(), int64(phcOffset), servoState, freqAdj) + log.Printf("%s offset %10d servo %s freq %+7.0f", dstDevice.File().Name(), int64(phcOffset), servoState.String(), freqAdj) switch servoState { case servo.StateJump: @@ -186,8 +227,113 @@ func PPSClockSync(pi ServoController, ppsSource Timestamper, dstDevice DeviceCon if err := dstDevice.AdjFreq(-freqAdj); err != nil { return fmt.Errorf("failed to adjust freq to %v: %w", -freqAdj, err) } + case servo.StateInit: + return nil default: return fmt.Errorf("skipping clock update: servo state is %v", servoState) } return nil } + +// PPSSinkFromDevice configures the targetDevice to be a PPS sink and report PPS timestamp events +func PPSSinkFromDevice(targetDevice DeviceController) (*PPSSink, error) { + pfd := unix.PollFd{ + Events: unix.POLLIN | unix.POLLPRI, + Fd: int32(targetDevice.Fd()), + } + ppsSink := PPSSink{ + PinDesc: rawPinDesc{Func: uint32(PinFuncExtTS)}, + Polarity: PTPRisingEdge, + PulseWidth: defaultPulseWidth, + Device: targetDevice, + CollectedEvents: []*time.Time{}, + pollDescriptor: pfd, + } + + req := PTPExtTTSRequest{ + flags: PTPEnableFeature | ppsSink.Polarity, + index: ppsSink.PinDesc.Index, + } + + err := targetDevice.extTTSRequest(req) + if err != nil { + return nil, fmt.Errorf("error configuring sink for device %s: %w", targetDevice.File().Name(), err) + } + + return &ppsSink, nil +} + +// getPPSEventTimestamp reads the first PPS event from the sink file descriptor and returns the timestamp of the event +func (ppsSink *PPSSink) getPPSEventTimestamp() (time.Time, error) { + var event PTPExtTTS + buf := make([]byte, binary.Size(event)) + _, err := ppsSink.Device.Read(buf) + if err != nil { + return time.Time{}, fmt.Errorf("error reading from sink %v: %w", ppsSink.Device.File().Name(), err) + } + event = *(*PTPExtTTS)(unsafe.Pointer(&buf[0])) + if event.Index != ppsSink.PinDesc.Index { + return time.Time{}, fmt.Errorf("extts on unexpected pin index %d, expected %d", event.Index, ppsSink.PinDesc.Index) + } + + eventTime := ptpClockTimeToTime(event.T) + + return eventTime, nil +} + +// pollFd polls the file descriptor for an event, and returns the number of events, the file descriptor, and error +func pollFd(pfd unix.PollFd) (int, unix.PollFd, error) { + fdSlice := []unix.PollFd{pfd} + for { + eventCount, err := unix.Poll(fdSlice, int(defaultPollerInterval.Milliseconds())) + // swallow EINTR + if !errors.Is(err, syscall.EINTR) { + return eventCount, fdSlice[0], err + } + } +} + +// pollPPSSink polls the sink for the timestamp of the first available PPS event +func (ppsSink *PPSSink) pollPPSSink() (time.Time, error) { + eventCount, newPollDescriptor, err := pollFd(ppsSink.pollDescriptor) + ppsSink.pollDescriptor = newPollDescriptor + ppsSink.CollectedEvents = []*time.Time{} + + if err != nil || eventCount < 0 { + return time.Time{}, fmt.Errorf("error polling sink %v: %w", ppsSink.Device.File().Name(), err) + } + + if eventCount == 0 { + return time.Time{}, nil + } + + if ppsSink.pollDescriptor.Revents&unix.POLLERR != 0 { + return time.Time{}, fmt.Errorf("error polling sink %v: POLLERR", ppsSink.Device.File().Name()) + } + + result, err := ppsSink.getPPSEventTimestamp() + if err != nil { + return time.Time{}, fmt.Errorf("error while fetching pps event on sink %v: %w", ppsSink.Device.File().Name(), err) + } + return result, nil +} + +// PollLatestPPSEvent returns the timestamp of the latest available PPS event +func PollLatestPPSEvent(ppsSink PPSPoller) (lastEvent time.Time, err error) { + for attempts := 0; attempts < PPSPollMaxAttempts; attempts++ { + newEvent, err := ppsSink.pollPPSSink() + if err != nil { + if !lastEvent.IsZero() { + return lastEvent, nil + } + return time.Time{}, fmt.Errorf("error polling for event: %w", err) + } + if !newEvent.IsZero() { + lastEvent = newEvent + continue + } + return lastEvent, nil + } + + return lastEvent, nil +} diff --git a/phc/pps_source_mocks.go b/phc/pps_source_mocks.go index 8d66dfaf..f8264d0d 100644 --- a/phc/pps_source_mocks.go +++ b/phc/pps_source_mocks.go @@ -142,6 +142,20 @@ func (mr *MockDeviceControllerMockRecorder) AdjFreq(freq interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdjFreq", reflect.TypeOf((*MockDeviceController)(nil).AdjFreq), freq) } +// Fd mocks base method. +func (m *MockDeviceController) Fd() uintptr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fd") + ret0, _ := ret[0].(uintptr) + return ret0 +} + +// Fd indicates an expected call of Fd. +func (mr *MockDeviceControllerMockRecorder) Fd() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fd", reflect.TypeOf((*MockDeviceController)(nil).Fd)) +} + // File mocks base method. func (m *MockDeviceController) File() *os.File { m.ctrl.T.Helper() @@ -156,6 +170,21 @@ func (mr *MockDeviceControllerMockRecorder) File() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "File", reflect.TypeOf((*MockDeviceController)(nil).File)) } +// Read mocks base method. +func (m *MockDeviceController) Read(buf []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", buf) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockDeviceControllerMockRecorder) Read(buf interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockDeviceController)(nil).Read), buf) +} + // Step mocks base method. func (m *MockDeviceController) Step(offset time.Duration) error { m.ctrl.T.Helper() @@ -185,6 +214,20 @@ func (mr *MockDeviceControllerMockRecorder) Time() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Time", reflect.TypeOf((*MockDeviceController)(nil).Time)) } +// extTTSRequest mocks base method. +func (m *MockDeviceController) extTTSRequest(req PTPExtTTSRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "extTTSRequest", req) + ret0, _ := ret[0].(error) + return ret0 +} + +// extTTSRequest indicates an expected call of extTTSRequest. +func (mr *MockDeviceControllerMockRecorder) extTTSRequest(req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "extTTSRequest", reflect.TypeOf((*MockDeviceController)(nil).extTTSRequest), req) +} + // setPTPPerout mocks base method. func (m *MockDeviceController) setPTPPerout(req PTPPeroutRequest) error { m.ctrl.T.Helper() @@ -212,3 +255,94 @@ func (mr *MockDeviceControllerMockRecorder) setPinFunc(index, pf, ch interface{} mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setPinFunc", reflect.TypeOf((*MockDeviceController)(nil).setPinFunc), index, pf, ch) } + +// MockPPSPoller is a mock of PPSPoller interface. +type MockPPSPoller struct { + ctrl *gomock.Controller + recorder *MockPPSPollerMockRecorder +} + +// MockPPSPollerMockRecorder is the mock recorder for MockPPSPoller. +type MockPPSPollerMockRecorder struct { + mock *MockPPSPoller +} + +// NewMockPPSPoller creates a new mock instance. +func NewMockPPSPoller(ctrl *gomock.Controller) *MockPPSPoller { + mock := &MockPPSPoller{ctrl: ctrl} + mock.recorder = &MockPPSPollerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPPSPoller) EXPECT() *MockPPSPollerMockRecorder { + return m.recorder +} + +// pollPPSSink mocks base method. +func (m *MockPPSPoller) pollPPSSink() (time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "pollPPSSink") + ret0, _ := ret[0].(time.Time) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// pollPPSSink indicates an expected call of pollPPSSink. +func (mr *MockPPSPollerMockRecorder) pollPPSSink() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "pollPPSSink", reflect.TypeOf((*MockPPSPoller)(nil).pollPPSSink)) +} + +// MockFrequencyGetter is a mock of FrequencyGetter interface. +type MockFrequencyGetter struct { + ctrl *gomock.Controller + recorder *MockFrequencyGetterMockRecorder +} + +// MockFrequencyGetterMockRecorder is the mock recorder for MockFrequencyGetter. +type MockFrequencyGetterMockRecorder struct { + mock *MockFrequencyGetter +} + +// NewMockFrequencyGetter creates a new mock instance. +func NewMockFrequencyGetter(ctrl *gomock.Controller) *MockFrequencyGetter { + mock := &MockFrequencyGetter{ctrl: ctrl} + mock.recorder = &MockFrequencyGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFrequencyGetter) EXPECT() *MockFrequencyGetterMockRecorder { + return m.recorder +} + +// FreqPPB mocks base method. +func (m *MockFrequencyGetter) FreqPPB() (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FreqPPB") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FreqPPB indicates an expected call of FreqPPB. +func (mr *MockFrequencyGetterMockRecorder) FreqPPB() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FreqPPB", reflect.TypeOf((*MockFrequencyGetter)(nil).FreqPPB)) +} + +// MaxFreqAdjPPB mocks base method. +func (m *MockFrequencyGetter) MaxFreqAdjPPB() (float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxFreqAdjPPB") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MaxFreqAdjPPB indicates an expected call of MaxFreqAdjPPB. +func (mr *MockFrequencyGetterMockRecorder) MaxFreqAdjPPB() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxFreqAdjPPB", reflect.TypeOf((*MockFrequencyGetter)(nil).MaxFreqAdjPPB)) +} diff --git a/phc/pps_source_test.go b/phc/pps_source_test.go index 18295662..63106151 100644 --- a/phc/pps_source_test.go +++ b/phc/pps_source_test.go @@ -17,6 +17,8 @@ limitations under the License. package phc import ( + "bytes" + "encoding/binary" "fmt" "os" "testing" @@ -48,6 +50,12 @@ func SetupMocks(t *testing.T) (servoMock *MockServoController, srcMock *MockTime } } +func SetupMockPoller(t *testing.T) (*MockPPSPoller, Finisher) { + ctrl := gomock.NewController(t) + mockPPSPoller := NewMockPPSPoller(ctrl) + return mockPPSPoller, ctrl.Finish +} + func TestActivatePPSSource(t *testing.T) { // Prepare _, _, mockDeviceController, finish := SetupMocks(t) @@ -55,7 +63,7 @@ func TestActivatePPSSource(t *testing.T) { var actualPeroutRequest PTPPeroutRequest gomock.InOrder( // Should set default pin to PPS - mockDeviceController.EXPECT().setPinFunc(uint(0), PinFuncPerOut, uint(0)).Return(nil), + mockDeviceController.EXPECT().setPinFunc(uint(4), PinFuncPerOut, uint(0)).Return(nil), // Should call Time once mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil), mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(nil).Do(func(arg PTPPeroutRequest) { actualPeroutRequest = arg }), @@ -71,7 +79,7 @@ func TestActivatePPSSource(t *testing.T) { } // Act - ppsSource, err := ActivatePPSSource(mockDeviceController) + ppsSource, err := ActivatePPSSource(mockDeviceController, 4) // Assert require.NoError(t, err) @@ -92,7 +100,7 @@ func TestActivatePPSSourceIgnoreSetPinFailure(t *testing.T) { ) // Act - ppsSource, err := ActivatePPSSource(mockDeviceController) + ppsSource, err := ActivatePPSSource(mockDeviceController, 0) // Assert require.NoError(t, err) @@ -122,7 +130,7 @@ func TestActivatePPSSourceSetPTPPeroutFailure(t *testing.T) { } // Act - ppsSource, err := ActivatePPSSource(mockDeviceController) + ppsSource, err := ActivatePPSSource(mockDeviceController, 0) // Assert require.NoError(t, err) @@ -143,7 +151,7 @@ func TestActivatePPSSourceSetPTPPeroutDoubleFailure(t *testing.T) { ) // Act - ppsSource, err := ActivatePPSSource(mockDeviceController) + ppsSource, err := ActivatePPSSource(mockDeviceController, 0) // Assert require.Error(t, err) @@ -162,7 +170,7 @@ func TestGetPPSTimestampSourceUnset(t *testing.T) { require.Error(t, err) } -func TestGetPPSTimestampMoreThanHalfNanossecondShouldAddSecond(t *testing.T) { +func TestGetPPSTimestampMoreThanHalfSecondShouldRemoveNanosseconds(t *testing.T) { _, _, mockDeviceController, finish := SetupMocks(t) defer finish() ppsSource := PPSSource{PHCDevice: mockDeviceController, state: PPSSet, peroutPhase: 23312} @@ -172,12 +180,12 @@ func TestGetPPSTimestampMoreThanHalfNanossecondShouldAddSecond(t *testing.T) { timestamp, err := ppsSource.Timestamp() // Assert - expected := time.Unix(1075896001, 23312) + expected := time.Unix(1075896000, 23312) require.NoError(t, err) require.EqualValues(t, expected, *timestamp) } -func TestGetPPSTimestampLessThanHalfNanossecondShouldKeepNanosseconds(t *testing.T) { +func TestGetPPSTimestampLessThanHalfSecondShouldRemoveNanosseconds(t *testing.T) { // Prepare _, _, mockDeviceController, finish := SetupMocks(t) defer finish() @@ -188,7 +196,23 @@ func TestGetPPSTimestampLessThanHalfNanossecondShouldKeepNanosseconds(t *testing timestamp, err := ppsSource.Timestamp() // Assert - expected := time.Unix(1075896000, 500023312) + expected := time.Unix(1075896000, 23312) + require.NoError(t, err) + require.EqualValues(t, expected, *timestamp) +} + +func TestGetPPSTimestampUnphased(t *testing.T) { + // Prepare + _, _, mockDeviceController, finish := SetupMocks(t) + defer finish() + ppsSource := PPSSource{PHCDevice: mockDeviceController, state: PPSSet} + mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil) + + // Act + timestamp, err := ppsSource.Timestamp() + + // Assert + expected := time.Unix(1075896000, 0) require.NoError(t, err) require.EqualValues(t, expected, *timestamp) } @@ -204,18 +228,17 @@ func TestPPSClockSyncServoLockedSuccess(t *testing.T) { servoMock, mockTimestamper, mockDeviceController, finish := SetupMocks(t) defer finish() - ppsTimestamp := time.Unix(1075896000, 100) + ppsSourceTimestamp := time.Unix(1075896000, 100) gomock.InOrder( - mockTimestamper.EXPECT().Timestamp().Return(&ppsTimestamp, nil), - mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 0), nil), + mockTimestamper.EXPECT().Timestamp().Return(&ppsSourceTimestamp, nil), servoMock.EXPECT().Sample(gomock.Any(), gomock.Any()).Return(0.1, servo.StateLocked), mockDeviceController.EXPECT().File().Return(os.NewFile(0, "test")), mockDeviceController.EXPECT().AdjFreq(-0.1).Return(nil), ) // Act - err := PPSClockSync(servoMock, mockTimestamper, mockDeviceController) + err := PPSClockSync(servoMock, mockTimestamper, time.Unix(1075896000, 23312), mockDeviceController) // Assert require.NoError(t, err) @@ -226,17 +249,16 @@ func TestPPSClockSyncServoLockedFailure(t *testing.T) { servoMock, mockTimestamper, mockDeviceController, finish := SetupMocks(t) defer finish() - ppsTimestamp := time.Unix(1075896000, 100) + ppsSourceTimestamp := time.Unix(1075896000, 100) gomock.InOrder( - mockTimestamper.EXPECT().Timestamp().Return(&ppsTimestamp, nil), - mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 0), nil), + mockTimestamper.EXPECT().Timestamp().Return(&ppsSourceTimestamp, nil), servoMock.EXPECT().Sample(gomock.Any(), gomock.Any()).Return(0.1, servo.StateLocked), mockDeviceController.EXPECT().File().Return(os.NewFile(0, "test")), mockDeviceController.EXPECT().AdjFreq(-0.1).Return(fmt.Errorf("error")), ) // Act - err := PPSClockSync(servoMock, mockTimestamper, mockDeviceController) + err := PPSClockSync(servoMock, mockTimestamper, time.Unix(1075896000, 23312), mockDeviceController) // Assert require.Error(t, err) @@ -246,18 +268,17 @@ func TestPPSClockSyncServoJumpSuccess(t *testing.T) { // Prepare servoMock, mockTimestamper, mockDeviceController, finish := SetupMocks(t) defer finish() - ppsTimestamp := time.Unix(1075896000, 100) + ppsSourceTimestamp := time.Unix(1075896000, 100) gomock.InOrder( - mockTimestamper.EXPECT().Timestamp().Return(&ppsTimestamp, nil), - mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 0), nil), + mockTimestamper.EXPECT().Timestamp().Return(&ppsSourceTimestamp, nil), servoMock.EXPECT().Sample(gomock.Any(), gomock.Any()).Return(0.1, servo.StateJump), mockDeviceController.EXPECT().File().Return(os.NewFile(0, "test")), // TODO: Improve comparison as, due to issues with typing, gomock comparison is not precise - mockDeviceController.EXPECT().Step(time.Duration(100)).Return(nil), + mockDeviceController.EXPECT().Step(time.Duration(1999999976788)).Return(nil), ) // Act - err := PPSClockSync(servoMock, mockTimestamper, mockDeviceController) + err := PPSClockSync(servoMock, mockTimestamper, time.Unix(1075894000, 23312), mockDeviceController) // Assert require.NoError(t, err) @@ -267,10 +288,9 @@ func TestPPSClockSyncServoJumpFailure(t *testing.T) { // Prepare servoMock, mockTimestamper, mockDeviceController, finish := SetupMocks(t) defer finish() - ppsTimestamp := time.Unix(1075896000, 100) + ppsSourceTimestamp := time.Unix(1075896000, 100) gomock.InOrder( - mockTimestamper.EXPECT().Timestamp().Return(&ppsTimestamp, nil), - mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 0), nil), + mockTimestamper.EXPECT().Timestamp().Return(&ppsSourceTimestamp, nil), servoMock.EXPECT().Sample(gomock.Any(), gomock.Any()).Return(0.1, servo.StateJump), mockDeviceController.EXPECT().File().Return(os.NewFile(0, "test")), // TODO: Improve comparison as, due to issues with typing, gomock comparison is not precise @@ -278,40 +298,260 @@ func TestPPSClockSyncServoJumpFailure(t *testing.T) { ) // Act - err := PPSClockSync(servoMock, mockTimestamper, mockDeviceController) + err := PPSClockSync(servoMock, mockTimestamper, time.Unix(1075896000, 23312), mockDeviceController) // Assert require.Error(t, err) } -func TestPPSClockSyncSrcFailure(t *testing.T) { +func TestPPSClockSyncServoInit(t *testing.T) { // Prepare servoMock, mockTimestamper, mockDeviceController, finish := SetupMocks(t) defer finish() + ppsSourceTimestamp := time.Unix(1075896000, 100) gomock.InOrder( - mockTimestamper.EXPECT().Timestamp().Return(nil, fmt.Errorf("error")), + mockTimestamper.EXPECT().Timestamp().Return(&ppsSourceTimestamp, nil), + servoMock.EXPECT().Sample(gomock.Any(), gomock.Any()).Return(0.1, servo.StateInit), + mockDeviceController.EXPECT().File().Return(os.NewFile(0, "test")), ) // Act - err := PPSClockSync(servoMock, mockTimestamper, mockDeviceController) + err := PPSClockSync(servoMock, mockTimestamper, time.Unix(1075896000, 23312), mockDeviceController) // Assert - require.Error(t, err) + require.NoError(t, err) } -func TestPPSClockSyncDstFailure(t *testing.T) { +func TestPPSClockSyncSrcFailure(t *testing.T) { // Prepare servoMock, mockTimestamper, mockDeviceController, finish := SetupMocks(t) defer finish() - ppsTimestamp := time.Unix(1075896000, 100) gomock.InOrder( - mockTimestamper.EXPECT().Timestamp().Return(&ppsTimestamp, nil), - mockDeviceController.EXPECT().Time().Return(time.Now(), fmt.Errorf("error")), + mockTimestamper.EXPECT().Timestamp().Return(nil, fmt.Errorf("error")), ) // Act - err := PPSClockSync(servoMock, mockTimestamper, mockDeviceController) + err := PPSClockSync(servoMock, mockTimestamper, time.Unix(1075896000, 23312), mockDeviceController) // Assert require.Error(t, err) } + +func TestNewPiServo(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockFrequencyGetter := NewMockFrequencyGetter(ctrl) + gomock.InOrder( + mockFrequencyGetter.EXPECT().FreqPPB().Return(1.0, nil), + mockFrequencyGetter.EXPECT().MaxFreqAdjPPB().Return(2.0, nil), + ) + + servo, err := NewPiServo(time.Duration(1), time.Duration(1), mockFrequencyGetter) + + require.NoError(t, err) + require.Equal(t, int64(1), servo.Servo.FirstStepThreshold) + require.Equal(t, true, servo.Servo.FirstUpdate) + require.Equal(t, -1.0, servo.MeanFreq()) + require.Equal(t, "INIT", servo.GetState().String()) + require.Equal(t, 2.0, servo.GetMaxFreq()) +} + +func TestNewPiServoFreqPPBError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockFrequencyGetter := NewMockFrequencyGetter(ctrl) + gomock.InOrder( + mockFrequencyGetter.EXPECT().FreqPPB().Return(1.0, fmt.Errorf("error")), + ) + + _, err := NewPiServo(time.Duration(1), time.Duration(1), mockFrequencyGetter) + + require.Error(t, err) +} + +func TestNewPiServoDefaultMaxFreq(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockFrequencyGetter := NewMockFrequencyGetter(ctrl) + gomock.InOrder( + mockFrequencyGetter.EXPECT().FreqPPB().Return(1.0, nil), + mockFrequencyGetter.EXPECT().MaxFreqAdjPPB().Return(2.0, fmt.Errorf("error")), + ) + + servo, err := NewPiServo(time.Duration(1), time.Duration(1), mockFrequencyGetter) + + require.NoError(t, err) + require.Equal(t, int64(1), servo.Servo.FirstStepThreshold) + require.Equal(t, true, servo.Servo.FirstUpdate) + require.Equal(t, -1.0, servo.MeanFreq()) + require.Equal(t, "INIT", servo.GetState().String()) + require.Equal(t, 500000.0, servo.GetMaxFreq()) +} + +func TestNewPiServoNoFirstStep(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockFrequencyGetter := NewMockFrequencyGetter(ctrl) + gomock.InOrder( + mockFrequencyGetter.EXPECT().FreqPPB().Return(1.0, nil), + mockFrequencyGetter.EXPECT().MaxFreqAdjPPB().Return(2.0, fmt.Errorf("error")), + ) + + servo, err := NewPiServo(time.Duration(1), time.Duration(0), mockFrequencyGetter) + + require.NoError(t, err) + require.Equal(t, false, servo.Servo.FirstUpdate) + require.Equal(t, -1.0, servo.MeanFreq()) + require.Equal(t, "INIT", servo.GetState().String()) + require.Equal(t, 500000.0, servo.GetMaxFreq()) +} + +func TestPollLatestPPSEvent_SuccessfulPollWithEvent(t *testing.T) { + mockPPSPoller, finish := SetupMockPoller(t) + defer finish() + + // Prepare + polledEventTime := time.Unix(1075896000, 500000000) + mockPPSPoller.EXPECT().pollPPSSink().Return(polledEventTime, nil) + mockPPSPoller.EXPECT().pollPPSSink().Return(time.Time{}, fmt.Errorf("any error signals no more events")) + + // Act + resultEventTime, err := PollLatestPPSEvent(mockPPSPoller) + + // Assert + require.Equal(t, polledEventTime, resultEventTime) + require.NoError(t, err) +} + +func TestPollLatestPPSEvent_SuccessfulPollWithoutEvent(t *testing.T) { + mockPPSPoller, finish := SetupMockPoller(t) + defer finish() + + // Prepare + mockPPSPoller.EXPECT().pollPPSSink().Return(time.Time{}, nil) + + // Act + event, err := PollLatestPPSEvent(mockPPSPoller) + + // Assert + require.Zero(t, event) + require.NoError(t, err) +} + +func TestPollLatestPPSEvent_ErrorPolling(t *testing.T) { + mockPPSPoller, finish := SetupMockPoller(t) + defer finish() + + // Prepare + mockPPSPoller.EXPECT().pollPPSSink().Return(time.Time{}, fmt.Errorf("poll error")) + + // Act + event, err := PollLatestPPSEvent(mockPPSPoller) + + // Assert + require.Zero(t, event) + require.Error(t, err) +} + +func TestPollLatestPPSEvent_MultiplePollsWithEvents(t *testing.T) { + mockPPSPoller, finish := SetupMockPoller(t) + defer finish() + + // Prepare + lastPolledEventTime := time.Unix(1075896000, 500000000) + mockPPSPoller.EXPECT().pollPPSSink().Return(lastPolledEventTime.Add(-1*time.Second), nil) + mockPPSPoller.EXPECT().pollPPSSink().Return(lastPolledEventTime, nil) + mockPPSPoller.EXPECT().pollPPSSink().Return(time.Time{}, nil) + + // Act + resultEventTime, err := PollLatestPPSEvent(mockPPSPoller) + + // Assert + require.Equal(t, lastPolledEventTime, resultEventTime) + require.NoError(t, err) +} + +func TestPollLatestPPSEvent_MultiplePollsWithError(t *testing.T) { + mockPPSPoller, finish := SetupMockPoller(t) + defer finish() + + // Prepare + lastPolledEventTime := time.Unix(1075896000, 500000000) + mockPPSPoller.EXPECT().pollPPSSink().Return(lastPolledEventTime, nil) + mockPPSPoller.EXPECT().pollPPSSink().Return(time.Time{}, fmt.Errorf("poll error")) + + // Act + resultEventTime, err := PollLatestPPSEvent(mockPPSPoller) + + // Assert + require.Equal(t, lastPolledEventTime, resultEventTime) + require.NoError(t, err) +} + +func TestPPSSink_getPPSEventTimestamp(t *testing.T) { + // Create a mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock DeviceController + mockDevice := NewMockDeviceController(ctrl) + + // Create a PPSSink instance + ppsSink := &PPSSink{ + Device: mockDevice, + PinDesc: rawPinDesc{Index: 1}, + } + + // Test cases + t.Run("successful read", func(t *testing.T) { + // Prepare + event := PTPExtTTS{Index: 1, T: PTPClockTime{Sec: 1}} + + mockDevice.EXPECT().Read(gomock.Any()).Return(1, nil).Do(func(buf []byte) { + var intBuffer bytes.Buffer + err := binary.Write(&intBuffer, binary.LittleEndian, &event) + require.NoError(t, err) + copy(buf, intBuffer.Bytes()) + fmt.Print(buf) + }) + + // Act + timestamp, err := ppsSink.getPPSEventTimestamp() + + // Assert + require.NoError(t, err) + require.Equal(t, timestamp, time.Unix(1, 0)) + }) + + t.Run("read error", func(t *testing.T) { + // Prepare + mockDevice.EXPECT().Read(gomock.Any()).Return(0, fmt.Errorf("read error")) + mockDevice.EXPECT().File().Return(os.NewFile(0, "test")) + + // Act + timestamp, err := ppsSink.getPPSEventTimestamp() + + // Assert + require.Error(t, err) + require.Zero(t, timestamp) + }) + + t.Run("unexpected channel", func(t *testing.T) { + // Prepare + event := PTPExtTTS{Index: 2, T: PTPClockTime{Sec: 1}} + + mockDevice.EXPECT().Read(gomock.Any()).Return(1, nil).Do(func(buf []byte) { + var intBuffer bytes.Buffer + err := binary.Write(&intBuffer, binary.LittleEndian, &event) + require.NoError(t, err) + copy(buf, intBuffer.Bytes()) + }) + + // Act + timestamp, err := ppsSink.getPPSEventTimestamp() + + // Assert + require.Error(t, err) + require.Zero(t, timestamp) + }) +} diff --git a/servo/pi.go b/servo/pi.go index e1f98d0d..791c5eb8 100644 --- a/servo/pi.go +++ b/servo/pi.go @@ -128,6 +128,11 @@ func (s *PiServo) SetMaxFreq(freq float64) { s.maxFreq = freq } +// GetMaxFreq gets current configured max frequency +func (s *PiServo) GetMaxFreq() float64 { + return s.maxFreq +} + // IsSpike function to check if offset is considered as spike func (s *PiServo) IsSpike(offset int64) bool { if s.filter == nil || s.count < 2 {