Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor PPS code #397

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion phc/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
const (
ptpMaxSamples = 25
ptpClkMagic = '='
nsPerSec = int32(1000000000)
nsPerSec = uint32(1000000000)
)

// ioctlPTPSysOffsetExtended is an IOCTL to get extended offset
Expand Down
117 changes: 0 additions & 117 deletions phc/phc.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,12 @@ import (
"time"
"unsafe"

log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)

// DefaultMaxClockFreqPPB value came from linuxptp project (clockadj.c)
const DefaultMaxClockFreqPPB = 500000.0

// PPS related constants
const (
ptpPeroutDutyCycle = (1 << 1)
ptpPeroutPhase = (1 << 2)
defaultTs2PhcChannel = 0
defaultTs2PhcIndex = 0
defaultPulseWidth = int32(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
)

// Time returns PTPClockTime as time.Time
func (t PTPClockTime) Time() time.Time {
return time.Unix(t.Sec, int64(t.NSec))
Expand Down Expand Up @@ -152,23 +138,6 @@ func Time(iface string, method TimeMethod) (time.Time, error) {
}
}

// PPSSource represents a PPS source
type PPSSource struct {
PHCDevice DeviceController
state PPSSourceState
peroutPhase int
}

// PPSSourceState represents the state of a PPS source
type PPSSourceState int

const (
// UnknownStatus is the initial state of a PPS source, which means PPS may or may not be configured
UnknownStatus PPSSourceState = iota
// PPSSet means the underlying device is activated as a PPS source
PPSSet
)

// Device represents a PHC device
type Device os.File

Expand Down Expand Up @@ -321,89 +290,3 @@ 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) }

// ActivatePPSSource configures the PHC device to be a PPS timestamp source
func ActivatePPSSource(dev DeviceController) (*PPSSource, error) {
// Initialize the PTPPeroutRequest struct
peroutRequest := PTPPeroutRequest{}

err := dev.setPinFunc(defaultTs2PhcIndex, PinFuncPerOut, defaultTs2PhcChannel)

if err != nil {
log.Errorf("Failed to set PPS Perout on pin index %d, channel %d, PHC %s. Error: %s. Continuing bravely on...",
defaultTs2PhcIndex, defaultTs2PhcChannel, dev.File().Name(), err)
}

ts, err := dev.Time()
if err != nil {
return nil, fmt.Errorf("failed (clock_gettime) on %s", dev.File().Name())
}

// Set the index and period
peroutRequest.Index = defaultTs2PhcIndex
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
peroutRequest.Flags |= ptpPeroutDutyCycle
peroutRequest.On = PTPClockTime{Sec: int64(pulsewidth / nsPerSec), NSec: uint32(pulsewidth % nsPerSec)}

// Set phase or start time
// TODO: reintroduce peroutPhase != -1 condition once peroutPhase is configurable
peroutRequest.StartOrPhase = PTPClockTime{Sec: int64(ts.Second() + ppsStartDelay), NSec: 0}

err = dev.setPTPPerout(peroutRequest)

if err != nil {
log.Debugf("retrying PTP_PEROUT_REQUEST2 with DUTY_CYCLE flag unset for backwards compatibility")
peroutRequest.Flags &^= ptpPeroutDutyCycle
err = dev.setPTPPerout(peroutRequest)

if err != nil {
return nil, err
}
}

return &PPSSource{PHCDevice: dev, state: PPSSet}, nil
}

// Timestamp returns the timestamp of the last PPS output edge from the given PPS source
// A Pointer is returned to avoid additional memory allocation
func (ppsSource *PPSSource) Timestamp() (*time.Time, error) {
if ppsSource.state != PPSSet {
return nil, fmt.Errorf("PPS source not set")
}

currTime, err := ppsSource.PHCDevice.Time()

if err != nil {
return nil, fmt.Errorf("error getting time (clock_gettime) on %s", ppsSource.PHCDevice.File().Name())
}

// subtract device perout phase from current time to get the time of the last perout output edge
// TODO: optimize section below using binary operations instead of type conversions
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
}
//nolint:unconvert
currTime = time.Unix(int64(sourceTs.Sec), int64(sourceTs.Nsec))
currTime = currTime.Add(time.Duration(ppsSource.peroutPhase))

return &currTime, nil
}
163 changes: 0 additions & 163 deletions phc/phc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,9 @@ limitations under the License.
package phc

import (
"fmt"
"os"
"testing"
"time"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)

func TestIfaceInfoToPHCDevice(t *testing.T) {
Expand Down Expand Up @@ -57,161 +52,3 @@ func TestMaxAdjFreq(t *testing.T) {
got = caps.maxAdj()
require.InEpsilon(t, 500000.0, got, 0.00001)
}

func TestActivatePPSSource(t *testing.T) {
// Prepare
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDeviceController := NewMockDeviceController(ctrl)
var actualPeroutRequest PTPPeroutRequest
gomock.InOrder(
// Should set default pin to PPS
mockDeviceController.EXPECT().setPinFunc(uint(0), 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 }),
)

// Should call setPTPPerout with correct parameters
expectedPeroutRequest := PTPPeroutRequest{
Index: uint32(0),
Flags: uint32(2),
StartOrPhase: PTPClockTime{Sec: 2},
Period: PTPClockTime{Sec: 1},
On: PTPClockTime{NSec: 500000000},
}

// Act
ppsSource, err := ActivatePPSSource(mockDeviceController)

// Assert
require.NoError(t, err)
require.EqualValues(t, expectedPeroutRequest, actualPeroutRequest, "setPTPPerout parameter mismatch")
require.Equal(t, PPSSet, ppsSource.state)
}

func TestActivatePPSSourceIgnoreSetPinFailure(t *testing.T) {
// Prepare
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDeviceController := NewMockDeviceController(ctrl)
gomock.InOrder(
// If ioctl set pin fails, we continue bravely on...
mockDeviceController.EXPECT().setPinFunc(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("error")),
mockDeviceController.EXPECT().File().Return(os.NewFile(3, "mock_file")),
mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil),
mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(nil),
)

// Act
ppsSource, err := ActivatePPSSource(mockDeviceController)

// Assert
require.NoError(t, err)
require.Equal(t, PPSSet, ppsSource.state)
}

func TestActivatePPSSourceSetPTPPeroutFailure(t *testing.T) {
// Prepare
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDeviceController := NewMockDeviceController(ctrl)
var actualPeroutRequest PTPPeroutRequest
gomock.InOrder(
mockDeviceController.EXPECT().setPinFunc(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("error")),
mockDeviceController.EXPECT().File().Return(os.NewFile(3, "mock_file")),
mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil),
// If first attempt to set PTPPerout fails
mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(fmt.Errorf("error")),
// Should retry setPTPPerout with backward compatible flag
mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(nil).Do(func(arg PTPPeroutRequest) { actualPeroutRequest = arg }),
)
expectedPeroutRequest := PTPPeroutRequest{
Index: uint32(0),
Flags: uint32(0x0),
StartOrPhase: PTPClockTime{Sec: 2},
Period: PTPClockTime{Sec: 1},
On: PTPClockTime{NSec: 500000000},
}

// Act
ppsSource, err := ActivatePPSSource(mockDeviceController)

// Assert
require.NoError(t, err)
require.EqualValues(t, expectedPeroutRequest, actualPeroutRequest, "setPTPPerout parameter mismatch")
require.Equal(t, PPSSet, ppsSource.state)
}

func TestActivatePPSSourceSetPTPPeroutDoubleFailure(t *testing.T) {
// Prepare
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDeviceController := NewMockDeviceController(ctrl)
gomock.InOrder(
mockDeviceController.EXPECT().setPinFunc(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("error")),
mockDeviceController.EXPECT().File().Return(os.NewFile(3, "mock_file")),
mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil),
mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(fmt.Errorf("error")),
mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(fmt.Errorf("error")),
)

// Act
ppsSource, err := ActivatePPSSource(mockDeviceController)

// Assert
require.Error(t, err)
require.Nil(t, ppsSource)
}

func TestGetPPSTimestampSourceUnset(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDeviceController := NewMockDeviceController(ctrl)
ppsSource := PPSSource{PHCDevice: mockDeviceController}

// Act
_, err := ppsSource.Timestamp()

// Assert
require.Error(t, err)
}

func TestGetPPSTimestampMoreThanHalfNanossecondShouldAddSecond(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDeviceController := NewMockDeviceController(ctrl)
ppsSource := PPSSource{PHCDevice: mockDeviceController, state: PPSSet, peroutPhase: 23312}
mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500023313), nil)

// Act
timestamp, err := ppsSource.Timestamp()

// Assert
expected := time.Unix(1075896001, 23312)
require.NoError(t, err)
require.EqualValues(t, expected, *timestamp)
}

func TestGetPPSTimestampLessThanHalfNanossecondShouldKeepNanosseconds(t *testing.T) {
// Prepare
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDeviceController := NewMockDeviceController(ctrl)
ppsSource := PPSSource{PHCDevice: mockDeviceController, state: PPSSet, peroutPhase: 23312}
mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500023312), nil)

// Act
timestamp, err := ppsSource.Timestamp()

// Assert
expected := time.Unix(1075896000, 500023312)
require.NoError(t, err)
require.EqualValues(t, expected, *timestamp)
}

func TestTimeToTimespec(t *testing.T) {
someTime := time.Unix(1075896000, 500000000)
result := timeToTimespec(someTime)
require.Equal(t, result, unix.Timespec{Sec: 1075896000, Nsec: 500000000})
}
Loading
Loading