From 709ce5c7546b7830d34d73476d85bee77d8d6fb1 Mon Sep 17 00:00:00 2001 From: Vitaly Grinberg Date: Mon, 30 Mar 2026 17:05:11 +0300 Subject: [PATCH 1/9] Reject phase offsets of inactive input pins The DPLL FFO feature introduced additional pin reports. These additional reports are generated because the pin FFO changed, but the phase offset is indicated as 0. Such reports can be misinterpreted and acted upon in the clock state decision function. Ignore DPLL pin replies of inactive pins and exclude them from the phase offset decision chain. Only allow pin replays matched by clock ID and parent device of type "PPS" in the connected state. Signed-off-by: Vitaly Grinberg Made-with: Cursor --- pkg/dpll-netlink/dpll-uapi.go | 7 ++ pkg/dpll/dpll.go | 48 ++++----- pkg/dpll/dpll_internal_test.go | 172 +++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 22 deletions(-) create mode 100644 pkg/dpll/dpll_internal_test.go diff --git a/pkg/dpll-netlink/dpll-uapi.go b/pkg/dpll-netlink/dpll-uapi.go index acb300d3..643350c6 100644 --- a/pkg/dpll-netlink/dpll-uapi.go +++ b/pkg/dpll-netlink/dpll-uapi.go @@ -182,6 +182,13 @@ const ( DPLL_PIN_STATE_CONNECTED = 1 DPLL_PIN_STATE_DISCONNECTED = 2 DPLL_PIN_STATE_SELECTABLE = 3 + + PinStateConnected = DPLL_PIN_STATE_CONNECTED + PinStateDisconnected = DPLL_PIN_STATE_DISCONNECTED + PinStateSelectable = DPLL_PIN_STATE_SELECTABLE + + DpllTypePPS uint32 = 1 + DpllTypeEEC uint32 = 2 ) // GetPinState returns DPLL pin state as a string diff --git a/pkg/dpll/dpll.go b/pkg/dpll/dpll.go index 979298e3..3c79b493 100644 --- a/pkg/dpll/dpll.go +++ b/pkg/dpll/dpll.go @@ -121,9 +121,14 @@ type DpllConfig struct { // is driver-specific and vendor-specific. clockId uint64 sync.Mutex - isMonitoring bool - subscriber []*DpllSubscriber - phaseOffsetPinFilter map[string]map[string]string + isMonitoring bool + subscriber []*DpllSubscriber + phaseOffsetPinFilter map[string]map[string]string + inSyncConditionThreshold uint64 + inSyncConditionTimes uint64 + + // devices holds the cache of DPLL device replies + devices []*nl.DoDeviceGetReply } func (d *DpllConfig) InSpec() bool { @@ -327,26 +332,23 @@ func (d *DpllConfig) Timer() int64 { return d.timer } -func (d *DpllConfig) PhaseOffsetPin(pin *nl.PinInfo) bool { - - if pin.ClockId == d.clockId && pin.ParentDevice[PPS_PIN_INDEX].PhaseOffset != math.MaxInt64 { - for k, v := range d.phaseOffsetPinFilter[strconv.FormatUint(d.clockId, 10)] { - switch k { - case "boardLabel": - if strings.Compare(pin.BoardLabel, v) != 0 { - return false - } - case "panelLabel": - if strings.Compare(pin.PanelLabel, v) != 0 { - return false - } - default: - glog.Warningf("unsupported phase offset pin filter key: %s", k) +// ActivePhaseOffsetPin checks whether the given pin is actively connected +// and feeds the relevant PPS DPLL matched by clock ID +func (d *DpllConfig) ActivePhaseOffsetPin(pin *nl.PinInfo) (int, bool) { + if pin.ClockId != d.clockId { + return -1, false + } + for i, p := range pin.ParentDevice { + if p.State != nl.PinStateConnected { + continue + } + for _, dev := range d.devices { + if dev.Id == p.ParentId && dev.ClockId == d.clockId && nl.GetDpllType(dev.Type) == "pps" { + return i, true } } - return true } - return false + return -1, false } // nlUpdateState updates DPLL state in the DpllConfig structure. @@ -373,8 +375,8 @@ func (d *DpllConfig) nlUpdateState(devices []*nl.DoDeviceGetReply, pins []*nl.Pi } } for _, pin := range pins { - if d.PhaseOffsetPin(pin) { - d.SetPhaseOffset(pin.ParentDevice[PPS_PIN_INDEX].PhaseOffset) + if index, ok := d.ActivePhaseOffsetPin(pin); ok { + d.SetPhaseOffset(pin.ParentDevice[index].PhaseOffset) glog.Info("setting phase offset to ", d.phaseOffset, " ns for clock id ", d.clockId, " iface ", d.iface) valid = true } @@ -493,6 +495,8 @@ func (d *DpllConfig) MonitorDpllNetlink() { goto abort } + d.devices = replies + if d.nlUpdateState(replies, []*nl.PinInfo{}) { d.stateDecision() } diff --git a/pkg/dpll/dpll_internal_test.go b/pkg/dpll/dpll_internal_test.go new file mode 100644 index 00000000..b40ae4ca --- /dev/null +++ b/pkg/dpll/dpll_internal_test.go @@ -0,0 +1,172 @@ +package dpll + +import ( + "testing" + + nl "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" + "github.com/stretchr/testify/assert" +) + +func TestActivePhaseOffsetPin(t *testing.T) { + const ( + testClockID uint64 = 0xAABBCCDD + otherClockID uint64 = 0x11223344 + ppsDeviceID uint32 = 10 + eecDeviceID uint32 = 20 + otherDeviceID uint32 = 30 + ) + + ppsDevice := &nl.DoDeviceGetReply{ + Id: ppsDeviceID, + ClockId: testClockID, + Type: nl.DpllTypePPS, + } + eecDevice := &nl.DoDeviceGetReply{ + Id: eecDeviceID, + ClockId: testClockID, + Type: nl.DpllTypeEEC, + } + otherClockPPS := &nl.DoDeviceGetReply{ + Id: otherDeviceID, + ClockId: otherClockID, + Type: nl.DpllTypePPS, + } + + tests := []struct { + name string + clockID uint64 + devices []*nl.DoDeviceGetReply + pin *nl.PinInfo + expectedIndex int + expectedOk bool + }{ + { + name: "pin clock ID mismatch", + clockID: testClockID, + devices: []*nl.DoDeviceGetReply{ppsDevice}, + pin: &nl.PinInfo{ + ClockId: otherClockID, + ParentDevice: []nl.PinParentDevice{ + {ParentId: ppsDeviceID, State: nl.PinStateConnected}, + }, + }, + expectedIndex: -1, + expectedOk: false, + }, + { + name: "connected to PPS device with matching clock", + clockID: testClockID, + devices: []*nl.DoDeviceGetReply{ppsDevice, eecDevice}, + pin: &nl.PinInfo{ + ClockId: testClockID, + ParentDevice: []nl.PinParentDevice{ + {ParentId: ppsDeviceID, State: nl.PinStateConnected}, + }, + }, + expectedIndex: 0, + expectedOk: true, + }, + { + name: "disconnected from PPS device", + clockID: testClockID, + devices: []*nl.DoDeviceGetReply{ppsDevice}, + pin: &nl.PinInfo{ + ClockId: testClockID, + ParentDevice: []nl.PinParentDevice{ + {ParentId: ppsDeviceID, State: nl.PinStateDisconnected}, + }, + }, + expectedIndex: -1, + expectedOk: false, + }, + { + name: "connected to EEC device only", + clockID: testClockID, + devices: []*nl.DoDeviceGetReply{eecDevice}, + pin: &nl.PinInfo{ + ClockId: testClockID, + ParentDevice: []nl.PinParentDevice{ + {ParentId: eecDeviceID, State: nl.PinStateConnected}, + }, + }, + expectedIndex: -1, + expectedOk: false, + }, + { + name: "connected to PPS device but different clock ID in device", + clockID: testClockID, + devices: []*nl.DoDeviceGetReply{otherClockPPS}, + pin: &nl.PinInfo{ + ClockId: testClockID, + ParentDevice: []nl.PinParentDevice{ + {ParentId: otherDeviceID, State: nl.PinStateConnected}, + }, + }, + expectedIndex: -1, + expectedOk: false, + }, + { + name: "multiple parents, second is connected PPS", + clockID: testClockID, + devices: []*nl.DoDeviceGetReply{ppsDevice, eecDevice}, + pin: &nl.PinInfo{ + ClockId: testClockID, + ParentDevice: []nl.PinParentDevice{ + {ParentId: eecDeviceID, State: nl.PinStateConnected}, + {ParentId: ppsDeviceID, State: nl.PinStateConnected}, + }, + }, + expectedIndex: 1, + expectedOk: true, + }, + { + name: "selectable state is not connected", + clockID: testClockID, + devices: []*nl.DoDeviceGetReply{ppsDevice}, + pin: &nl.PinInfo{ + ClockId: testClockID, + ParentDevice: []nl.PinParentDevice{ + {ParentId: ppsDeviceID, State: nl.PinStateSelectable}, + }, + }, + expectedIndex: -1, + expectedOk: false, + }, + { + name: "no parent devices", + clockID: testClockID, + devices: []*nl.DoDeviceGetReply{ppsDevice}, + pin: &nl.PinInfo{ + ClockId: testClockID, + ParentDevice: []nl.PinParentDevice{}, + }, + expectedIndex: -1, + expectedOk: false, + }, + { + name: "no cached devices", + clockID: testClockID, + devices: nil, + pin: &nl.PinInfo{ + ClockId: testClockID, + ParentDevice: []nl.PinParentDevice{ + {ParentId: ppsDeviceID, State: nl.PinStateConnected}, + }, + }, + expectedIndex: -1, + expectedOk: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DpllConfig{ + clockId: tt.clockID, + devices: tt.devices, + } + index, ok := d.ActivePhaseOffsetPin(tt.pin) + assert.Equal(t, tt.expectedIndex, index, "device index") + assert.Equal(t, tt.expectedOk, ok, "match result") + }) + } +} From 15fd347c80f92e6ca9f5d5f86d1e6ebf845a7cec Mon Sep 17 00:00:00 2001 From: Vitaly Grinberg Date: Mon, 9 Mar 2026 22:44:28 +0200 Subject: [PATCH 2/9] Ignore phase offsets from non-input pins Signed-off-by: Vitaly Grinberg --- pkg/dpll-netlink/dpll-uapi.go | 3 +++ pkg/dpll/dpll.go | 2 +- pkg/dpll/dpll_internal_test.go | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/dpll-netlink/dpll-uapi.go b/pkg/dpll-netlink/dpll-uapi.go index 643350c6..2bc6aa24 100644 --- a/pkg/dpll-netlink/dpll-uapi.go +++ b/pkg/dpll-netlink/dpll-uapi.go @@ -187,6 +187,9 @@ const ( PinStateDisconnected = DPLL_PIN_STATE_DISCONNECTED PinStateSelectable = DPLL_PIN_STATE_SELECTABLE + PinDirectionInput = DPLL_PIN_DIRECTION_INPUT + PinDirectionOutput = DPLL_PIN_DIRECTION_OUTPUT + DpllTypePPS uint32 = 1 DpllTypeEEC uint32 = 2 ) diff --git a/pkg/dpll/dpll.go b/pkg/dpll/dpll.go index 3c79b493..5917419a 100644 --- a/pkg/dpll/dpll.go +++ b/pkg/dpll/dpll.go @@ -339,7 +339,7 @@ func (d *DpllConfig) ActivePhaseOffsetPin(pin *nl.PinInfo) (int, bool) { return -1, false } for i, p := range pin.ParentDevice { - if p.State != nl.PinStateConnected { + if p.State != nl.PinStateConnected || p.Direction != nl.PinDirectionInput { continue } for _, dev := range d.devices { diff --git a/pkg/dpll/dpll_internal_test.go b/pkg/dpll/dpll_internal_test.go index b40ae4ca..740f5dcf 100644 --- a/pkg/dpll/dpll_internal_test.go +++ b/pkg/dpll/dpll_internal_test.go @@ -60,7 +60,7 @@ func TestActivePhaseOffsetPin(t *testing.T) { pin: &nl.PinInfo{ ClockId: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: ppsDeviceID, State: nl.PinStateConnected}, + {ParentId: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, }, }, expectedIndex: 0, @@ -112,8 +112,8 @@ func TestActivePhaseOffsetPin(t *testing.T) { pin: &nl.PinInfo{ ClockId: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: eecDeviceID, State: nl.PinStateConnected}, - {ParentId: ppsDeviceID, State: nl.PinStateConnected}, + {ParentId: eecDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, + {ParentId: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, }, }, expectedIndex: 1, From eca626b640b61b2f33d572f8e6f4ce394386fd9a Mon Sep 17 00:00:00 2001 From: Vitaly Grinberg Date: Tue, 31 Mar 2026 11:32:18 +0300 Subject: [PATCH 3/9] Create DPLLPins abstraction and use them as fall back for SMA/U.FL pins Made-with: Cursor Signed-off-by: Vitaly Grinberg --- addons/intel/clock-chain.go | 574 +++++++++++------- addons/intel/clock-chain_test.go | 195 ++++++ addons/intel/clockID.go | 122 ++++ addons/intel/clockID_test.go | 144 +++++ addons/intel/common.go | 88 +++ addons/intel/dpllPins.go | 143 +++++ addons/intel/dpllPins_test.go | 463 ++++++++++++++ addons/intel/e810.go | 363 ++++------- addons/intel/e810_test.go | 502 +++++++++++++++ addons/intel/gnss_detect_test.go | 56 -- addons/intel/intel_test.go | 105 ---- addons/intel/mock.go | 12 - addons/intel/mock_test.go | 384 ++++++++++++ addons/intel/phaseAdjust.go | 70 ++- addons/intel/pinConfig.go | 75 +++ addons/intel/pinConfig_test.go | 93 +++ addons/intel/testdata/profile-t-tsc.yaml | 1 - .../testdata/profile-tbc-no-input-delays.yaml | 176 ++++++ addons/intel/testdata/profile-tbc.yaml | 5 +- addons/intel/testdata/profile-tgm-old.yaml | 1 - addons/intel/testdata/profile-tgm.yaml | 5 +- addons/intel/ublx.go | 116 ++++ addons/intel/ublx_test.go | 134 ++++ pkg/daemon/daemon.go | 30 +- pkg/daemon/testdata/profile-tbc.yaml | 1 - pkg/dpll-netlink/dpll-uapi.go | 389 +++++++----- pkg/dpll-netlink/dpll.go | 235 ++++--- pkg/dpll-netlink/dpll_test.go | 10 +- pkg/dpll/dpll.go | 327 +++++++--- pkg/dpll/dpll_internal_test.go | 48 +- pkg/dpll/dpll_test.go | 22 +- pkg/event/event.go | 5 + 32 files changed, 3847 insertions(+), 1047 deletions(-) create mode 100644 addons/intel/clock-chain_test.go create mode 100644 addons/intel/clockID.go create mode 100644 addons/intel/clockID_test.go create mode 100644 addons/intel/common.go create mode 100644 addons/intel/dpllPins.go create mode 100644 addons/intel/dpllPins_test.go create mode 100644 addons/intel/e810_test.go delete mode 100644 addons/intel/intel_test.go delete mode 100644 addons/intel/mock.go create mode 100644 addons/intel/mock_test.go create mode 100644 addons/intel/pinConfig.go create mode 100644 addons/intel/pinConfig_test.go create mode 100644 addons/intel/testdata/profile-tbc-no-input-delays.yaml create mode 100644 addons/intel/ublx.go create mode 100644 addons/intel/ublx_test.go diff --git a/addons/intel/clock-chain.go b/addons/intel/clock-chain.go index 8c3f32f7..59c883f8 100644 --- a/addons/intel/clock-chain.go +++ b/addons/intel/clock-chain.go @@ -1,10 +1,9 @@ package intel import ( + "errors" "fmt" - "os" - "slices" - "strconv" + "time" "github.com/golang/glog" dpll "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" @@ -12,49 +11,51 @@ import ( ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" ) -// FileSystemInterface defines the interface for filesystem operations to enable mocking. -type FileSystemInterface interface { - ReadDir(dirname string) ([]os.DirEntry, error) - WriteFile(filename string, data []byte, perm os.FileMode) error -} - -// RealFileSystem implements FileSystemInterface using real OS operations. -type RealFileSystem struct{} +type ( + // ClockChainType represents the type of clock chain + ClockChainType int -// ReadDir reads the contents of the directory specified by dirname. -func (fs *RealFileSystem) ReadDir(dirname string) ([]os.DirEntry, error) { - return os.ReadDir(dirname) -} + // ClockChain represents a set of interrelated clocks + ClockChain struct { + Type ClockChainType `json:"clockChainType"` + LeadingNIC CardInfo `json:"LeadingNIC"` + OtherNICs []CardInfo `json:"otherNICs,omitempty"` + DpllPins DPLLPins `json:"dpllPins"` + } -// WriteFile writes the data to the file specified by filename. -func (fs *RealFileSystem) WriteFile(filename string, data []byte, perm os.FileMode) error { - return os.WriteFile(filename, data, perm) -} + // ClockChainInterface is the mockable public interface of the ClockChain + ClockChainInterface interface { + EnterNormalTBC() error + EnterHoldoverTBC() error + SetPinDefaults() error + GetLeadingNIC() CardInfo + } -// Default filesystem implementation (used by clock-chain, gnss_detect, tests). -var filesystem FileSystemInterface = &RealFileSystem{} + // CardInfo represents an individual card in the clock chain + CardInfo struct { + Name string `json:"name"` + DpllClockID uint64 `json:"dpllClockId"` + // upstreamPort specifies the slave port in the T-BC case. For example, if the "name" + // is ens4f0, the "upstreamPort" could be ens4f1, depending on ptp4l config + UpstreamPort string `json:"upstreamPort"` + // Pins map[string]dpll.PinInfo `json:"pins"` + } -type ClockChainType int -type ClockChain struct { - Type ClockChainType `json:"clockChainType"` - LeadingNIC CardInfo `json:"leadingNIC"` - DpllPins []*dpll.PinInfo `json:"dpllPins"` -} -type CardInfo struct { - Name string `json:"name"` - DpllClockId string `json:"dpllClockId"` - // upstreamPort specifies the slave port in the T-BC case. For example, if the "name" - // is ens4f0, the "upstreamPort" could be ens4f1, depending on ptp4l config - UpstreamPort string `json:"upstreamPort"` - Pins map[string]dpll.PinInfo `json:"pins"` -} + // PhaseInputsProvider abstracts access to PhaseInputs so InitClockChain can + // accept different option structs (e.g., E810Opts) + PhaseInputsProvider interface { + GetPhaseInputs() []PhaseInputs + } +) const ( ClockTypeUnset ClockChainType = iota ClockTypeTGM ClockTypeTBC - PrioEnable = 0 - PrioDisable = 255 +) +const ( + PriorityEnabled = 0 + PriorityDisabled = 255 ) var ClockTypesMap = map[string]ClockChainType{ @@ -69,47 +70,46 @@ const ( sdp22 = "CVL-SDP22" sdp23 = "CVL-SDP23" gnss = "GNSS-1PPS" + sma1Input = "SMA1" + sma2Input = "SMA2/U.FL2" + c8270Rclka = "C827_0-RCLKA" + c8270Rclkb = "C827_0-RCLKB" eecDpllIndex = 0 ppsDpllIndex = 1 sdp22PpsEnable = "2 0 0 1 0" + sdp20PpsEnable = "1 0 0 1 0" + sma2 = "SMA2" ) type PinParentControl struct { - EecEnabled bool - PpsEnabled bool + EecPriority uint8 + PpsPriority uint8 + EecOutputState uint8 + PpsOutputState uint8 + EecDirection *uint8 + PpsDirection *uint8 } type PinControl struct { Label string ParentControl PinParentControl } -var internalPinLabels = []string{sdp20, sdp21, sdp22, sdp23, gnss} - -func (ch *ClockChain) GetLiveDpllPinsInfo() error { - if !unitTest { - conn, err := dpll.Dial(nil) - if err != nil { - return fmt.Errorf("failed to dial DPLL: %v", err) - } - defer conn.Close() - ch.DpllPins, err = conn.DumpPinGet() - if err != nil { - return fmt.Errorf("failed to dump DPLL pins: %v", err) - } - } else { - ch.DpllPins = DpllPins - } - return nil +// GetLeadingNIC returns the leading NIC from the clock chain +func (c *ClockChain) GetLeadingNIC() CardInfo { + return c.LeadingNIC } -func (ch *ClockChain) ResolveInterconnections(e810Opts E810Opts, nodeProfile *ptpv1.PtpProfile) (*[]delayCompensation, error) { +func (c *ClockChain) resolveInterconnections(opts PhaseInputsProvider, nodeProfile *ptpv1.PtpProfile) (*[]delayCompensation, error) { compensations := []delayCompensation{} - for _, card := range e810Opts.InputDelays { + for _, card := range opts.GetPhaseInputs() { delays, err := InitInternalDelays(card.Part) if err != nil { return nil, err } - if card.Input != nil { + glog.Infof("card: %+v", card) + var clockID uint64 + + if !card.GnssInput && card.UpstreamPort == "" { externalDelay := card.Input.DelayPs connector := card.Input.Connector link := findInternalLink(delays.ExternalInputs, connector) @@ -121,39 +121,43 @@ func (ch *ClockChain) ResolveInterconnections(e810Opts E810Opts, nodeProfile *pt pinLabel = link.Pin internalDelay = link.DelayPs - clockId, err := addClockId(card.Id, nodeProfile) + clockID, err = addClockID(card.ID, nodeProfile) if err != nil { return nil, err } - compensations = append(compensations, delayCompensation{ DelayPs: int32(externalDelay) + internalDelay, pinLabel: pinLabel, - iface: card.Id, + iface: card.ID, direction: "input", - clockId: *clockId, + clockID: clockID, + }) + // Track non-leading NICs + c.OtherNICs = append(c.OtherNICs, CardInfo{ + Name: card.ID, + DpllClockID: clockID, + UpstreamPort: card.UpstreamPort, }) } else { - ch.LeadingNIC.Name = card.Id - ch.LeadingNIC.UpstreamPort = card.UpstreamPort - clockId, err := addClockId(card.Id, nodeProfile) + c.LeadingNIC.Name = card.ID + c.LeadingNIC.UpstreamPort = card.UpstreamPort + c.LeadingNIC.DpllClockID, err = addClockID(card.ID, nodeProfile) if err != nil { return nil, err } - ch.LeadingNIC.DpllClockId = *clockId if card.GnssInput { - ch.Type = ClockTypeTGM + c.Type = ClockTypeTGM gnssLink := &delays.GnssInput compensations = append(compensations, delayCompensation{ DelayPs: gnssLink.DelayPs, pinLabel: gnssLink.Pin, - iface: card.Id, + iface: card.ID, direction: "input", - clockId: *clockId, + clockID: c.LeadingNIC.DpllClockID, }) } else { // if no GNSS and no external, then ptp4l input - ch.Type = ClockTypeTBC + c.Type = ClockTypeTBC } } for _, outputConn := range card.PhaseOutputConnectors { @@ -161,83 +165,67 @@ func (ch *ClockChain) ResolveInterconnections(e810Opts E810Opts, nodeProfile *pt if link == nil { return nil, fmt.Errorf("plugin E810 error: can't find connector %s in the card %s spec", outputConn, card.Part) } - clockId, err := addClockId(card.Id, nodeProfile) + clockID, err = addClockID(card.ID, nodeProfile) if err != nil { return nil, err } compensations = append(compensations, delayCompensation{ DelayPs: link.DelayPs, pinLabel: link.Pin, - iface: card.Id, + iface: card.ID, direction: "output", - clockId: *clockId, + clockID: clockID, }) } } return &compensations, nil } -func InitClockChain(e810Opts E810Opts, nodeProfile *ptpv1.PtpProfile) (*ClockChain, error) { - var chain = &ClockChain{ - LeadingNIC: CardInfo{ - Pins: make(map[string]dpll.PinInfo, 0), - }, +// InitClockChain initializes the ClockChain struct based on live DPLL pin info +func InitClockChain(opts PhaseInputsProvider, nodeProfile *ptpv1.PtpProfile) (*ClockChain, error) { + chain := &ClockChain{ + LeadingNIC: CardInfo{}, + OtherNICs: make([]CardInfo, 0), + DpllPins: DpllPins, } - err := chain.GetLiveDpllPinsInfo() + err := chain.DpllPins.FetchPins() if err != nil { return chain, err } - comps, err := chain.ResolveInterconnections(e810Opts, nodeProfile) + + comps, err := chain.resolveInterconnections(opts, nodeProfile) if err != nil { glog.Errorf("fail to get delay compensations, %s", err) } - if !unitTest { - err = sendDelayCompensation(comps, chain.DpllPins) - if err != nil { - glog.Errorf("fail to send delay compensations, %s", err) - } + err = SendDelayCompensation(comps, chain.DpllPins) + if err != nil { + glog.Errorf("fail to send delay compensations, %s", err) } - err = chain.GetLeadingCardSDP() + glog.Info("about to set DPLL pin priorities to defaults") + err = chain.SetPinDefaults() if err != nil { return chain, err } if chain.Type == ClockTypeTBC { (*nodeProfile).PtpSettings["clockType"] = "T-BC" glog.Info("about to init TBC pins") - _, err = chain.InitPinsTBC() + err = chain.InitPinsTBC() if err != nil { return chain, fmt.Errorf("failed to initialize pins for T-BC operation: %s", err.Error()) } glog.Info("about to enter TBC Normal mode") - _, err = chain.EnterNormalTBC() + err = chain.EnterNormalTBC() if err != nil { return chain, fmt.Errorf("failed to enter T-BC normal mode: %s", err.Error()) } - } else { - (*nodeProfile).PtpSettings["clockType"] = "T-GM" - glog.Info("about to init TGM pins") - _, err = chain.InitPinsTGM() } return chain, err } -func (ch *ClockChain) GetLeadingCardSDP() error { - clockId, err := strconv.ParseUint(ch.LeadingNIC.DpllClockId, 10, 64) - if err != nil { - return err - } - for _, pin := range ch.DpllPins { - if pin.ClockId == clockId && slices.Contains(internalPinLabels, pin.BoardLabel) { - ch.LeadingNIC.Pins[pin.BoardLabel] = *pin - } - } - return nil -} - func writeSysFs(path string, val string) error { glog.Infof("writing " + val + " to " + path) - err := filesystem.WriteFile(path, []byte(val), 0666) + err := filesystem.WriteFile(path, []byte(val), 0o666) if err != nil { return fmt.Errorf("e810 failed to write " + val + " to " + path + ": " + err.Error()) } @@ -247,258 +235,398 @@ func writeSysFs(path string, val string) error { func (c *ClockChain) SetPinsControl(pins []PinControl) (*[]dpll.PinParentDeviceCtl, error) { pinCommands := []dpll.PinParentDeviceCtl{} for _, pinCtl := range pins { - dpllPin, found := c.LeadingNIC.Pins[pinCtl.Label] - if !found { - return nil, fmt.Errorf("%s pin not found in the leading card", pinCtl.Label) + dpllPin := c.DpllPins.GetByLabel(pinCtl.Label, c.LeadingNIC.DpllClockID) + if dpllPin == nil { + glog.Errorf("pin not found with label %s for clockID %d", pinCtl.Label, c.LeadingNIC.DpllClockID) + continue } - pinCommand := SetPinControlData(dpllPin, pinCtl.ParentControl) - pinCommands = append(pinCommands, *pinCommand) + pinCommands = append(pinCommands, SetPinControlData(*dpllPin, pinCtl.ParentControl)...) } return &pinCommands, nil } -func SetPinControlData(pin dpll.PinInfo, control PinParentControl) *dpll.PinParentDeviceCtl { - Pin := dpll.PinParentDeviceCtl{ - Id: pin.Id, - PinParentCtl: make([]dpll.PinControl, 0), +// SetPinsControlForAllNICs sets pins across all NICs (leading + other NICs) +// This is used specifically for initialization functions like SetPinDefaults +func (c *ClockChain) SetPinsControlForAllNICs(pins []PinControl) (*[]dpll.PinParentDeviceCtl, error) { + pinCommands := []dpll.PinParentDeviceCtl{} + errs := make([]error, 0) + + for _, pinCtl := range pins { + foundPins := c.DpllPins.GetAllPinsByLabel(pinCtl.Label) + if len(foundPins) == 0 && pinCtl.Label == sma2Input { + pinCtl.Label = sma2 + foundPins = c.DpllPins.GetAllPinsByLabel(pinCtl.Label) + } + if len(foundPins) == 0 { + errs = append(errs, fmt.Errorf("pin %s not found on any nic", pinCtl.Label)) + continue + } + for _, pin := range foundPins { + pinCommands = append(pinCommands, SetPinControlData(*pin, pinCtl.ParentControl)...) + } + } + + return &pinCommands, errors.Join(errs...) +} + +// buildDirectionCmd checks if any parent device direction is changing. +// If so, it returns a direction-only command and updates pin.ParentDevice +// directions in place. Returns nil if no direction change is needed. +// The kernel rejects combining direction changes with prio/state. +func buildDirectionCmd(pin *dpll.PinInfo, control PinParentControl) *dpll.PinParentDeviceCtl { + var cmd *dpll.PinParentDeviceCtl + + for i, parentDevice := range pin.ParentDevice { + var direction *uint32 + switch i { + case eecDpllIndex: + if control.EecDirection != nil { + v := uint32(*control.EecDirection) + direction = &v + } + case ppsDpllIndex: + if control.PpsDirection != nil { + v := uint32(*control.PpsDirection) + direction = &v + } + } + if direction != nil && *direction != parentDevice.Direction && pin.Capabilities&dpll.PinCapDir != 0 { + if cmd == nil { + cmd = &dpll.PinParentDeviceCtl{ID: pin.ID} + } + cmd.PinParentCtl = append(cmd.PinParentCtl, + dpll.PinControl{PinParentID: parentDevice.ParentID, Direction: direction}) + pin.ParentDevice[i].Direction = *direction + } } - var enable bool - for deviceIndex, parentDevice := range pin.ParentDevice { - pc := dpll.PinControl{} - pc.PinParentId = parentDevice.ParentId - switch deviceIndex { + return cmd +} + +// SetPinControlData builds DPLL netlink commands to configure a pin's parent devices. +func SetPinControlData(pin dpll.PinInfo, control PinParentControl) []dpll.PinParentDeviceCtl { + dirCmd := buildDirectionCmd(&pin, control) + + cmd := dpll.PinParentDeviceCtl{ID: pin.ID} + for i, parentDevice := range pin.ParentDevice { + var prio, outputState uint32 + + switch i { case eecDpllIndex: - enable = control.EecEnabled + prio = uint32(control.EecPriority) + outputState = uint32(control.EecOutputState) case ppsDpllIndex: - enable = control.PpsEnabled + prio = uint32(control.PpsPriority) + outputState = uint32(control.PpsOutputState) } - if parentDevice.Direction == dpll.DPLL_PIN_DIRECTION_INPUT { - pc.Prio = func(enabled bool) *uint32 { - var p uint32 - if enabled { - p = PrioEnable - } else { - p = PrioDisable - } - return &p - }(enable) - } else { - pc.State = func(enabled bool) *uint32 { - var s uint32 - if enabled { - s = dpll.DPLL_PIN_STATE_CONNECTED - } else { - s = dpll.DPLL_PIN_STATE_DISCONNECTED - } - return &s - }(enable) + pc := dpll.PinControl{PinParentID: parentDevice.ParentID} + if parentDevice.Direction == dpll.PinDirectionInput { + if pin.Capabilities&dpll.PinCapState != 0 { + selectable := uint32(dpll.PinStateSelectable) + pc.State = &selectable + } + if parentDevice.Prio != nil && pin.Capabilities&dpll.PinCapPrio != 0 { + pc.Prio = &prio + } + } else if pin.Capabilities&dpll.PinCapState != 0 { + pc.State = &outputState } - Pin.PinParentCtl = append(Pin.PinParentCtl, pc) + cmd.PinParentCtl = append(cmd.PinParentCtl, pc) + } + + if dirCmd != nil { + return []dpll.PinParentDeviceCtl{*dirCmd, cmd} } - return &Pin + return []dpll.PinParentDeviceCtl{cmd} } func (c *ClockChain) EnableE810Outputs() error { + // # echo 2 2 > /sys/class/net/$ETH/device/ptp/ptp*/pins/SMA2 // # echo 2 0 0 1 0 > /sys/class/net/$ETH/device/ptp/ptp*/period var pinPath string - if unitTest { - glog.Info("skip pin config in unit test") - return nil - } else { - deviceDir := fmt.Sprintf("/sys/class/net/%s/device/ptp/", c.LeadingNIC.Name) - phcs, err := filesystem.ReadDir(deviceDir) + + deviceDir := fmt.Sprintf("/sys/class/net/%s/device/ptp/", c.LeadingNIC.Name) + phcs, err := filesystem.ReadDir(deviceDir) + if err != nil { + return fmt.Errorf("e810 failed to read " + deviceDir + ": " + err.Error()) + } + if len(phcs) > 1 { + glog.Error("e810 cards should have one PHC per NIC, but %s has %d", + c.LeadingNIC.Name, len(phcs)) + } + if len(phcs) == 0 { + e := fmt.Sprintf("e810 cards should have one PHC per NIC, but %s has 0", + c.LeadingNIC.Name) + glog.Error(e) + return errors.New(e) + } + if hasSysfsSMAPins(c.LeadingNIC.Name) { + err = pinConfig.applyPinSet(c.LeadingNIC.Name, pinSet{"SMA2": "2 2"}) if err != nil { - return fmt.Errorf("e810 failed to read " + deviceDir + ": " + err.Error()) + glog.Errorf("failed to set SMA2 pin via sysfs: %s", err) } - for _, phc := range phcs { - pinPath = fmt.Sprintf("/sys/class/net/%s/device/ptp/%s/period", c.LeadingNIC.Name, phc.Name()) - err := writeSysFs(pinPath, sdp22PpsEnable) - if err != nil { - return fmt.Errorf("failed to write " + sdp22PpsEnable + " to " + pinPath + ": " + err.Error()) - } + } else { + sma2Cmds := c.DpllPins.GetCommandsForPluginPinSet(c.LeadingNIC.DpllClockID, map[string]string{"SMA2": "2 2"}) + err = c.DpllPins.ApplyPinCommands(sma2Cmds) + if err != nil { + glog.Errorf("failed to set SMA2 pin to output: %s", err) } } + + pinPath = fmt.Sprintf("%s%s/period", deviceDir, phcs[0].Name()) + err = writeSysFs(pinPath, sdp22PpsEnable) + if err != nil { + glog.Errorf("failed to write " + sdp22PpsEnable + " to " + pinPath + ": " + err.Error()) + } + return nil } // InitPinsTBC initializes the leading card E810 and DPLL pins for T-BC operation -func (c *ClockChain) InitPinsTBC() (*[]dpll.PinParentDeviceCtl, error) { +func (c *ClockChain) InitPinsTBC() error { // Enable 1PPS output on SDP22 // (To synchronize the DPLL1 to the E810 PHC synced by ptp4l): err := c.EnableE810Outputs() if err != nil { - return nil, err + return fmt.Errorf("failed to enable E810 outputs: %w", err) } - // Disable GNSS-1PPS, SDP20 and SDP21 - commands, err := c.SetPinsControl([]PinControl{ + // Disable GNSS-1PPS (all cards), SDP20 and SDP21 + commandsGnss, err := c.SetPinsControlForAllNICs([]PinControl{ { Label: gnss, ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, + EecPriority: PriorityDisabled, + PpsPriority: PriorityDisabled, + }, + }, + }) + if err != nil { + glog.Error("failed to disable GNSS: ", err) + } + commands, err := c.SetPinsControl([]PinControl{ + + { + Label: sdp22, + ParentControl: PinParentControl{ + EecPriority: PriorityDisabled, + PpsPriority: PriorityDisabled, }, }, { Label: sdp20, ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, + EecPriority: PriorityDisabled, + PpsPriority: PriorityDisabled, }, }, { Label: sdp21, ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, + EecOutputState: dpll.PinStateDisconnected, + PpsOutputState: dpll.PinStateDisconnected, + }, + }, + { + Label: sdp23, + ParentControl: PinParentControl{ + EecOutputState: dpll.PinStateDisconnected, + PpsOutputState: dpll.PinStateDisconnected, }, }, }) if err != nil { - return nil, err + glog.Error("failed to set pins control: ", err) } - return commands, BatchPinSet(commands) + *commands = append(*commands, *commandsGnss...) + + err = BatchPinSet(commands) + // even if there was an error we still need to refresh the pin state. + fetchErr := c.DpllPins.FetchPins() + return errors.Join(err, fetchErr) } // EnterHoldoverTBC configures the leading card DPLL pins for T-BC holdover -func (c *ClockChain) EnterHoldoverTBC() (*[]dpll.PinParentDeviceCtl, error) { +func (c *ClockChain) EnterHoldoverTBC() error { // Disable DPLL inputs from e810 (SDP22) // Enable DPLL Outputs to e810 (SDP21, SDP23) commands, err := c.SetPinsControl([]PinControl{ { Label: sdp22, ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, + EecPriority: PriorityDisabled, + PpsPriority: PriorityDisabled, }, }, { Label: sdp23, ParentControl: PinParentControl{ - EecEnabled: true, - PpsEnabled: true, - }, - }, - { - Label: sdp21, - ParentControl: PinParentControl{ - EecEnabled: true, - PpsEnabled: true, + EecOutputState: dpll.PinStateConnected, + PpsOutputState: dpll.PinStateConnected, }, }, }) if err != nil { - return nil, err + return err } - return commands, BatchPinSet(commands) + err = BatchPinSet(commands) + // even if there was an error we still need to refresh the pin state. + fetchErr := c.DpllPins.FetchPins() + return errors.Join(err, fetchErr) } // EnterNormalTBC configures the leading card DPLL pins for regular T-BC operation -func (c *ClockChain) EnterNormalTBC() (*[]dpll.PinParentDeviceCtl, error) { +func (c *ClockChain) EnterNormalTBC() error { // Disable DPLL Outputs to e810 (SDP23, SDP21) // Enable DPLL inputs from e810 (SDP22) commands, err := c.SetPinsControl([]PinControl{ { Label: sdp22, ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: true, - }, - }, - { - Label: sdp21, - ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, + EecPriority: PriorityDisabled, + PpsPriority: PriorityEnabled, }, }, { Label: sdp23, ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, + EecOutputState: dpll.PinStateDisconnected, + PpsOutputState: dpll.PinStateDisconnected, }, }, }) if err != nil { - return nil, err + return err } - return commands, BatchPinSet(commands) + err = BatchPinSet(commands) + // even if there was an error we still need to refresh the pin state. + fetchErr := c.DpllPins.FetchPins() + return errors.Join(err, fetchErr) } -func (c *ClockChain) InitPinsTGM() (*[]dpll.PinParentDeviceCtl, error) { - // Set GNSS-1PPS priority to 0 (max priority) - // Disable DPLL inputs from e810 (SDP20, SDP22) - // Enable DPLL Outputs to e810 (SDP21, SDP23) - commands, err := c.SetPinsControl([]PinControl{ +// SetPinDefaults initializes DPLL pins to default recommended values +func (c *ClockChain) SetPinDefaults() error { + // DPLL Priority List: + // + // Recommended | Pin Index | EEC-DPLL0 | PPS-DPLL1 + // Priority | | (Frequency/Glitchless) | (Phase/Glitch Allowed) + // ------------|-----------|------------------------------|----------------------------- + // 0 | 6 | 1PPS from GNSS (GNSS-1PPS) | 1PPS from GNSS (GNSS-1PPS) + // 2 | 5 | 1PPS from SMA2 (SMA2) | 1PPS from SMA2 (SMA2) + // 3 | 4 | 1PPS from SMA1 (SMA1) | 1PPS from SMA1 (SMA1) + // 4 | 1 | Reserved | 1PPS from E810 (CVL-SDP20) + // 5 | 0 | Reserved | 1PPS from E810 (CVL-SDP22) + // 6 | -- | Reserved | Reserved + // 7 | -- | Reserved | Reserved + // 8 | 2 | Recovered CLK1 (C827_0-RCLKA) | Recovered CLK1 (C827_0-RCLKA) + // 9 | 3 | Recovered CLK2 (C827_0-RCLKB) | Recovered CLK2 (C827_0-RCLKB) + // 10 | -- | OCXO | OCXO + d := uint8(dpll.PinDirectionInput) + // Also, Enable DPLL Outputs to e810 (SDP21, SDP23) + commands, err := c.SetPinsControlForAllNICs([]PinControl{ { Label: gnss, ParentControl: PinParentControl{ - EecEnabled: true, - PpsEnabled: true, + EecPriority: 0, + PpsPriority: 0, + }, + }, + { + Label: sma1Input, + ParentControl: PinParentControl{ + EecPriority: 3, + PpsPriority: 3, + EecDirection: &d, + PpsDirection: &d, + }, + }, + { + Label: sma2Input, + ParentControl: PinParentControl{ + EecPriority: 2, + PpsPriority: 2, + EecDirection: &d, + PpsDirection: &d, }, }, { Label: sdp20, ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, + EecPriority: PriorityDisabled, + PpsPriority: 4, }, }, { Label: sdp22, ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, + EecPriority: PriorityDisabled, + PpsPriority: 5, }, }, { Label: sdp21, ParentControl: PinParentControl{ - EecEnabled: true, - PpsEnabled: true, + EecOutputState: dpll.PinStateDisconnected, + PpsOutputState: dpll.PinStateConnected, }, }, { Label: sdp23, ParentControl: PinParentControl{ - EecEnabled: true, - PpsEnabled: true, + EecOutputState: dpll.PinStateDisconnected, + PpsOutputState: dpll.PinStateConnected, + }, + }, + { + Label: c8270Rclka, + ParentControl: PinParentControl{ + EecPriority: 8, + PpsPriority: 8, + }, + }, + { + Label: c8270Rclkb, + ParentControl: PinParentControl{ + EecPriority: 9, + PpsPriority: 9, }, }, }) if err != nil { - return nil, err + return err } - return commands, BatchPinSet(commands) + err = BatchPinSet(commands) + // even if there was an error we still need to refresh the pin state. + fetchErr := c.DpllPins.FetchPins() + return errors.Join(err, fetchErr) } -func BatchPinSet(commands *[]dpll.PinParentDeviceCtl) error { - if unitTest { - return nil - } +// BatchPinSet function pointer allows mocking of BatchPinSet +var BatchPinSet = batchPinSet + +func batchPinSet(commands *[]dpll.PinParentDeviceCtl) error { conn, err := dpll.Dial(nil) if err != nil { return fmt.Errorf("failed to dial DPLL: %v", err) } + //nolint:errcheck defer conn.Close() for _, command := range *commands { - glog.Infof("DPLL pin command %++v", command) + glog.Infof("DPLL pin command %#v", command) b, err := dpll.EncodePinControl(command) if err != nil { return err } - err = conn.SendCommand(dpll.DPLL_CMD_PIN_SET, b) + err = conn.SendCommand(dpll.DpllCmdPinSet, b) if err != nil { glog.Error("failed to send pin command: ", err) return err } - info, err := conn.DoPinGet(dpll.DoPinGetRequest{Id: command.Id}) + info, err := conn.DoPinGet(dpll.DoPinGetRequest{ID: command.ID}) if err != nil { glog.Error("failed to get pin: ", err) return err } - reply, err := dpll.GetPinInfoHR(info) + reply, err := dpll.GetPinInfoHR(info, time.Now()) if err != nil { glog.Error("failed to convert pin reply to human readable: ", err) return err diff --git a/addons/intel/clock-chain_test.go b/addons/intel/clock-chain_test.go new file mode 100644 index 00000000..f37b361e --- /dev/null +++ b/addons/intel/clock-chain_test.go @@ -0,0 +1,195 @@ +package intel + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ProcessProfileTbcClockChain(t *testing.T) { + _, restoreDPLLPins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json") + defer restoreDPLLPins() + restoreDelay := setupMockDelayCompensation() + defer restoreDelay() + mockPinSet, restorePinSet := setupBatchPinSetMock() + defer restorePinSet() + // Setup filesystem mock for TBC profile (3 devices with pins) + mockFS, restoreFs := setupMockFS() + defer restoreFs() + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + + // EnableE810Outputs is called for the leading NIC (ens4f0) - needs specific paths + mockFS.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + mockFS.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + mockFS.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) + + mockPinConfig, restorePins := setupMockPinConfig() + defer restorePins() + + // Can read test profile + profile, err := loadProfile("./testdata/profile-tbc.yaml") + assert.NoError(t, err) + + mockClockIDsFromProfile(mockFS, profile) + + // Can run PTP config change handler without errors + p, d := E810("e810") + err = p.OnPTPConfigChange(d, profile) + assert.NoError(t, err) + ccData := clockChain.(*ClockChain) + assert.Equal(t, ClockTypeTBC, ccData.Type, "identified a wrong clock type") + assert.Equal(t, uint64(5799633565432596414), ccData.LeadingNIC.DpllClockID, "identified a wrong clock ID ") + assert.Equal(t, "ens4f1", ccData.LeadingNIC.UpstreamPort, "wrong upstream port") + assert.Equal(t, 0, mockPinConfig.actualPinSetCount) + assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) + assert.NotNil(t, mockPinSet.commands, "DPLL commands should have been issued") + assert.Greater(t, len(*mockPinSet.commands), 0, "should have DPLL pin commands") + + // Test holdover entry + mockPinSet.reset() + err = clockChain.EnterHoldoverTBC() + assert.NoError(t, err) + assert.Equal(t, 2, len(*mockPinSet.commands)) + + // Test holdover exit + mockPinSet.reset() + err = clockChain.EnterNormalTBC() + assert.NoError(t, err) + assert.Equal(t, 2, len(*mockPinSet.commands)) + + // Ensure switching back to TGM resets any pins + mockPinSet.reset() + tgmProfile, err := loadProfile("./testdata/profile-tgm.yaml") + assert.NoError(t, err) + err = OnPTPConfigChangeE810(nil, tgmProfile) + assert.NoError(t, err) + assert.NotNil(t, mockPinSet.commands, "Ensure clockChain.SetPinDefaults was called") +} + +func Test_ProcessProfileTtscClockChain(t *testing.T) { + _, restoreDPLLPins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json") + defer restoreDPLLPins() + restoreDelay := setupMockDelayCompensation() + defer restoreDelay() + mockPinSet, restorePinSet := setupBatchPinSetMock() + defer restorePinSet() + // Setup filesystem mock for T-TSC profile (1 device with pins) + mockFS, restoreFs := setupMockFS() + defer restoreFs() + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + + // EnableE810Outputs is called for the leading NIC (ens4f0) - needs specific paths + mockFS.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + mockFS.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + mockFS.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) + + mockPinConfig, restorePins := setupMockPinConfig() + defer restorePins() + + // Can read test profile + profile, err := loadProfile("./testdata/profile-t-tsc.yaml") + assert.NoError(t, err) + + mockClockIDsFromProfile(mockFS, profile) + + // Can run PTP config change handler without errors + p, d := E810("e810") + err = p.OnPTPConfigChange(d, profile) + assert.NoError(t, err) + ccData := clockChain.(*ClockChain) + assert.Equal(t, ClockTypeTBC, ccData.Type, "identified a wrong clock type") + assert.Equal(t, uint64(5799633565432596414), ccData.LeadingNIC.DpllClockID, "identified a wrong clock ID ") + assert.Equal(t, "ens4f1", ccData.LeadingNIC.UpstreamPort, "wrong upstream port") + assert.NotNil(t, mockPinSet.commands, "Ensure some pins were set") + assert.Equal(t, 0, mockPinConfig.actualPinSetCount) + assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) + + // Test holdover entry + mockPinSet.reset() + err = clockChain.EnterHoldoverTBC() + assert.NoError(t, err) + assert.Equal(t, 2, len(*mockPinSet.commands)) + + // Test holdover exit + mockPinSet.reset() + err = clockChain.EnterNormalTBC() + assert.NoError(t, err) + assert.Equal(t, 2, len(*mockPinSet.commands)) +} + +func Test_SetPinDefaults_AllNICs(t *testing.T) { + _, restoreDPLLPins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json") + defer restoreDPLLPins() + restoreDelay := setupMockDelayCompensation() + defer restoreDelay() + mockPinSet, restorePinSet := setupBatchPinSetMock() + defer restorePinSet() + + mockPinConfig, restorePinConfig := setupMockPinConfig() + defer restorePinConfig() + + // Setup filesystem mock for EnableE810Outputs + mockFS, restoreFs := setupMockFS() + defer restoreFs() + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + mockFS.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + mockFS.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + mockFS.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) + + // Load a profile with multiple NICs (leading + other NICs) + profile, err := loadProfile("./testdata/profile-tbc.yaml") + assert.NoError(t, err) + + mockClockIDsFromProfile(mockFS, profile) + + // Initialize the clock chain with multiple NICs + err = OnPTPConfigChangeE810(nil, profile) + assert.NoError(t, err) + assert.Equal(t, 0, mockPinConfig.actualPinSetCount) + assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) + + // Verify we have the expected clock chain structure + ccData := clockChain.(*ClockChain) + assert.Equal(t, ClockTypeTBC, ccData.Type) + assert.Equal(t, "ens4f0", ccData.LeadingNIC.Name) + assert.Equal(t, 2, len(ccData.OtherNICs), "should have 2 other NICs (ens5f0, ens8f0)") + + // Reset to only capture commands from the explicit SetPinDefaults call + mockPinSet.reset() + err = clockChain.SetPinDefaults() + assert.NoError(t, err) + assert.NotNil(t, mockPinSet.commands) + + // SetPinDefaults configures 9 different pin types, and we have 3 NICs total + // Each pin type should have a command for each NIC that has that pin + assert.Equal(t, len(*mockPinSet.commands), 27, "should have exactly 27 pin commands") + + // Verify that commands include pins from multiple clock IDs + clockIDsSeen := make(map[uint64]bool) + pinLabelsSeen := make(map[string]bool) + + mockPins := ccData.DpllPins.(*mockedDPLLPins) + for _, cmd := range *mockPinSet.commands { + // Find which pin this command refers to by searching all pins + for _, pin := range mockPins.pins { + if pin.ID == cmd.ID { + clockIDsSeen[pin.ClockID] = true + pinLabelsSeen[pin.BoardLabel] = true + break + } + } + } + + // We should see commands for multiple clock IDs (multiple NICs) + assert.GreaterOrEqual(t, len(clockIDsSeen), 2, "should have commands for at least 2 different clock IDs") + + // We should see commands for the standard configurable pin types + expectedPins := []string{ + "GNSS-1PPS", "SMA1", "SMA2/U.FL2", "CVL-SDP20", "CVL-SDP22", + "CVL-SDP21", "CVL-SDP23", "C827_0-RCLKA", "C827_0-RCLKB", + } + for _, expectedPin := range expectedPins { + assert.True(t, pinLabelsSeen[expectedPin], "should have command for pin %s", expectedPin) + } +} diff --git a/addons/intel/clockID.go b/addons/intel/clockID.go new file mode 100644 index 00000000..e96cc774 --- /dev/null +++ b/addons/intel/clockID.go @@ -0,0 +1,122 @@ +package intel + +import ( + "encoding/binary" + "fmt" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/golang/glog" + dpll "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" +) + +const ( + pciConfigSpaceSize = 256 + pciExtendedCapabilityDsnID = 3 + pciExtendedCapabilityNextOffset = 2 + pciExtendedCapabilityOffsetShift = 4 + pciExtendedCapabilityDataOffset = 4 +) + +// getClockID returns the DPLL clock ID for a network device. +func getClockID(device string) uint64 { + clockID := getPCIClockID(device) + if clockID != 0 { + return clockID + } + return getDevlinkClockID(device) +} + +func getPCIClockID(device string) uint64 { + b, err := filesystem.ReadFile(fmt.Sprintf("/sys/class/net/%s/device/config", device)) + if err != nil { + glog.Error(err) + return 0 + } + var offset uint16 = pciConfigSpaceSize + var id uint16 + for { + // TODO: Add test for == case + if len(b) <= int(offset) { + glog.Errorf("PCI config space too short (%d bytes) for device %s", len(b), device) + return 0 + } + id = binary.LittleEndian.Uint16(b[offset:]) + if id != pciExtendedCapabilityDsnID { + if id == 0 { + glog.Errorf("DSN capability not found for device %s", device) + return 0 + } + offset = binary.LittleEndian.Uint16(b[offset+pciExtendedCapabilityNextOffset:]) >> pciExtendedCapabilityOffsetShift + continue + } + break + } + return binary.LittleEndian.Uint64(b[offset+pciExtendedCapabilityDataOffset:]) +} + +func getDevlinkClockID(device string) uint64 { + devicePath, err := filesystem.ReadLink(fmt.Sprintf("/sys/class/net/%s/device", device)) + if err != nil { + glog.Errorf("failed to resolve PCI address for %s: %v", device, err) + return 0 + } + pciAddr := filepath.Base(devicePath) + + out, err := exec.Command("devlink", "dev", "info", "pci/"+pciAddr).Output() + if err != nil { + glog.Errorf("getDevlinkClockID: devlink failed for %s (pci/%s): %v", device, pciAddr, err) + return 0 + } + + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(strings.TrimSpace(line)) + if len(fields) != 2 || fields[0] != "serial_number" { + continue + } + var clockID uint64 + // "50-7c-6f-ff-ff-1f-b5-80" -> "507c6fffff1fb580" -> 0x507c6fffff1fb580 + clockID, err = strconv.ParseUint(strings.ReplaceAll(fields[1], "-", ""), 16, 64) + if err != nil { + glog.Errorf("getDevlinkClockID: failed to parse serial '%s': %v", fields[1], err) + return 0 + } + return clockID + } + glog.Errorf("getDevlinkClockID: serial_number not found in devlink output for %s (pci/%s)", device, pciAddr) + return 0 +} + +// Using a named anonymous function to allow mocking +var getAllDpllDevices = func() ([]*dpll.DoDeviceGetReply, error) { + conn, err := dpll.Dial(nil) + if err != nil { + return nil, err + } + //nolint:errcheck + defer conn.Close() + return conn.DumpDeviceGet() +} + +// getClockIDByModule returns ClockID for a given DPLL module name, preferring PPS type if present +func getClockIDByModule(module string) (uint64, error) { + devices, err := getAllDpllDevices() + if err != nil { + return 0, err + } + var anyID uint64 + for _, d := range devices { + if strings.EqualFold(d.ModuleName, module) { + if d.Type == 1 { // PPS + return d.ClockID, nil + } + anyID = d.ClockID + } + } + if anyID != 0 { + return anyID, nil + } + return 0, fmt.Errorf("module %s DPLL not found", module) +} diff --git a/addons/intel/clockID_test.go b/addons/intel/clockID_test.go new file mode 100644 index 00000000..a3e21ed4 --- /dev/null +++ b/addons/intel/clockID_test.go @@ -0,0 +1,144 @@ +package intel + +import ( + "encoding/binary" + "fmt" + "testing" + + dpll "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" + "github.com/stretchr/testify/assert" +) + +func generatePCIDataForClockID(id uint64) []byte { + return generateTestPCIDataForClockID(id, 0) +} + +func generateTestPCIDataForClockID(id uint64, extraSections int) []byte { + result := make([]byte, pciConfigSpaceSize) + nextSectionOffset := pciConfigSpaceSize + for i := range extraSections { + id := i + pciExtendedCapabilityDsnID + 1 // Anything != pciExtendedCapabilityDsnID + sectionSize := (1 << pciExtendedCapabilityOffsetShift) * (i + 1) + dataSize := sectionSize - pciExtendedCapabilityDataOffset + nextSectionOffset = nextSectionOffset + sectionSize + shiftedOffset := nextSectionOffset << pciExtendedCapabilityOffsetShift + result = binary.LittleEndian.AppendUint16(result, uint16(id)) + result = binary.LittleEndian.AppendUint16(result, uint16(shiftedOffset)) + result = append(result, make([]byte, dataSize)...) + } + result = binary.LittleEndian.AppendUint16(result, uint16(pciExtendedCapabilityDsnID)) // 16-bit capability id + result = binary.LittleEndian.AppendUint16(result, uint16(1)) // 16-bit capability size (shiftedl + result = binary.LittleEndian.AppendUint64(result, id) + return result +} + +func Test_getPCIClockID(t *testing.T) { + mfs, restore := setupMockFS() + defer restore() + + notFound := uint64(0) + + // No such file + mfs.ExpectReadFile("/sys/class/net/missing/device/config", []byte{}, fmt.Errorf("No such file")) + clockID := getPCIClockID("missing") + assert.Equal(t, notFound, clockID) + mfs.VerifyAllCalls(t) + + // Config empty + mfs.ExpectReadFile("/sys/class/net/short/device/config", []byte{}, nil) + clockID = getPCIClockID("short") + assert.Equal(t, notFound, clockID) + mfs.VerifyAllCalls(t) + + // No DSN + mfs.ExpectReadFile("/sys/class/net/empty/device/config", make([]byte, pciConfigSpaceSize+64), nil) + clockID = getPCIClockID("empty") + assert.Equal(t, notFound, clockID) + mfs.VerifyAllCalls(t) + + // DSN at start of space + expectedID := uint64(1111222233334) + mfs.ExpectReadFile("/sys/class/net/one/device/config", generatePCIDataForClockID(expectedID), nil) + clockID = getPCIClockID("one") + assert.Equal(t, expectedID, clockID) + mfs.VerifyAllCalls(t) + + // DSN after a few other sections + expectedID = uint64(5555666677778) + mfs.ExpectReadFile("/sys/class/net/two/device/config", generateTestPCIDataForClockID(expectedID, 3), nil) + clockID = getPCIClockID("two") + assert.Equal(t, expectedID, clockID) + mfs.VerifyAllCalls(t) + + // Config space truncated + expectedID = uint64(7777888899990) + fullData := generateTestPCIDataForClockID(expectedID, 2) + mfs.ExpectReadFile("/sys/class/net/truncated/device/config", fullData[:len(fullData)-16], nil) + clockID = getPCIClockID("truncated") + assert.Equal(t, notFound, clockID) + mfs.VerifyAllCalls(t) +} + +func Test_getClockIDByModule(t *testing.T) { + notFound := uint64(0) + + getAllDpllDevices = func() ([]*dpll.DoDeviceGetReply, error) { + return nil, fmt.Errorf("Fake error") + } + clockID, err := getClockIDByModule("module") + assert.Error(t, err) + assert.Equal(t, notFound, clockID) + + getAllDpllDevices = func() ([]*dpll.DoDeviceGetReply, error) { + return []*dpll.DoDeviceGetReply{}, nil + } + clockID, err = getClockIDByModule("module") + assert.Error(t, err) + assert.Equal(t, notFound, clockID) + + getAllDpllDevices = func() ([]*dpll.DoDeviceGetReply, error) { + return []*dpll.DoDeviceGetReply{ + { + ID: 0, + ModuleName: "other", + Type: 1, + ClockID: 1, + }, + { + ID: 1, + ModuleName: "module", + Type: 2, + ClockID: 2, + }, + { + ID: 2, + ModuleName: "module", + Type: 1, + ClockID: 42, + }, + }, nil + } + clockID, err = getClockIDByModule("module") + assert.NoError(t, err) + assert.Equal(t, uint64(42), clockID) + + getAllDpllDevices = func() ([]*dpll.DoDeviceGetReply, error) { + return []*dpll.DoDeviceGetReply{ + { + ID: 0, + ModuleName: "other", + Type: 1, + ClockID: 1, + }, + { + ID: 1, + ModuleName: "module", + Type: 2, + ClockID: 2, + }, + }, nil + } + clockID, err = getClockIDByModule("module") + assert.NoError(t, err) + assert.Equal(t, uint64(2), clockID) +} diff --git a/addons/intel/common.go b/addons/intel/common.go new file mode 100644 index 00000000..2e8237e7 --- /dev/null +++ b/addons/intel/common.go @@ -0,0 +1,88 @@ +// Package intel contains plugins for all supported Intel NICs +package intel + +import ( + "os" + "slices" + + ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" +) + +// PluginOpts contains all configuration data common to all addons/intel NIC plugins +type PluginOpts struct { + Devices []string `json:"devices"` + DevicePins map[string]pinSet `json:"pins"` + DeviceFreqencies map[string]frqSet `json:"frequencies"` + DpllSettings map[string]uint64 `json:"settings"` + PhaseOffsetPins map[string]map[string]string `json:"phaseOffsetPins"` +} + +// PluginData contains all persistent data commont to all addons/intel NIC plugins +type PluginData struct { + name string + hwplugins []string +} + +// PopulateHwConfig populates hwconfig for all intel plugins +func (data *PluginData) PopulateHwConfig(_ *interface{}, hwconfigs *[]ptpv1.HwConfig) error { + for _, _hwconfig := range data.hwplugins { + hwConfig := ptpv1.HwConfig{} + hwConfig.DeviceID = data.name + hwConfig.Status = _hwconfig + *hwconfigs = append(*hwconfigs, hwConfig) + } + return nil +} + +func extendWithKeys[T any](s []string, m map[string]T) []string { + for key := range m { + if !slices.Contains(s, key) { + s = append(s, key) + } + } + return s +} + +// allDevices enumerates all defined devices (Devices/DevicePins/DeviceFrequencies/PhaseOffsets) +func (opts *PluginOpts) allDevices() []string { + // Enumerate all defined devices (Devices/DevicePins/DeviceFrequencies) + allDevices := opts.Devices + allDevices = extendWithKeys(allDevices, opts.DevicePins) + allDevices = extendWithKeys(allDevices, opts.DeviceFreqencies) + allDevices = extendWithKeys(allDevices, opts.PhaseOffsetPins) + return allDevices +} + +// FileSystemInterface defines the interface for filesystem operations to enable mocking +type FileSystemInterface interface { + ReadDir(dirname string) ([]os.DirEntry, error) + WriteFile(filename string, data []byte, perm os.FileMode) error + ReadFile(filename string) ([]byte, error) + ReadLink(filename string) (string, error) +} + +// RealFileSystem implements FileSystemInterface using real OS operations +type RealFileSystem struct{} + +// ReadDir reads the contents of the directory specified by dirname +func (fs *RealFileSystem) ReadDir(dirname string) ([]os.DirEntry, error) { + return os.ReadDir(dirname) +} + +// WriteFile writes the data to the file specified by filename +func (fs *RealFileSystem) WriteFile(filename string, data []byte, perm os.FileMode) error { + return os.WriteFile(filename, data, perm) +} + +// ReadFile reads the data from the file specified by the filename +func (fs *RealFileSystem) ReadFile(filename string) ([]byte, error) { + return os.ReadFile(filename) +} + +// ReadLink returns the destination of a symbolic link. +func (fs *RealFileSystem) ReadLink(filename string) (string, error) { + return os.Readlink(filename) +} + +// Default filesystem implementation +var filesystem FileSystemInterface = &RealFileSystem{} diff --git a/addons/intel/dpllPins.go b/addons/intel/dpllPins.go new file mode 100644 index 00000000..1220e7d8 --- /dev/null +++ b/addons/intel/dpllPins.go @@ -0,0 +1,143 @@ +package intel + +import ( + "errors" + "fmt" + "strings" + + "github.com/golang/glog" + dpll "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" +) + +// DPLLPins abstracts DPLL pin operations for mocking in tests. +type DPLLPins interface { + ApplyPinCommands(commands []dpll.PinParentDeviceCtl) error + FetchPins() error + GetByLabel(label string, clockID uint64) *dpll.PinInfo + GetAllPinsByLabel(label string) []*dpll.PinInfo + GetCommandsForPluginPinSet(clockID uint64, pinset pinSet) []dpll.PinParentDeviceCtl +} + +type dpllPins []*dpll.PinInfo + +// DpllPins is the package-level DPLL pin accessor, replaceable for testing. +var DpllPins DPLLPins = &dpllPins{} + +func (d *dpllPins) FetchPins() error { + var pins []*dpll.PinInfo + + conn, err := dpll.Dial(nil) + if err != nil { + return fmt.Errorf("failed to dial DPLL: %v", err) + } + //nolint:errcheck + defer conn.Close() + pins, err = conn.DumpPinGet() + if err != nil { + return fmt.Errorf("failed to dump DPLL pins: %v", err) + } + *d = dpllPins(pins) + return nil +} + +func (d *dpllPins) GetByLabel(label string, clockID uint64) *dpll.PinInfo { + for _, pin := range *d { + if pin.BoardLabel == label && pin.ClockID == clockID { + return pin + } + } + return nil +} + +func (d *dpllPins) GetAllPinsByLabel(label string) []*dpll.PinInfo { + result := make([]*dpll.PinInfo, 0) + + for _, pin := range *d { + if pin.BoardLabel == label { + result = append(result, pin) + } + } + return result +} + +func (d *dpllPins) GetCommandsForPluginPinSet(clockID uint64, pinset pinSet) []dpll.PinParentDeviceCtl { + pinCommands := make([]dpll.PinParentDeviceCtl, 0) + + for label, valueStr := range pinset { + // TODO: Move label checks to a higher level function + // if label == "U.FL1" || label == "U.FL2" { + // glog.Warningf("%s can not longer be set via the pins on the plugin; values ignored", label) + // continue + // } + + valueStr = strings.TrimSpace(valueStr) + values := strings.Fields(valueStr) + if len(values) != 2 { + glog.Errorf("Failed to unpack values for pin %s of clockID %d from '%s'", label, clockID, valueStr) + continue + } + + pinInfo := d.GetByLabel(label, clockID) + if pinInfo == nil { + glog.Errorf("not found pin with label %s for clockID %d", label, clockID) + continue + } + if len(pinInfo.ParentDevice) == 0 { + glog.Errorf("Unable to configure: No parent devices for pin %s for clockID %s", label, clockID) + continue + } + + ppCtrl := PinParentControl{} + + // For now we are ignoring the second value and setting both parent devices the same. + for i, parentDev := range pinInfo.ParentDevice { + var state uint8 + var priority uint8 + var direction *uint8 + + // TODO add checks for capabilies such as direction changes. + switch values[0] { + case "0": + if parentDev.Direction == dpll.PinDirectionInput { + state = dpll.PinStateSelectable + } else { + state = dpll.PinStateDisconnected + } + priority = PriorityDisabled + case "1": + state = dpll.PinStateConnected + v := uint8(dpll.PinDirectionInput) + direction = &v + priority = PriorityEnabled + case "2": + state = dpll.PinStateConnected + v := uint8(dpll.PinDirectionOutput) + direction = &v + priority = PriorityEnabled + default: + glog.Errorf("invalid initial value in pin config for clock id %s pin %s: '%s'", clockID, label, values[0]) + continue + } + + switch i { + case eecDpllIndex: + ppCtrl.EecOutputState = state + ppCtrl.EecDirection = direction + ppCtrl.EecPriority = priority + case ppsDpllIndex: + ppCtrl.PpsOutputState = state + ppCtrl.PpsDirection = direction + ppCtrl.PpsPriority = priority + } + } + pinCommands = append(pinCommands, SetPinControlData(*pinInfo, ppCtrl)...) + } + return pinCommands +} + +func (d *dpllPins) ApplyPinCommands(commands []dpll.PinParentDeviceCtl) error { + err := BatchPinSet(&commands) + // event if there was an error we still need to refresh the pin state. + fetchErr := d.FetchPins() + return errors.Join(err, fetchErr) +} diff --git a/addons/intel/dpllPins_test.go b/addons/intel/dpllPins_test.go new file mode 100644 index 00000000..880b7cd4 --- /dev/null +++ b/addons/intel/dpllPins_test.go @@ -0,0 +1,463 @@ +package intel + +import ( + "testing" + + dpll "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" + "github.com/stretchr/testify/assert" +) + +const allPinCaps = dpll.PinCapDir | dpll.PinCapPrio | dpll.PinCapState + +func makeTwoParentPin(id uint32, label string, clockID uint64, eecDir, ppsDir uint32) *dpll.PinInfo { + return &dpll.PinInfo{ + ID: id, + BoardLabel: label, + ClockID: clockID, + Type: dpll.PinTypeEXT, + Capabilities: allPinCaps, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 100, Direction: eecDir, Prio: toPtr[uint32](0), State: dpll.PinStateDisconnected}, + {ParentID: 200, Direction: ppsDir, Prio: toPtr[uint32](0), State: dpll.PinStateDisconnected}, + }, + } +} + +func makePins(pins ...*dpll.PinInfo) dpllPins { + return dpllPins(pins) +} + +func TestGetByLabel(t *testing.T) { + pins := makePins( + makeTwoParentPin(1, "SMA1", 1000, dpll.PinDirectionInput, dpll.PinDirectionInput), + makeTwoParentPin(2, "SMA2", 1000, dpll.PinDirectionOutput, dpll.PinDirectionOutput), + makeTwoParentPin(3, "SMA1", 2000, dpll.PinDirectionInput, dpll.PinDirectionInput), + ) + + t.Run("found", func(t *testing.T) { + pin := pins.GetByLabel("SMA1", 1000) + assert.NotNil(t, pin) + assert.Equal(t, uint32(1), pin.ID) + }) + + t.Run("found_different_clockID", func(t *testing.T) { + pin := pins.GetByLabel("SMA1", 2000) + assert.NotNil(t, pin) + assert.Equal(t, uint32(3), pin.ID) + }) + + t.Run("wrong_label", func(t *testing.T) { + pin := pins.GetByLabel("GNSS_1PPS_IN", 1000) + assert.Nil(t, pin) + }) + + t.Run("wrong_clockID", func(t *testing.T) { + pin := pins.GetByLabel("SMA1", 9999) + assert.Nil(t, pin) + }) +} + +func TestGetCommandsForPluginPinSet_BadFormat(t *testing.T) { + pins := makePins( + makeTwoParentPin(1, "SMA1", 1000, dpll.PinDirectionInput, dpll.PinDirectionInput), + ) + + t.Run("single_value", func(t *testing.T) { + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": "1"}) + assert.Empty(t, cmds) + }) + + t.Run("three_values", func(t *testing.T) { + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": "1 1 1"}) + assert.Empty(t, cmds) + }) + + t.Run("empty_value", func(t *testing.T) { + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": ""}) + assert.Empty(t, cmds) + }) +} + +func TestGetCommandsForPluginPinSet_PinNotFound(t *testing.T) { + pins := makePins( + makeTwoParentPin(1, "SMA1", 1000, dpll.PinDirectionInput, dpll.PinDirectionInput), + ) + + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA2": "1 1"}) + assert.Empty(t, cmds) +} + +func TestGetCommandsForPluginPinSet_NoParentDevices(t *testing.T) { + pins := makePins(&dpll.PinInfo{ + ID: 1, + BoardLabel: "SMA1", + ClockID: 1000, + ParentDevice: []dpll.PinParentDevice{}, + }) + + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": "1 1"}) + assert.Empty(t, cmds) +} + +func TestGetCommandsForPluginPinSet_InvalidValue(t *testing.T) { + pins := makePins( + makeTwoParentPin(1, "SMA1", 1000, dpll.PinDirectionInput, dpll.PinDirectionInput), + ) + + // The default branch's `continue` only skips the inner parent-device loop, + // so SetPinControlData is still called with a zero-valued PinParentControl. + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": "9 9"}) + assert.Len(t, cmds, 1) + cmd := cmds[0] + assert.Len(t, cmd.PinParentCtl, 2) + for _, pc := range cmd.PinParentCtl { + assert.Nil(t, pc.Direction) + assert.NotNil(t, pc.Prio) + assert.Equal(t, uint32(0), *pc.Prio) + assert.NotNil(t, pc.State, "input pins always get state=selectable") + assert.Equal(t, uint32(dpll.PinStateSelectable), *pc.State) + } +} + +func toPtr[V any](v V) *V { return &v } + +func TestGetCommandsForPluginPinSet_Value0_FromConnected(t *testing.T) { + pins := makePins(&dpll.PinInfo{ + ID: 1, + BoardLabel: "SMA1", + ClockID: 1000, + Capabilities: allPinCaps, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 100, Direction: dpll.PinDirectionInput, Prio: toPtr[uint32](0), State: dpll.PinStateConnected}, + {ParentID: 200, Direction: dpll.PinDirectionInput, Prio: toPtr[uint32](0), State: dpll.PinStateConnected}, + }, + }) + + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": "0 0"}) + assert.Len(t, cmds, 1) + cmd := cmds[0] + assert.Equal(t, uint32(1), cmd.ID) + assert.Len(t, cmd.PinParentCtl, 2) + + for _, pc := range cmd.PinParentCtl { + assert.Nil(t, pc.Direction, "direction should be nil for value 0") + assert.NotNil(t, pc.Prio) + assert.Equal(t, uint32(PriorityDisabled), *pc.Prio) + assert.NotNil(t, pc.State, "input pins always get state=selectable") + assert.Equal(t, uint32(dpll.PinStateSelectable), *pc.State) + } +} + +func TestGetCommandsForPluginPinSet_Value0_FromDisconnected(t *testing.T) { + pins := makePins(&dpll.PinInfo{ + ID: 1, + BoardLabel: "SMA1", + ClockID: 1000, + Capabilities: allPinCaps, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 100, Direction: dpll.PinDirectionOutput, Prio: toPtr[uint32](0), State: dpll.PinStateDisconnected}, + {ParentID: 200, Direction: dpll.PinDirectionOutput, Prio: toPtr[uint32](0), State: dpll.PinStateDisconnected}, + }, + }) + + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": "0 0"}) + assert.Len(t, cmds, 1) + cmd := cmds[0] + assert.Len(t, cmd.PinParentCtl, 2) + + for _, pc := range cmd.PinParentCtl { + assert.Nil(t, pc.Direction, "direction should be nil for value 0") + // parentDevice.Direction is Output, so State is set + assert.NotNil(t, pc.State) + assert.Equal(t, uint32(dpll.PinStateDisconnected), *pc.State) + assert.Nil(t, pc.Prio, "prio should not be set for output direction") + } +} + +func TestGetCommandsForPluginPinSet_Value1_SetInput(t *testing.T) { + pins := makePins(&dpll.PinInfo{ + ID: 5, + BoardLabel: "SMA1", + ClockID: 1000, + Capabilities: allPinCaps, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 100, Direction: dpll.PinDirectionOutput, Prio: toPtr[uint32](0), State: dpll.PinStateDisconnected}, + {ParentID: 200, Direction: dpll.PinDirectionOutput, Prio: toPtr[uint32](0), State: dpll.PinStateDisconnected}, + }, + }) + + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": "1 1"}) + assert.Len(t, cmds, 2, "direction change should produce two commands") + + dirCmd := cmds[0] + assert.Equal(t, uint32(5), dirCmd.ID) + assert.Len(t, dirCmd.PinParentCtl, 2) + for _, pc := range dirCmd.PinParentCtl { + assert.NotNil(t, pc.Direction, "first command sets direction") + assert.Equal(t, uint32(dpll.PinDirectionInput), *pc.Direction) + assert.Nil(t, pc.Prio, "first command has no prio") + assert.Nil(t, pc.State, "first command has no state") + } + + dataCmd := cmds[1] + assert.Equal(t, uint32(5), dataCmd.ID) + assert.Len(t, dataCmd.PinParentCtl, 2) + for _, pc := range dataCmd.PinParentCtl { + assert.Nil(t, pc.Direction, "second command has no direction") + assert.NotNil(t, pc.Prio, "second command sets prio for input") + assert.Equal(t, uint32(PriorityEnabled), *pc.Prio) + assert.NotNil(t, pc.State, "input pins always get state=selectable") + assert.Equal(t, uint32(dpll.PinStateSelectable), *pc.State) + } +} + +func TestGetCommandsForPluginPinSet_Value2_SetOutput(t *testing.T) { + pins := makePins(&dpll.PinInfo{ + ID: 7, + BoardLabel: "SMA2", + ClockID: 1000, + Capabilities: allPinCaps, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 100, Direction: dpll.PinDirectionInput, Prio: toPtr[uint32](0), State: dpll.PinStateConnected}, + {ParentID: 200, Direction: dpll.PinDirectionInput, Prio: toPtr[uint32](0), State: dpll.PinStateConnected}, + }, + }) + + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA2": "2 2"}) + assert.Len(t, cmds, 2, "direction change should produce two commands") + + dirCmd := cmds[0] + assert.Equal(t, uint32(7), dirCmd.ID) + assert.Len(t, dirCmd.PinParentCtl, 2) + for _, pc := range dirCmd.PinParentCtl { + assert.NotNil(t, pc.Direction, "first command sets direction") + assert.Equal(t, uint32(dpll.PinDirectionOutput), *pc.Direction) + assert.Nil(t, pc.Prio, "first command has no prio") + assert.Nil(t, pc.State, "first command has no state") + } + + dataCmd := cmds[1] + assert.Equal(t, uint32(7), dataCmd.ID) + assert.Len(t, dataCmd.PinParentCtl, 2) + for _, pc := range dataCmd.PinParentCtl { + assert.Nil(t, pc.Direction, "second command has no direction") + assert.NotNil(t, pc.State, "second command sets state for output") + assert.Equal(t, uint32(dpll.PinStateConnected), *pc.State) + assert.Nil(t, pc.Prio, "second command has no prio for output") + } +} + +func TestGetCommandsForPluginPinSet_MultiplePins(t *testing.T) { + pins := makePins( + makeTwoParentPin(1, "SMA1", 1000, dpll.PinDirectionInput, dpll.PinDirectionInput), + makeTwoParentPin(2, "SMA2", 1000, dpll.PinDirectionOutput, dpll.PinDirectionOutput), + ) + + ps := pinSet{ + "SMA1": "1 1", + "SMA2": "2 2", + } + cmds := pins.GetCommandsForPluginPinSet(1000, ps) + assert.Len(t, cmds, 2) + + cmdByID := map[uint32]dpll.PinParentDeviceCtl{} + for _, c := range cmds { + cmdByID[c.ID] = c + } + + sma1 := cmdByID[1] + assert.Len(t, sma1.PinParentCtl, 2) + for _, pc := range sma1.PinParentCtl { + assert.Nil(t, pc.Direction, "no direction change for Input->Input") + assert.NotNil(t, pc.Prio) + assert.NotNil(t, pc.State, "input pins always get state=selectable") + assert.Equal(t, uint32(dpll.PinStateSelectable), *pc.State) + } + + sma2 := cmdByID[2] + assert.Len(t, sma2.PinParentCtl, 2) + for _, pc := range sma2.PinParentCtl { + assert.Nil(t, pc.Direction, "no direction change for Output->Output") + assert.NotNil(t, pc.State) + } +} + +func TestGetCommandsForPluginPinSet_ParentIDs(t *testing.T) { + pins := makePins(&dpll.PinInfo{ + ID: 10, + BoardLabel: "SMA1", + ClockID: 5000, + Capabilities: allPinCaps, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 42, Direction: dpll.PinDirectionInput, Prio: toPtr[uint32](0)}, + {ParentID: 99, Direction: dpll.PinDirectionInput, Prio: toPtr[uint32](0)}, + }, + }) + + cmds := pins.GetCommandsForPluginPinSet(5000, pinSet{"SMA1": "1 1"}) + assert.Len(t, cmds, 1) + assert.Equal(t, uint32(42), cmds[0].PinParentCtl[0].PinParentID) + assert.Equal(t, uint32(99), cmds[0].PinParentCtl[1].PinParentID) +} + +func TestGetCommandsForPluginPinSet_WhitespaceHandling(t *testing.T) { + pins := makePins( + makeTwoParentPin(1, "SMA1", 1000, dpll.PinDirectionInput, dpll.PinDirectionInput), + ) + + t.Run("leading_trailing_spaces", func(t *testing.T) { + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": " 1 1 "}) + assert.Len(t, cmds, 1) + }) + + t.Run("extra_internal_spaces", func(t *testing.T) { + cmds := pins.GetCommandsForPluginPinSet(1000, pinSet{"SMA1": "1 1"}) + assert.Len(t, cmds, 1) + }) +} + +func TestSetPinControlData_StateOnlyPin(t *testing.T) { + pin := dpll.PinInfo{ + ID: 15, + BoardLabel: "U.FL1", + Capabilities: dpll.PinCapState, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 0, Direction: dpll.PinDirectionOutput, State: dpll.PinStateConnected}, + {ParentID: 1, Direction: dpll.PinDirectionOutput, State: dpll.PinStateConnected}, + }, + } + + control := PinParentControl{ + EecOutputState: dpll.PinStateDisconnected, + PpsOutputState: dpll.PinStateDisconnected, + } + cmds := SetPinControlData(pin, control) + assert.Len(t, cmds, 1) + for _, pc := range cmds[0].PinParentCtl { + assert.NotNil(t, pc.State, "state-can-change should allow State") + assert.Equal(t, uint32(dpll.PinStateDisconnected), *pc.State) + assert.Nil(t, pc.Prio, "no priority-can-change capability") + assert.Nil(t, pc.Direction, "no direction-can-change capability") + } +} + +func TestSetPinControlData_StateOnlyPin_InputDirection(t *testing.T) { + pin := dpll.PinInfo{ + ID: 37, + BoardLabel: "U.FL2", + Capabilities: dpll.PinCapState, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 2, Direction: dpll.PinDirectionInput, State: dpll.PinStateDisconnected}, + {ParentID: 3, Direction: dpll.PinDirectionInput, State: dpll.PinStateDisconnected}, + }, + } + + control := PinParentControl{ + EecPriority: PriorityDisabled, + PpsPriority: PriorityDisabled, + EecOutputState: dpll.PinStateDisconnected, + PpsOutputState: dpll.PinStateDisconnected, + } + cmds := SetPinControlData(pin, control) + assert.Len(t, cmds, 1) + for _, pc := range cmds[0].PinParentCtl { + assert.Nil(t, pc.Prio, "no priority-can-change: prio must not be set") + assert.Nil(t, pc.Direction, "no direction-can-change: direction must not be set") + assert.NotNil(t, pc.State, "input pin with state-can-change gets state=selectable") + assert.Equal(t, uint32(dpll.PinStateSelectable), *pc.State) + } +} + +func TestSetPinControlData_NoCaps(t *testing.T) { + pin := dpll.PinInfo{ + ID: 99, + Capabilities: dpll.PinCapNone, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 0, Direction: dpll.PinDirectionInput}, + {ParentID: 1, Direction: dpll.PinDirectionInput}, + }, + } + + control := PinParentControl{ + EecPriority: 0, + PpsPriority: 0, + EecOutputState: dpll.PinStateConnected, + PpsOutputState: dpll.PinStateConnected, + } + cmds := SetPinControlData(pin, control) + assert.Len(t, cmds, 1) + for _, pc := range cmds[0].PinParentCtl { + assert.Nil(t, pc.Prio, "no capabilities: prio must not be set") + assert.Nil(t, pc.State, "no capabilities: state must not be set") + assert.Nil(t, pc.Direction, "no capabilities: direction must not be set") + } +} + +func TestSetPinControlData_PrioCapButNilPrio(t *testing.T) { + pin := dpll.PinInfo{ + ID: 58, + BoardLabel: "U.FL2", + Capabilities: dpll.PinCapPrio | dpll.PinCapState, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 4, Direction: dpll.PinDirectionInput, State: dpll.PinStateDisconnected}, + {ParentID: 5, Direction: dpll.PinDirectionInput, State: dpll.PinStateDisconnected}, + }, + } + + control := PinParentControl{ + EecPriority: PriorityDisabled, + PpsPriority: PriorityDisabled, + } + cmds := SetPinControlData(pin, control) + assert.Len(t, cmds, 1) + for _, pc := range cmds[0].PinParentCtl { + assert.Nil(t, pc.Prio, "kernel reports no prio on parent device: prio must not be set") + assert.NotNil(t, pc.State, "input pin with state-can-change gets state=selectable") + assert.Equal(t, uint32(dpll.PinStateSelectable), *pc.State) + } +} + +func TestBuildDirectionCmd_NoDirCap(t *testing.T) { + eecDir := uint8(dpll.PinDirectionOutput) + ppsDir := uint8(dpll.PinDirectionOutput) + pin := dpll.PinInfo{ + ID: 10, + Capabilities: dpll.PinCapPrio | dpll.PinCapState, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 0, Direction: dpll.PinDirectionInput}, + {ParentID: 1, Direction: dpll.PinDirectionInput}, + }, + } + control := PinParentControl{ + EecDirection: &eecDir, + PpsDirection: &ppsDir, + } + cmd := buildDirectionCmd(&pin, control) + assert.Nil(t, cmd, "direction change should be skipped without PinCapDir") + assert.Equal(t, uint32(dpll.PinDirectionInput), pin.ParentDevice[0].Direction, "direction should not be updated") + assert.Equal(t, uint32(dpll.PinDirectionInput), pin.ParentDevice[1].Direction, "direction should not be updated") +} + +func TestApplyPinCommands(t *testing.T) { + _, restorePins := setupMockDPLLPins( + makeTwoParentPin(1, "SMA1", 1000, dpll.PinDirectionInput, dpll.PinDirectionInput), + ) + defer restorePins() + + mockPinSet, restore := setupBatchPinSetMock() + defer restore() + + cmds := []dpll.PinParentDeviceCtl{ + { + ID: 1, + PinParentCtl: []dpll.PinControl{ + {PinParentID: 100, Prio: toPtr[uint32](0)}, + }, + }, + } + + err := DpllPins.ApplyPinCommands(cmds) + assert.NoError(t, err) + assert.NotNil(t, mockPinSet.commands) + assert.Len(t, *mockPinSet.commands, 1) +} diff --git a/addons/intel/e810.go b/addons/intel/e810.go index 210259a7..fe08a566 100644 --- a/addons/intel/e810.go +++ b/addons/intel/e810.go @@ -1,115 +1,44 @@ package intel import ( - "encoding/binary" "encoding/json" "fmt" - "os" - "os/exec" - "reflect" "strconv" "strings" "github.com/golang/glog" "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll" - dpll_netlink "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/plugin" ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" ) +var pluginNameE810 = "e810" + type E810Opts struct { - EnableDefaultConfig bool `json:"enableDefaultConfig"` - UblxCmds []E810UblxCmds `json:"ublxCmds"` - DevicePins map[string]map[string]string `json:"pins"` - DpllSettings map[string]uint64 `json:"settings"` - PhaseOffsetPins map[string]map[string]string `json:"phaseOffsetPins"` - InputDelays []InputPhaseDelays `json:"interconnections"` + PluginOpts + EnableDefaultConfig bool `json:"enableDefaultConfig"` + UblxCmds UblxCmdList `json:"ublxCmds"` + PhaseInputs []PhaseInputs `json:"interconnections"` } -type E810UblxCmds struct { - ReportOutput bool `json:"reportOutput"` - Args []string `json:"args"` -} +// GetPhaseInputs implements PhaseInputsProvider +func (o E810Opts) GetPhaseInputs() []PhaseInputs { return o.PhaseInputs } type E810PluginData struct { - hwplugins *[]string + PluginData } -// Sourced from https://github.com/RHsyseng/oot-ice/blob/main/ptp-config.sh -var EnableE810PTPConfig = ` -#!/bin/bash -set -eu - -ETH=$(grep -e 000e -e 000f /sys/class/net/*/device/subsystem_device | awk -F"/" '{print $5}') - -for DEV in $ETH; do - if [ -f /sys/class/net/$DEV/device/ptp/ptp*/pins/U.FL2 ]; then - echo 0 2 > /sys/class/net/$DEV/device/ptp/ptp*/pins/U.FL2 - echo 0 1 > /sys/class/net/$DEV/device/ptp/ptp*/pins/U.FL1 - echo 0 2 > /sys/class/net/$DEV/device/ptp/ptp*/pins/SMA2 - echo 0 1 > /sys/class/net/$DEV/device/ptp/ptp*/pins/SMA1 - fi -done - -echo "Disabled all SMA and U.FL Connections" -` - -var unitTest bool -var clockChain = &ClockChain{} - -// For mocking DPLL pin info -var DpllPins = []*dpll_netlink.PinInfo{} - -func getDefaultUblxCmds() []E810UblxCmds { - // Ublx command to output NAV-CLOCK every second - cfgMsgNavClock := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-p", "CFG-MSG,1,34,1"}, - } - // Ublx command to output NAV-STATUS every second - cfgMsgNavStatus := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-p", "CFG-MSG,1,3,1"}, - } +var ( + clockChain ClockChainInterface = &ClockChain{DpllPins: DpllPins} - // Ublx command to disable SA messages - cfgMsgDisableSA := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-p", "CFG-MSG,0xf0,0x02,0"}, + // defaultE810PinConfig -> All outputs disabled + defaultE810PinConfig = pinSet{ + "SMA1": "0 1", + "SMA2": "0 2", + "U.FL1": "0 1", + "U.FL2": "0 2", } - // Ublx command to disable SV messages - cfgMsgDisableSV := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-p", "CFG-MSG,0xf0,0x03,0"}, - } - // Ublx command to disable VTG messages - cfgMsgDisableVTG := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-z", "CFG-MSGOUT-NMEA_ID_VTG_I2C,0"}, - } - // Ublx command to disable GST messages - cfgMsgDisableGST := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-z", "CFG-MSGOUT-NMEA_ID_GST_I2C,0"}, - } - // Ublx command to disable ZDA messages - cfgMsgDisableZDA := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-z", "CFG-MSGOUT-NMEA_ID_ZDA_I2C,0"}, - } - // Ublx command to disable GBS messages - cfgMsgDisableGBS := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-z", "CFG-MSGOUT-NMEA_ID_GBS_I2C,0"}, - } - // Ublx command to save configuration to storage - cfgSave := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-p", "SAVE"}, - } - return []E810UblxCmds{cfgMsgNavClock, cfgMsgNavStatus, cfgMsgDisableSA, cfgMsgDisableSV, - cfgMsgDisableVTG, cfgMsgDisableGST, cfgMsgDisableZDA, cfgMsgDisableGBS, cfgSave} -} +) func OnPTPConfigChangeE810(data *interface{}, nodeProfile *ptpv1.PtpProfile) error { glog.Info("calling onPTPConfigChange for e810 plugin") @@ -118,92 +47,120 @@ func OnPTPConfigChangeE810(data *interface{}, nodeProfile *ptpv1.PtpProfile) err var e810Opts E810Opts var err error - var optsByteArray []byte - var stdout []byte - var pinPath string e810Opts.EnableDefaultConfig = false + err = DpllPins.FetchPins() + if err != nil { + return err + } + for name, opts := range (*nodeProfile).Plugins { - if name == "e810" { - optsByteArray, _ = json.Marshal(opts) + if name == pluginNameE810 { + optsByteArray, _ := json.Marshal(opts) err = json.Unmarshal(optsByteArray, &e810Opts) if err != nil { glog.Error("e810 failed to unmarshal opts: " + err.Error()) } - // for unit testing only, PtpSettings may include "unitTest" key. The value is - // the path where resulting configuration files will be written, instead of /var/run - _, unitTest = (*nodeProfile).PtpSettings["unitTest"] - if unitTest { - MockPins() - } - if e810Opts.EnableDefaultConfig { - stdout, _ = exec.Command("/usr/bin/bash", "-c", EnableE810PTPConfig).Output() - glog.Infof(string(stdout)) - } + allDevices := e810Opts.allDevices() + + clockIDs := make(map[string]uint64) + if (*nodeProfile).PtpSettings == nil { (*nodeProfile).PtpSettings = make(map[string]string) } - for device, pins := range e810Opts.DevicePins { - dpllClockIdStr := fmt.Sprintf("%s[%s]", dpll.ClockIdStr, device) - if !unitTest { - (*nodeProfile).PtpSettings[dpllClockIdStr] = strconv.FormatUint(getClockIdE810(device), 10) - for pin, value := range pins { - deviceDir := fmt.Sprintf("/sys/class/net/%s/device/ptp/", device) - phcs, err := os.ReadDir(deviceDir) - if err != nil { - glog.Error("e810 failed to read " + deviceDir + ": " + err.Error()) - continue - } - for _, phc := range phcs { - pinPath = fmt.Sprintf("/sys/class/net/%s/device/ptp/%s/pins/%s", device, phc.Name(), pin) - glog.Infof("echo %s > %s", value, pinPath) - err = os.WriteFile(pinPath, []byte(value), 0666) - if err != nil { - glog.Error("e810 failed to write " + value + " to " + pinPath + ": " + err.Error()) - } - } - } + + glog.Infof("Initializing e810 plugin for profile %s and devices %v", *nodeProfile.Name, allDevices) + for _, device := range allDevices { + dpllClockIDStr := fmt.Sprintf("%s[%s]", dpll.ClockIdStr, device) + clkID := getClockID(device) + if clkID == 0 { + glog.Errorf("failed to get clockID for device %s; pins for this device will not be configured", device) + } + clockIDs[device] = clkID + (*nodeProfile).PtpSettings[dpllClockIDStr] = strconv.FormatUint(clkID, 10) + } + + for device, frequencies := range e810Opts.DeviceFreqencies { + err = pinConfig.applyPinFrq(device, frequencies) + if err != nil { + glog.Errorf("e810 failed to set PHC frequencies for %s: %s", device, err) } } + // Copy DPLL Settings from plugin config to PtpSettings for k, v := range e810Opts.DpllSettings { if _, ok := (*nodeProfile).PtpSettings[k]; !ok { (*nodeProfile).PtpSettings[k] = strconv.FormatUint(v, 10) } } + + // Copy PhaseOffsetPins settings from plugin config to PtpSettings for iface, properties := range e810Opts.PhaseOffsetPins { - ifaceFound := false - for dev := range e810Opts.DevicePins { - if strings.Compare(iface, dev) == 0 { - ifaceFound = true - break - } - } - if !ifaceFound { - glog.Errorf("e810 phase offset pin filter initialization failed: interface %s not found among %v", - iface, reflect.ValueOf(e810Opts.DevicePins).MapKeys()) - break - } for pinProperty, value := range properties { - key := strings.Join([]string{iface, "phaseOffsetFilter", strconv.FormatUint(getClockIdE810(iface), 10), pinProperty}, ".") + key := strings.Join([]string{iface, "phaseOffsetFilter", strconv.FormatUint(getClockID(iface), 10), pinProperty}, ".") (*nodeProfile).PtpSettings[key] = value } } - if e810Opts.InputDelays != nil { - if unitTest { - // Mock clock chain DPLL pins in unit test - clockChain.DpllPins = DpllPins - } + + // Initialize clockChain + if e810Opts.PhaseInputs != nil { clockChain, err = InitClockChain(e810Opts, nodeProfile) if err != nil { return err } - (*nodeProfile).PtpSettings["leadingInterface"] = clockChain.LeadingNIC.Name - (*nodeProfile).PtpSettings["upstreamPort"] = clockChain.LeadingNIC.UpstreamPort + (*nodeProfile).PtpSettings["leadingInterface"] = clockChain.GetLeadingNIC().Name + (*nodeProfile).PtpSettings["upstreamPort"] = clockChain.GetLeadingNIC().UpstreamPort } else { - glog.Error("no clock chain set") + glog.Infof("No clock chain set: Restoring any previous pin state changes") + err = clockChain.SetPinDefaults() + if err != nil { + glog.Errorf("Could not restore clockChain pin defaults: %s", err) + } + clockChain = &ClockChain{DpllPins: DpllPins} + err = DpllPins.FetchPins() + if err != nil { + glog.Errorf("Could not determine the current state of the dpll pins: %s", err) + } + } + + if e810Opts.EnableDefaultConfig { + for _, device := range allDevices { + if hasSysfsSMAPins(device) { + err = pinConfig.applyPinSet(device, defaultE810PinConfig) + } else { + err = DpllPins.ApplyPinCommands(DpllPins.GetCommandsForPluginPinSet(clockIDs[device], defaultE810PinConfig)) + } + if err != nil { + glog.Errorf("e810 failed to set default Pin configuration for %s: %s", device, err) + } + } + } + + // Initialize all user-specified phc pins and frequencies + for device, pins := range e810Opts.DevicePins { + if hasSysfsSMAPins(device) { + err = pinConfig.applyPinSet(device, pins) + } else { + commands := DpllPins.GetCommandsForPluginPinSet(clockIDs[device], pins) + if pinSetHasSMAInput(pins) { + gnssPin := DpllPins.GetByLabel(gnss, clockIDs[device]) + if gnssPin != nil { + gnssCommands := SetPinControlData(*gnssPin, PinParentControl{ + EecPriority: 4, + PpsPriority: 4, + }) + commands = append(commands, gnssCommands...) + } else { + glog.Warningf("SMA input detected but GNSS-1PPS pin not found for clockID %d", clockIDs[device]) + } + } + err = DpllPins.ApplyPinCommands(commands) + } + if err != nil { + glog.Errorf("e810 failed to set Pin configuration for %s: %s", device, err) + } } } } @@ -211,17 +168,16 @@ func OnPTPConfigChangeE810(data *interface{}, nodeProfile *ptpv1.PtpProfile) err } func AfterRunPTPCommandE810(data *interface{}, nodeProfile *ptpv1.PtpProfile, command string) error { + pluginData := (*data).(*E810PluginData) glog.Info("calling AfterRunPTPCommandE810 for e810 plugin") var e810Opts E810Opts var err error - var optsByteArray []byte - var stdout []byte e810Opts.EnableDefaultConfig = false for name, opts := range (*nodeProfile).Plugins { - if name == "e810" { - optsByteArray, _ = json.Marshal(opts) + if name == pluginNameE810 { + optsByteArray, _ := json.Marshal(opts) err = json.Unmarshal(optsByteArray, &e810Opts) if err != nil { glog.Error("e810 failed to unmarshal opts: " + err.Error()) @@ -229,34 +185,28 @@ func AfterRunPTPCommandE810(data *interface{}, nodeProfile *ptpv1.PtpProfile, co switch command { case "gpspipe": glog.Infof("AfterRunPTPCommandE810 doing ublx config for command: %s", command) - for _, ublxOpt := range append(e810Opts.UblxCmds, getDefaultUblxCmds()...) { - ublxArgs := ublxOpt.Args - glog.Infof("Running /usr/bin/ubxtool with args %s", strings.Join(ublxArgs, ", ")) - stdout, err = exec.Command("/usr/local/bin/ubxtool", ublxArgs...).CombinedOutput() - //stdout, err = exec.Command("/usr/local/bin/ubxtool", "-p", "STATUS").CombinedOutput() - if data != nil && ublxOpt.ReportOutput { - _data := *data - glog.Infof("Saving status to hwconfig: %s", string(stdout)) - var pluginData *E810PluginData = _data.(*E810PluginData) - _pluginData := *pluginData - statusString := fmt.Sprintf("ublx data: %s", string(stdout)) - *_pluginData.hwplugins = append(*_pluginData.hwplugins, statusString) - } else { - glog.Infof("Not saving status to hwconfig: %s", string(stdout)) - } - } + // Execute user-supplied UblxCmds first: + pluginData.hwplugins = append(pluginData.hwplugins, e810Opts.UblxCmds.runAll()...) + // Finish with the default commands: + pluginData.hwplugins = append(pluginData.hwplugins, defaultUblxCmds().runAll()...) case "tbc-ho-exit": - _, err = clockChain.EnterNormalTBC() + err = clockChain.EnterNormalTBC() if err != nil { - return fmt.Errorf("e810: failed to exit T-BC holdover") + return fmt.Errorf("e810: failed to enter T-BC normal mode") } - glog.Info("e810: exit T-BC holdover") + glog.Info("e810: enter T-BC normal mode") case "tbc-ho-entry": - _, err = clockChain.EnterHoldoverTBC() + err = clockChain.EnterHoldoverTBC() if err != nil { return fmt.Errorf("e810: failed to enter T-BC holdover") } glog.Info("e810: enter T-BC holdover") + case "reset-to-default": + err = clockChain.SetPinDefaults() + if err != nil { + return fmt.Errorf("e810: failed to reset pins to default") + } + glog.Info("e810: reset pins to default") default: glog.Infof("AfterRunPTPCommandE810 doing nothing for command: %s", command) } @@ -265,80 +215,31 @@ func AfterRunPTPCommandE810(data *interface{}, nodeProfile *ptpv1.PtpProfile, co return nil } -func PopulateHwConfigE810(data *interface{}, hwconfigs *[]ptpv1.HwConfig) error { - //hwConfig := ptpv1.HwConfig{} - //hwConfig.DeviceID = "e810" - //*hwconfigs = append(*hwconfigs, hwConfig) - if data != nil { - _data := *data - var pluginData *E810PluginData = _data.(*E810PluginData) - _pluginData := *pluginData - if _pluginData.hwplugins != nil { - for _, _hwconfig := range *_pluginData.hwplugins { - hwConfig := ptpv1.HwConfig{} - hwConfig.DeviceID = "e810" - hwConfig.Status = _hwconfig - *hwconfigs = append(*hwconfigs, hwConfig) - } - } - } - return nil -} - func E810(name string) (*plugin.Plugin, *interface{}) { - if name != "e810" { + if name != pluginNameE810 { glog.Errorf("Plugin must be initialized as 'e810'") return nil, nil } glog.Infof("registering e810 plugin") - hwplugins := []string{} - pluginData := E810PluginData{hwplugins: &hwplugins} - _plugin := plugin.Plugin{Name: "e810", + pluginData := E810PluginData{ + PluginData: PluginData{name: pluginNameE810}, + } + _plugin := plugin.Plugin{ + Name: pluginNameE810, OnPTPConfigChange: OnPTPConfigChangeE810, AfterRunPTPCommand: AfterRunPTPCommandE810, - PopulateHwConfig: PopulateHwConfigE810, + PopulateHwConfig: pluginData.PopulateHwConfig, } var iface interface{} = &pluginData return &_plugin, &iface } -func getClockIdE810(device string) uint64 { - const ( - PCI_EXT_CAP_ID_DSN = 3 - PCI_CFG_SPACE_SIZE = 256 - PCI_EXT_CAP_NEXT_OFFSET = 2 - PCI_EXT_CAP_OFFSET_SHIFT = 4 - PCI_EXT_CAP_DATA_OFFSET = 4 - ) - b, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/device/config", device)) - if err != nil { - glog.Error(err) - return 0 - } - // Extended capability space starts right on PCI_CFG_SPACE - var offset uint16 = PCI_CFG_SPACE_SIZE - var id uint16 - for { - id = binary.LittleEndian.Uint16(b[offset:]) - if id != PCI_EXT_CAP_ID_DSN { - if id == 0 { - glog.Errorf("can't find DSN for device %s", device) - return 0 - } - offset = binary.LittleEndian.Uint16(b[offset+PCI_EXT_CAP_NEXT_OFFSET:]) >> PCI_EXT_CAP_OFFSET_SHIFT - continue +func pinSetHasSMAInput(pins pinSet) bool { + for label, value := range pins { + if (label == "SMA1" || label == "SMA2") && + strings.HasPrefix(strings.TrimSpace(value), "1") { + return true } - break - } - return binary.LittleEndian.Uint64(b[offset+PCI_EXT_CAP_DATA_OFFSET:]) -} - -func loadPins(path string) (*[]dpll_netlink.PinInfo, error) { - pins := &[]dpll_netlink.PinInfo{} - ptext, err := os.ReadFile(path) - if err != nil { - return pins, err } - err = json.Unmarshal([]byte(ptext), pins) - return pins, err + return false } diff --git a/addons/intel/e810_test.go b/addons/intel/e810_test.go new file mode 100644 index 00000000..0903bfe4 --- /dev/null +++ b/addons/intel/e810_test.go @@ -0,0 +1,502 @@ +package intel + +import ( + "errors" + "fmt" + "os" + "slices" + "testing" + + dpll "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" + ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" + "github.com/stretchr/testify/assert" +) + +func Test_E810(t *testing.T) { + p, d := E810("e810") + assert.NotNil(t, p) + assert.NotNil(t, d) + + p, d = E810("not_e810") + assert.Nil(t, p) + assert.Nil(t, d) +} + +func Test_AfterRunPTPCommandE810(t *testing.T) { + profile, err := loadProfile("./testdata/profile-tgm.yaml") + assert.NoError(t, err) + p, d := E810("e810") + data := (*d).(*E810PluginData) + + err = p.AfterRunPTPCommand(d, profile, "bad command") + assert.NoError(t, err) + + mockExec, execRestore := setupExecMock() + defer execRestore() + mockExec.setDefaults("output", nil) + err = p.AfterRunPTPCommand(d, profile, "gpspipe") + assert.NoError(t, err) + // Ensure all 9 required calls are the last 9: + requiredUblxCmds := []string{ + "CFG-MSG,1,34,1", + "CFG-MSG,1,3,1", + "CFG-MSG,0xf0,0x02,0", + "CFG-MSG,0xf0,0x03,0", + "CFG-MSGOUT-NMEA_ID_VTG_I2C,0", + "CFG-MSGOUT-NMEA_ID_GST_I2C,0", + "CFG-MSGOUT-NMEA_ID_ZDA_I2C,0", + "CFG-MSGOUT-NMEA_ID_GBS_I2C,0", + "SAVE", + } + found := make([]string, 0, len(requiredUblxCmds)) + for _, call := range mockExec.actualCalls { + for _, arg := range call.args { + if slices.Contains(requiredUblxCmds, arg) { + found = append(found, arg) + } + } + } + assert.Equal(t, requiredUblxCmds, found) + // And expect 3 of them to have produced output (as specified in the profile) + assert.Equal(t, 3, len(data.hwplugins)) +} + +func Test_initInternalDelays(t *testing.T) { + delays, err := InitInternalDelays("E810-XXVDA4T") + assert.NoError(t, err) + assert.Equal(t, "E810-XXVDA4T", delays.PartType) + assert.Len(t, delays.ExternalInputs, 3) + assert.Len(t, delays.ExternalOutputs, 3) +} + +func Test_initInternalDelays_BadPart(t *testing.T) { + _, err := InitInternalDelays("Dummy") + assert.Error(t, err) +} + +func Test_ProcessProfileTGMNew(t *testing.T) { + _, restorePins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json") + defer restorePins() + restoreDelay := setupMockDelayCompensation() + defer restoreDelay() + mockPinSet, restorePinSet := setupBatchPinSetMock() + defer restorePinSet() + profile, err := loadProfile("./testdata/profile-tgm.yaml") + assert.NoError(t, err) + p, d := E810("e810") + + mockFs, restoreFs := setupMockFS() + defer restoreFs() + mockClockIDsFromProfile(mockFs, profile) + + err = p.OnPTPConfigChange(d, profile) + assert.NoError(t, err) + assert.NotNil(t, mockPinSet.commands, "Ensure clockChain.SetPinDefaults was called") +} + +// Test that the profile with no phase inputs is processed correctly +func Test_ProcessProfileTBCNoPhaseInputs(t *testing.T) { + _, restoreDPLLPins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json") + defer restoreDPLLPins() + restoreDelay := setupMockDelayCompensation() + defer restoreDelay() + mockPinSet, restorePinSet := setupBatchPinSetMock() + defer restorePinSet() + + // Setup filesystem mock for TBC profile - EnableE810Outputs needs this + mockFS, restoreFs := setupMockFS() + defer restoreFs() + + // mockPins + mockPinConfig, restorePins := setupMockPinConfig() + defer restorePins() + + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + + // EnableE810Outputs reads the ptp directory and writes period (SMA2 is now via DPLL) + mockFS.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + mockFS.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + mockFS.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) + + profile, err := loadProfile("./testdata/profile-tbc-no-input-delays.yaml") + assert.NoError(t, err) + p, d := E810("e810") + + mockClockIDsFromProfile(mockFS, profile) + + err = p.OnPTPConfigChange(d, profile) + assert.NoError(t, err) + assert.Equal(t, 0, mockPinConfig.actualPinSetCount) + assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) + + // Verify that clockChain was initialized (SetPinDefaults is called as part of InitClockChain) + // If SetPinDefaults wasn't called, InitClockChain would have failed + assert.NotNil(t, clockChain, "clockChain should be initialized") + ccData := clockChain.(*ClockChain) + assert.Equal(t, ClockTypeTBC, ccData.Type, "clockChain should be T-BC type") + assert.NotNil(t, mockPinSet.commands, "Ensure clockChain.SetPinDefaults was called") + + // Verify all expected filesystem calls were made + mockFS.VerifyAllCalls(t) +} + +func Test_ProcessProfileTGMOld(t *testing.T) { + _, restorePins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json") + defer restorePins() + restoreDelay := setupMockDelayCompensation() + defer restoreDelay() + mockPinSet, restorePinSet := setupBatchPinSetMock() + defer restorePinSet() + profile, err := loadProfile("./testdata/profile-tgm-old.yaml") + assert.NoError(t, err) + p, d := E810("e810") + + mockFS, restoreFs := setupMockFS() + defer restoreFs() + mockClockIDsFromProfile(mockFS, profile) + + err = p.OnPTPConfigChange(d, profile) + assert.NoError(t, err) + assert.NotNil(t, mockPinSet.commands, "Ensure some pins were set") +} + +func TestEnableE810Outputs(t *testing.T) { + mockPinSet, restorePinSet := setupBatchPinSetMock() + defer restorePinSet() + + sma2Pin := dpll.PinInfo{ + ID: 10, + ClockID: 1000, + BoardLabel: "SMA2", + Type: dpll.PinTypeEXT, + Capabilities: dpll.PinCapDir | dpll.PinCapPrio | dpll.PinCapState, + ParentDevice: []dpll.PinParentDevice{ + {ParentID: 1, Direction: dpll.PinDirectionOutput}, + {ParentID: 2, Direction: dpll.PinDirectionOutput}, + }, + } + + tests := []struct { + name string + setupMock func(*MockFileSystem) + clockChain *ClockChain + expectedError string + }{ + { + name: "DPLL path - no sysfs SMA pins", + clockChain: &ClockChain{ + LeadingNIC: CardInfo{Name: "ens4f0", DpllClockID: 1000}, + DpllPins: &mockedDPLLPins{pins: dpllPins{&sma2Pin}}, + }, + setupMock: func(m *MockFileSystem) { + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + m.ExpectReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + m.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) + }, + expectedError: "", + }, + { + name: "Sysfs path - SMA pins available", + clockChain: &ClockChain{ + LeadingNIC: CardInfo{Name: "ens4f0", DpllClockID: 1000}, + DpllPins: &mockedDPLLPins{pins: dpllPins{&sma2Pin}}, + }, + setupMock: func(m *MockFileSystem) { + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + m.ExpectReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", []byte("0 1"), nil) + m.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA2", []byte{}, os.FileMode(0o666), nil) + m.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) + }, + expectedError: "", + }, + { + name: "Sysfs path - SMA2 write fails", + clockChain: &ClockChain{ + LeadingNIC: CardInfo{Name: "ens4f0", DpllClockID: 1000}, + DpllPins: &mockedDPLLPins{pins: dpllPins{&sma2Pin}}, + }, + setupMock: func(m *MockFileSystem) { + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + m.ExpectReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", []byte("0 1"), nil) + m.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA2", []byte{}, os.FileMode(0o666), errors.New("SMA2 write failed")) + m.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) + }, + expectedError: "", + }, + { + name: "ReadDir fails", + clockChain: &ClockChain{ + LeadingNIC: CardInfo{Name: "ens4f0", DpllClockID: 1000}, + DpllPins: &mockedDPLLPins{pins: dpllPins{&sma2Pin}}, + }, + setupMock: func(m *MockFileSystem) { + m.ExpectReadDir("/sys/class/net/ens4f0/device/ptp/", []os.DirEntry{}, errors.New("permission denied")) + }, + expectedError: "e810 failed to read /sys/class/net/ens4f0/device/ptp/: permission denied", + }, + { + name: "No PHC directories found", + clockChain: &ClockChain{ + LeadingNIC: CardInfo{Name: "ens4f0", DpllClockID: 1000}, + DpllPins: &mockedDPLLPins{pins: dpllPins{&sma2Pin}}, + }, + setupMock: func(m *MockFileSystem) { + m.ExpectReadDir("/sys/class/net/ens4f0/device/ptp/", []os.DirEntry{}, nil) + }, + expectedError: "e810 cards should have one PHC per NIC, but ens4f0 has 0", + }, + { + name: "Multiple PHC directories (warning case)", + clockChain: &ClockChain{ + LeadingNIC: CardInfo{Name: "ens4f0", DpllClockID: 1000}, + DpllPins: &mockedDPLLPins{pins: dpllPins{&sma2Pin}}, + }, + setupMock: func(m *MockFileSystem) { + phcEntries := []os.DirEntry{ + MockDirEntry{name: "ptp0", isDir: true}, + MockDirEntry{name: "ptp1", isDir: true}, + } + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + m.ExpectReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + m.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) + }, + expectedError: "", + }, + { + name: "Period write fails - should not return error but log", + clockChain: &ClockChain{ + LeadingNIC: CardInfo{Name: "ens4f0", DpllClockID: 1000}, + DpllPins: &mockedDPLLPins{pins: dpllPins{&sma2Pin}}, + }, + setupMock: func(m *MockFileSystem) { + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + m.ExpectReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + m.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), errors.New("period write failed")) + }, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockPinSet.reset() + DpllPins = &mockedDPLLPins{pins: dpllPins{&sma2Pin}} + + // Setup mock filesystem + mockFS, restoreFs := setupMockFS() + defer restoreFs() + tt.setupMock(mockFS) + + // Execute function + err := tt.clockChain.EnableE810Outputs() + + // Check error + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + } + + // Verify all expected calls were made + mockFS.VerifyAllCalls(t) + }) + } +} + +func Test_AfterRunPTPCommandE810ClockChain(t *testing.T) { + profile, err := loadProfile("./testdata/profile-tgm.yaml") + assert.NoError(t, err) + p, d := E810("e810") + + err = p.AfterRunPTPCommand(d, profile, "bad command") + assert.NoError(t, err) + + mClockChain := &mockClockChain{} + clockChain = mClockChain + err = p.AfterRunPTPCommand(d, profile, "reset-to-default") + assert.NoError(t, err) + mClockChain.assertCallCounts(t, 0, 0, 1) + + mClockChain.returnErr = fmt.Errorf("Fake error") + err = p.AfterRunPTPCommand(d, profile, "reset-to-default") + assert.Error(t, err) + mClockChain.assertCallCounts(t, 0, 0, 2) + + mClockChain = &mockClockChain{} + clockChain = mClockChain + err = p.AfterRunPTPCommand(d, profile, "tbc-ho-entry") + assert.NoError(t, err) + mClockChain.assertCallCounts(t, 0, 1, 0) + mClockChain.returnErr = fmt.Errorf("Fake error") + err = p.AfterRunPTPCommand(d, profile, "tbc-ho-entry") + assert.Error(t, err) + mClockChain.assertCallCounts(t, 0, 2, 0) + + mClockChain = &mockClockChain{} + clockChain = mClockChain + err = p.AfterRunPTPCommand(d, profile, "tbc-ho-exit") + assert.NoError(t, err) + mClockChain.assertCallCounts(t, 1, 0, 0) + mClockChain.returnErr = fmt.Errorf("Fake error") + err = p.AfterRunPTPCommand(d, profile, "tbc-ho-exit") + assert.Error(t, err) + mClockChain.assertCallCounts(t, 2, 0, 0) +} + +func TestPinSetHasSMAInput(t *testing.T) { + tests := []struct { + name string + pins pinSet + expected bool + }{ + { + name: "SMA1 input", + pins: pinSet{"SMA1": "1 1"}, + expected: true, + }, + { + name: "SMA2 input", + pins: pinSet{"SMA2": "1 2"}, + expected: true, + }, + { + name: "SMA1 input with leading spaces", + pins: pinSet{"SMA1": " 1 1 "}, + expected: true, + }, + { + name: "SMA1 disabled", + pins: pinSet{"SMA1": "0 1"}, + expected: false, + }, + { + name: "SMA2 output", + pins: pinSet{"SMA2": "2 2"}, + expected: false, + }, + { + name: "no SMA pins", + pins: pinSet{"U.FL1": "1 1"}, + expected: false, + }, + { + name: "empty pinset", + pins: pinSet{}, + expected: false, + }, + { + name: "both SMA disabled", + pins: pinSet{"SMA1": "0 1", "SMA2": "0 2"}, + expected: false, + }, + { + name: "SMA1 disabled but SMA2 input", + pins: pinSet{"SMA1": "0 1", "SMA2": "1 2"}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, pinSetHasSMAInput(tt.pins)) + }) + } +} + +func TestDevicePins_DPLL_SMAInput_SetsGNSSPriority(t *testing.T) { + sma1Pin := makeTwoParentPin(1, "SMA1", 1000, + dpll.PinDirectionInput, dpll.PinDirectionInput) + gnssPin := makeTwoParentPin(2, "GNSS-1PPS", 1000, + dpll.PinDirectionInput, dpll.PinDirectionInput) + pins := makePins(sma1Pin, gnssPin) + + ps := pinSet{"SMA1": "1 1"} + commands := pins.GetCommandsForPluginPinSet(1000, ps) + + if pinSetHasSMAInput(ps) { + gnssPinInfo := pins.GetByLabel("GNSS-1PPS", 1000) + if gnssPinInfo != nil { + gnssCommands := SetPinControlData(*gnssPinInfo, PinParentControl{ + EecPriority: 4, + PpsPriority: 4, + }) + commands = append(commands, gnssCommands...) + } + } + + gnssFound := false + for _, cmd := range commands { + if cmd.ID == 2 { + gnssFound = true + assert.Len(t, cmd.PinParentCtl, 2) + for _, pc := range cmd.PinParentCtl { + assert.NotNil(t, pc.Prio) + assert.Equal(t, uint32(4), *pc.Prio) + } + } + } + assert.True(t, gnssFound, "GNSS-1PPS command should be present") +} + +func TestDevicePins_DPLL_NoSMAInput_NoGNSSCommand(t *testing.T) { + sma2Pin := makeTwoParentPin(1, "SMA2", 1000, + dpll.PinDirectionOutput, dpll.PinDirectionOutput) + gnssPin := makeTwoParentPin(2, "GNSS-1PPS", 1000, + dpll.PinDirectionInput, dpll.PinDirectionInput) + pins := makePins(sma2Pin, gnssPin) + + ps := pinSet{"SMA2": "2 2"} + commands := pins.GetCommandsForPluginPinSet(1000, ps) + + if pinSetHasSMAInput(ps) { + gnssPinInfo := pins.GetByLabel("GNSS-1PPS", 1000) + if gnssPinInfo != nil { + gnssCommands := SetPinControlData(*gnssPinInfo, PinParentControl{ + EecPriority: 4, + PpsPriority: 4, + }) + commands = append(commands, gnssCommands...) + } + } + + for _, cmd := range commands { + assert.NotEqual(t, uint32(2), cmd.ID, + "GNSS-1PPS command should NOT be present when SMA is output") + } +} + +func Test_PopulateHwConfdigE810(t *testing.T) { + p, d := E810("e810") + data := (*d).(*E810PluginData) + err := p.PopulateHwConfig(d, nil) + assert.NoError(t, err) + + output := []ptpv1.HwConfig{} + err = p.PopulateHwConfig(d, &output) + assert.NoError(t, err) + assert.Equal(t, 0, len(output)) + + data.hwplugins = []string{"A", "B", "C"} + err = p.PopulateHwConfig(d, &output) + assert.NoError(t, err) + assert.Equal(t, []ptpv1.HwConfig{ + { + DeviceID: "e810", + Status: "A", + }, + { + DeviceID: "e810", + Status: "B", + }, + { + DeviceID: "e810", + Status: "C", + }, + }, + output) +} diff --git a/addons/intel/gnss_detect_test.go b/addons/intel/gnss_detect_test.go index baa5539b..8a47d68e 100644 --- a/addons/intel/gnss_detect_test.go +++ b/addons/intel/gnss_detect_test.go @@ -2,8 +2,6 @@ package intel import ( "errors" - "fmt" - "io/fs" "os" "strings" "testing" @@ -12,60 +10,6 @@ import ( "github.com/stretchr/testify/assert" ) -// MockDirEntry is a minimal os.DirEntry stub for tests. -type MockDirEntry struct { - name string -} - -func (m MockDirEntry) Name() string { return m.name } -func (m MockDirEntry) IsDir() bool { return false } -func (m MockDirEntry) Type() fs.FileMode { return 0 } -func (m MockDirEntry) Info() (os.FileInfo, error) { return nil, fs.ErrNotExist } - -type mockReadDirExpect struct { - path string - entries []os.DirEntry - err error -} - -// MockFileSystem implements FileSystemInterface with scripted ReadDir responses -// (WriteFile is not expected by gnss tests). -type MockFileSystem struct { - expect []mockReadDirExpect - calls int -} - -func (m *MockFileSystem) ExpectReadDir(path string, entries []os.DirEntry, err error) { - m.expect = append(m.expect, mockReadDirExpect{path: path, entries: entries, err: err}) -} - -func (m *MockFileSystem) WriteFile(filename string, data []byte, perm os.FileMode) error { - return fmt.Errorf("MockFileSystem: unexpected WriteFile(%s)", filename) -} - -func (m *MockFileSystem) ReadDir(dirname string) ([]os.DirEntry, error) { - if m.calls >= len(m.expect) { - return nil, errors.New("MockFileSystem: unexpected ReadDir call") - } - e := m.expect[m.calls] - m.calls++ - if e.path != dirname { - return nil, fmt.Errorf("MockFileSystem: path mismatch: got %q, want %q", dirname, e.path) - } - return e.entries, e.err -} - -func (m *MockFileSystem) VerifyAllCalls(t *testing.T) { - assert.Equal(t, len(m.expect), m.calls, "ReadDir expectations should be fully consumed") -} - -func setupMockFS() (*MockFileSystem, func()) { - prev := filesystem - mock := &MockFileSystem{} - filesystem = mock - return mock, func() { filesystem = prev } -} - func TestFindLeadingInterface(t *testing.T) { tests := []struct { name string diff --git a/addons/intel/intel_test.go b/addons/intel/intel_test.go deleted file mode 100644 index 71b3547c..00000000 --- a/addons/intel/intel_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package intel - -import ( - "os" - "testing" - - ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" - "github.com/stretchr/testify/assert" - "sigs.k8s.io/yaml" -) - -func loadProfile(path string) (*ptpv1.PtpProfile, error) { - profileData, err := os.ReadFile(path) - if err != nil { - return &ptpv1.PtpProfile{}, err - } - profile := ptpv1.PtpProfile{} - err = yaml.Unmarshal(profileData, &profile) - if err != nil { - return &ptpv1.PtpProfile{}, err - } - return &profile, nil -} - -func Test_initInternalDelays(t *testing.T) { - delays, err := InitInternalDelays("E810-XXVDA4T") - assert.NoError(t, err) - assert.Equal(t, "E810-XXVDA4T", delays.PartType) - assert.Len(t, delays.ExternalInputs, 3) - assert.Len(t, delays.ExternalOutputs, 3) -} - -func Test_initInternalDelays_BadPart(t *testing.T) { - _, err := InitInternalDelays("Dummy") - assert.Error(t, err) -} -func Test_ParseVpd(t *testing.T) { - b, err := os.ReadFile("./testdata/vpd.bin") - assert.NoError(t, err) - vpd := ParseVpd(b) - assert.Equal(t, "Intel(R) Ethernet Network Adapter E810-XXVDA4T", vpd.VendorSpecific1) - assert.Equal(t, "2422", vpd.VendorSpecific2) - assert.Equal(t, "M56954-005", vpd.PartNumber) - assert.Equal(t, "507C6F1FB174", vpd.SerialNumber) -} - -func Test_ProcessProfileTGMNew(t *testing.T) { - unitTest = true - profile, err := loadProfile("./testdata/profile-tgm.yaml") - assert.NoError(t, err) - err = OnPTPConfigChangeE810(nil, profile) - assert.NoError(t, err) -} - -func Test_ProcessProfilesTbcTtsc(t *testing.T) { - unitTest = true - for _, config := range []string{"./testdata/profile-tbc.yaml", "./testdata/profile-t-tsc.yaml"} { - // Can read test profile - profile, err := loadProfile(config) - assert.NoError(t, err) - - // Can run PTP config change handler without errors - err = OnPTPConfigChangeE810(nil, profile) - assert.NoError(t, err) - assert.Equal(t, ClockTypeTBC, clockChain.Type, "identified a wrong clock type") - assert.Equal(t, "5799633565432596414", clockChain.LeadingNIC.DpllClockId, "identified a wrong clock ID ") - assert.Equal(t, 5, len(clockChain.LeadingNIC.Pins), "wrong number of internal pins") - assert.Equal(t, "ens4f1", clockChain.LeadingNIC.UpstreamPort, "wrong upstream port") - // Test holdover entry - commands, err := clockChain.EnterHoldoverTBC() - assert.NoError(t, err) - assert.Equal(t, 3, len(*commands)) - // Test holdover exit - commands, err = clockChain.EnterNormalTBC() - assert.NoError(t, err) - assert.Equal(t, 3, len(*commands)) - // Test error cases - unitTest = false - err = writeSysFs("/sys/0/dummy", "dummy") - assert.Error(t, err) - err = clockChain.GetLiveDpllPinsInfo() - assert.Error(t, err) - _, err = clockChain.SetPinsControl([]PinControl{ - { - Label: "1", - ParentControl: PinParentControl{ - EecEnabled: false, - PpsEnabled: false, - }, - }}) - assert.Error(t, err, "1 pin not found in the leading card") - err = clockChain.EnableE810Outputs() - assert.Error(t, err, "e810 failed to write 1 0 0 0 100 to /sys/class/net/ens4f0/device/ptp/ptp*/period") - _, err = clockChain.InitPinsTBC() - assert.Error(t, err, "failed to write...") - } -} - -func Test_ProcessProfileTGMOld(t *testing.T) { - unitTest = true - profile, err := loadProfile("./testdata/profile-tgm-old.yaml") - assert.NoError(t, err) - err = OnPTPConfigChangeE810(nil, profile) - assert.NoError(t, err) -} diff --git a/addons/intel/mock.go b/addons/intel/mock.go deleted file mode 100644 index 56a9e894..00000000 --- a/addons/intel/mock.go +++ /dev/null @@ -1,12 +0,0 @@ -package intel - -func MockPins() { - pins, err := loadPins("./testdata/dpll-pins.json") - if err != nil { - panic(err) - } - // Mock DPLL pins - for _, pin := range *pins { - DpllPins = append(DpllPins, &pin) - } -} diff --git a/addons/intel/mock_test.go b/addons/intel/mock_test.go new file mode 100644 index 00000000..6afb1cb8 --- /dev/null +++ b/addons/intel/mock_test.go @@ -0,0 +1,384 @@ +package intel + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "testing" + + dpll "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink" + ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" +) + +// mockBatchPinSet is a simple mock to unit-test pin set operations +type mockBatchPinSet struct { + commands *[]dpll.PinParentDeviceCtl +} + +func (m *mockBatchPinSet) mock(commands *[]dpll.PinParentDeviceCtl) error { + if m.commands == nil { + cmds := make([]dpll.PinParentDeviceCtl, 0) + m.commands = &cmds + } + *m.commands = append(*m.commands, *commands...) + return nil +} + +func (m *mockBatchPinSet) reset() { + m.commands = nil +} + +func setupBatchPinSetMock() (*mockBatchPinSet, func()) { + originalBatchPinset := BatchPinSet + mock := &mockBatchPinSet{} + BatchPinSet = mock.mock + return mock, func() { BatchPinSet = originalBatchPinset } +} + +// MockFileSystem is a simple mock implementation of FileSystemInterface +type MockFileSystem struct { + // Expected calls and responses + readDirCalls []ReadDirCall + writeFileCalls []WriteFileCall + readFileCalls []ReadFileCall + readLinkCalls []ReadLinkCall + + currentReadDir int + currentWriteFile int + currentReadFile int + currentReadLink int + // Allowed (but not verified) calls and responses + allowedReadDir map[string]ReadDirCall + allowedWriteFile map[string]WriteFileCall + allowedReadFile map[string]ReadFileCall + allowedReadLink map[string]ReadLinkCall +} + +func setupMockFS() (*MockFileSystem, func()) { + originalFilesystem := filesystem + mock := &MockFileSystem{} + filesystem = mock + return mock, func() { filesystem = originalFilesystem } +} + +type ReadDirCall struct { + expectedPath string + returnDirs []os.DirEntry + returnError error +} + +type WriteFileCall struct { + expectedPath string + expectedData []byte + expectedPerm os.FileMode + returnError error +} + +type ReadFileCall struct { + expectedPath string + returnData []byte + returnError error +} + +type ReadLinkCall struct { + expectedPath string + returnData string + returnError error +} + +func (m *MockFileSystem) ExpectReadDir(path string, dirs []os.DirEntry, err error) { + m.readDirCalls = append(m.readDirCalls, ReadDirCall{ + expectedPath: path, + returnDirs: dirs, + returnError: err, + }) +} + +func (m *MockFileSystem) AllowReadDir(path string, dirs []os.DirEntry, err error) { + if m.allowedReadDir == nil { + m.allowedReadDir = make(map[string]ReadDirCall) + } + m.allowedReadDir[path] = ReadDirCall{ + expectedPath: path, + returnDirs: dirs, + returnError: err, + } +} + +func (m *MockFileSystem) ExpectWriteFile(path string, data []byte, perm os.FileMode, err error) { + m.writeFileCalls = append(m.writeFileCalls, WriteFileCall{ + expectedPath: path, + expectedData: data, + expectedPerm: perm, + returnError: err, + }) +} + +func (m *MockFileSystem) AllowWriteFile(path string) { + if m.allowedWriteFile == nil { + m.allowedWriteFile = make(map[string]WriteFileCall) + } + m.allowedWriteFile[path] = WriteFileCall{ + expectedPath: path, + } +} + +func (m *MockFileSystem) ExpectReadFile(path string, data []byte, err error) { + m.readFileCalls = append(m.readFileCalls, ReadFileCall{ + expectedPath: path, + returnData: data, + returnError: err, + }) +} + +func (m *MockFileSystem) AllowReadFile(path string, data []byte, err error) { + if m.allowedReadFile == nil { + m.allowedReadFile = make(map[string]ReadFileCall) + } + m.allowedReadFile[path] = ReadFileCall{ + expectedPath: path, + returnData: data, + returnError: err, + } +} + +func (m *MockFileSystem) ExpectReadLink(path string, data string, err error) { + m.readLinkCalls = append(m.readLinkCalls, ReadLinkCall{ + expectedPath: path, + returnData: data, + returnError: err, + }) +} + +func (m *MockFileSystem) AllowReadLink(path string, data string, err error) { + if m.allowedReadLink == nil { + m.allowedReadLink = make(map[string]ReadLinkCall) + } + m.allowedReadLink[path] = ReadLinkCall{ + expectedPath: path, + returnData: data, + returnError: err, + } +} + +func (m *MockFileSystem) ReadDir(dirname string) ([]os.DirEntry, error) { + if allowed, ok := m.allowedReadDir[dirname]; ok { + return allowed.returnDirs, allowed.returnError + } + if m.currentReadDir >= len(m.readDirCalls) { + return nil, fmt.Errorf("unexpected ReadDir call (%s)", dirname) + } + call := m.readDirCalls[m.currentReadDir] + m.currentReadDir++ + // Allow wildcard matching - if expectedPath is empty, accept any path + if call.expectedPath != "" && call.expectedPath != dirname { + return nil, fmt.Errorf("ReadDir called with unexpected path (%s), was expecting %s", dirname, call.expectedPath) + } + return call.returnDirs, call.returnError +} + +func (m *MockFileSystem) WriteFile(filename string, data []byte, _ os.FileMode) error { + if _, ok := m.allowedWriteFile[filename]; ok { + m.AllowReadFile(filename, data, nil) + return nil + } + if m.currentWriteFile >= len(m.writeFileCalls) { + return fmt.Errorf("unexpected WriteFile call (%s)", filename) + } + call := m.writeFileCalls[m.currentWriteFile] + m.currentWriteFile++ + if call.expectedPath != "" && call.expectedPath != filename { + return fmt.Errorf("WriteFile called with unexpected path (%s), was expecting %s", filename, call.expectedPath) + } + return call.returnError +} + +func (m *MockFileSystem) ReadFile(filename string) ([]byte, error) { + if allowed, ok := m.allowedReadFile[filename]; ok { + return allowed.returnData, allowed.returnError + } + if m.currentReadFile >= len(m.readFileCalls) { + return nil, fmt.Errorf("Unexpected ReadFile call (%s)", filename) + } + call := m.readFileCalls[m.currentReadFile] + m.currentReadFile++ + if call.expectedPath != "" && call.expectedPath != filename { + return nil, fmt.Errorf("ReadFile called with unexpected filename (%s), was expecting %s", filename, call.expectedPath) + } + return call.returnData, call.returnError +} + +func (m *MockFileSystem) ReadLink(filename string) (string, error) { + if allowed, ok := m.allowedReadLink[filename]; ok { + return allowed.returnData, allowed.returnError + } + if m.currentReadLink >= len(m.readLinkCalls) { + return "", fmt.Errorf("Unexpected ReadLink call (%s)", filename) + } + call := m.readLinkCalls[m.currentReadLink] + m.currentReadLink++ + if call.expectedPath != "" && call.expectedPath != filename { + return "", fmt.Errorf("ReadLink called with unexpected filename (%s), was expecting %s", filename, call.expectedPath) + } + return call.returnData, call.returnError +} + +func (m *MockFileSystem) VerifyAllCalls(t *testing.T) { + assert.Equal(t, len(m.readDirCalls), m.currentReadDir, "Not all expected ReadDir calls were made") + assert.Equal(t, len(m.writeFileCalls), m.currentWriteFile, "Not all expected WriteFile calls were made") + assert.Equal(t, len(m.readFileCalls), m.currentReadFile, "Not all expected ReadFile calls were made") + assert.Equal(t, len(m.readLinkCalls), m.currentReadLink, "Not all expected ReadLink calls were made") +} + +// MockDirEntry implements os.DirEntry for testing +type MockDirEntry struct { + name string + isDir bool +} + +func (m MockDirEntry) Name() string { return m.name } +func (m MockDirEntry) IsDir() bool { return m.isDir } +func (m MockDirEntry) Type() os.FileMode { return 0 } +func (m MockDirEntry) Info() (os.FileInfo, error) { return nil, nil } + +func loadProfile(path string) (*ptpv1.PtpProfile, error) { + profileData, err := os.ReadFile(path) + if err != nil { + return &ptpv1.PtpProfile{}, err + } + profile := ptpv1.PtpProfile{} + err = yaml.Unmarshal(profileData, &profile) + if err != nil { + return &ptpv1.PtpProfile{}, err + } + if profile.Name == nil { + return &profile, fmt.Errorf("Could not parse profile") + } + return &profile, nil +} + +type mockClockChain struct { + returnErr error + enterNormalTBCCount int + enterHoldoverTBCCount int + setPinDefaultsCount int +} + +func (m *mockClockChain) EnterNormalTBC() error { + m.enterNormalTBCCount++ + return m.returnErr +} + +func (m *mockClockChain) EnterHoldoverTBC() error { + m.enterHoldoverTBCCount++ + return m.returnErr +} + +func (m *mockClockChain) SetPinDefaults() error { + m.setPinDefaultsCount++ + return m.returnErr +} + +func (m *mockClockChain) GetLeadingNIC() CardInfo { + return CardInfo{} +} + +func (m *mockClockChain) assertCallCounts(t *testing.T, expectedNormalTBC, expectedHoldoverTBC, expectedSetPinDefaults int) { + assert.Equal(t, expectedNormalTBC, m.enterNormalTBCCount, "Expected enterNormalTBCCount") + assert.Equal(t, expectedHoldoverTBC, m.enterHoldoverTBCCount, "Expected enterHoldoverTBCCount") + assert.Equal(t, expectedSetPinDefaults, m.setPinDefaultsCount, "Expected setPinDefaultsCount") +} + +type mockPinConfig struct { + actualPinSetCount int + actualPinFrqCount int +} + +func (m *mockPinConfig) applyPinSet(_ string, pins pinSet) error { + m.actualPinSetCount += len(pins) + return nil +} + +func (m *mockPinConfig) applyPinFrq(_ string, frq frqSet) error { + m.actualPinFrqCount += len(frq) + return nil +} + +func setupMockPinConfig() (*mockPinConfig, func()) { + mockPins := mockPinConfig{} + origPinConfig := pinConfig + pinConfig = &mockPins + return &mockPins, func() { pinConfig = origPinConfig } +} + +func mockClockIDsFromProfile(mfs *MockFileSystem, profile *ptpv1.PtpProfile) { + for key, val := range profile.PtpSettings { + var iface string + if strings.HasPrefix(key, "clockId[") && strings.HasSuffix(key, "]") { + iface = strings.TrimSuffix(strings.TrimPrefix(key, "clockId["), "]") + id, err := strconv.ParseUint(val, 10, 64) + if err != nil { + continue + } + mfs.AllowReadFile(fmt.Sprintf("/sys/class/net/%s/device/config", iface), generatePCIDataForClockID(id), nil) + } + } +} + + +type mockedDPLLPins struct { + pins dpllPins +} + +func (m *mockedDPLLPins) FetchPins() error { return nil } + +func (m *mockedDPLLPins) GetByLabel(label string, clockID uint64) *dpll.PinInfo { + return m.pins.GetByLabel(label, clockID) +} + +func (m *mockedDPLLPins) GetAllPinsByLabel(label string) []*dpll.PinInfo { + return m.pins.GetAllPinsByLabel(label) +} + +func (m *mockedDPLLPins) GetCommandsForPluginPinSet(clockID uint64, pinset pinSet) []dpll.PinParentDeviceCtl { + return m.pins.GetCommandsForPluginPinSet(clockID, pinset) +} + +func (m *mockedDPLLPins) ApplyPinCommands(commands []dpll.PinParentDeviceCtl) error { + return BatchPinSet(&commands) +} + +func setupMockDPLLPins(pins ...*dpll.PinInfo) (*mockedDPLLPins, func()) { + orig := DpllPins + mock := &mockedDPLLPins{pins: dpllPins(pins)} + DpllPins = mock + return mock, func() { DpllPins = orig } +} + +func setupMockDPLLPinsFromJSON(path string) (*mockedDPLLPins, func()) { //nolint: unparam // it may be used for other pin files in the future it doesn't make the code overly complex + pins := []dpll.PinInfo{} + data, err := os.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("failed to read pins from %s: %v", path, err)) + } + if err = json.Unmarshal(data, &pins); err != nil { + panic(fmt.Sprintf("failed to unmarshal pins from %s: %v", path, err)) + } + ptrs := make([]*dpll.PinInfo, len(pins)) + for i := range pins { + ptrs[i] = &pins[i] + } + return setupMockDPLLPins(ptrs...) +} + +func setupMockDelayCompensation() func() { + orig := SendDelayCompensation + SendDelayCompensation = func(_ *[]delayCompensation, _ DPLLPins) error { + return nil + } + return func() { SendDelayCompensation = orig } +} diff --git a/addons/intel/phaseAdjust.go b/addons/intel/phaseAdjust.go index 4be69312..5ad7ee25 100644 --- a/addons/intel/phaseAdjust.go +++ b/addons/intel/phaseAdjust.go @@ -13,28 +13,32 @@ import ( "sigs.k8s.io/yaml" ) -type InputDelay struct { +// InputConnector is a connector on the input side of the card +type InputConnector struct { Connector string `json:"connector"` - DelayPs int `json:"delayPs"` + DelayPs int `json:"delayPs,omitempty"` // DelayVariationPs int `json:"delayVariationPs"` } -type InputPhaseDelays struct { - Id string `json:"id"` - Part string `json:"Part"` - Input *InputDelay `json:"inputPhaseDelay"` - GnssInput bool `json:"gnssInput"` - PhaseOutputConnectors []string `json:"phaseOutputConnectors"` - UpstreamPort string `json:"upstreamPort"` +// PhaseInputs is a list of phase inputs for a card +type PhaseInputs struct { + ID string `json:"id"` + Part string `json:"Part"` + Input InputConnector `json:"inputConnector"` + GnssInput bool `json:"gnssInput"` + PhaseOutputConnectors []string `json:"phaseOutputConnectors"` + UpstreamPort string `json:"upstreamPort"` } +// InternalLink is a link between pin and connector type InternalLink struct { Connector string `yaml:"connector"` Pin string `yaml:"pin"` - DelayPs int32 `yaml:"delayPs"` + DelayPs int32 `yaml:"delayPs,omitempty"` // DelayVariationPs uint32 `yaml:"delayVariationPs"` } +// InternalDelays is a list of internal delays for a card type InternalDelays struct { PartType string `yaml:"partType"` ExternalInputs []InternalLink `yaml:"externalInputs"` @@ -47,7 +51,7 @@ type delayCompensation struct { pinLabel string iface string direction string - clockId string + clockID uint64 } var hardware = map[string]string{ @@ -95,41 +99,41 @@ func InitInternalDelays(part string) (*InternalDelays, error) { return nil, fmt.Errorf("can't find delays for %s", part) } -func sendDelayCompensation(comp *[]delayCompensation, DpllPins []*dpll.PinInfo) error { +// SendDelayCompensation is a function variable for mocking in tests +var SendDelayCompensation = sendDelayCompensation + +func sendDelayCompensation(comp *[]delayCompensation, pins DPLLPins) error { glog.Info(comp) conn, err := dpll.Dial(nil) if err != nil { return fmt.Errorf("failed to dial DPLL: %v", err) } + //nolint:errcheck defer conn.Close() - for _, pin := range DpllPins { - for _, dc := range *comp { - desiredClockId, err := strconv.ParseUint(dc.clockId, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse clock id %s: %v", dc.clockId, err) - } - if desiredClockId == pin.ClockId && strings.EqualFold(pin.BoardLabel, dc.pinLabel) { - err = conn.PinPhaseAdjust(dpll.PinPhaseAdjustRequest{Id: pin.Id, PhaseAdjust: dc.DelayPs}) - if err != nil { - return fmt.Errorf("failed to send phase adjustment to %s clock id %d: %v", - pin.BoardLabel, desiredClockId, err) - } - glog.Infof("set phaseAdjust of pin %s at clock ID %x to %d ps", pin.BoardLabel, pin.ClockId, dc.DelayPs) - } else { - } + for _, dc := range *comp { + pin := pins.GetByLabel(dc.pinLabel, dc.clockID) + if pin == nil { + glog.Warningf("pin %s not found for clock ID %d; skipping phase adjustment", dc.pinLabel, dc.clockID) + continue + } + err = conn.PinPhaseAdjust(dpll.PinPhaseAdjustRequest{ID: pin.ID, PhaseAdjust: dc.DelayPs}) + if err != nil { + return fmt.Errorf("failed to send phase adjustment to %s clock id %d: %v", + pin.BoardLabel, dc.clockID, err) } + glog.Infof("set phaseAdjust of pin %s at clock ID %x to %d ps", pin.BoardLabel, pin.ClockID, dc.DelayPs) } return nil } -func addClockId(iface string, nodeProfile *ptpv1.PtpProfile) (*string, error) { - dpllClockIdStr := fmt.Sprintf("%s[%s]", "clockId", iface) - clockId, found := (*nodeProfile).PtpSettings[dpllClockIdStr] +func addClockID(iface string, nodeProfile *ptpv1.PtpProfile) (uint64, error) { + dpllClockIDStr := fmt.Sprintf("clockId[%s]", iface) + clockIDStr, found := (*nodeProfile).PtpSettings[dpllClockIDStr] if !found { - return nil, fmt.Errorf("plugin E810 error: can't find clock ID for interface %s - are all pins configured?", iface) + return 0, fmt.Errorf("plugin E810 error: can't find clock ID for interface %s - are all pins configured?", iface) } - return &clockId, nil + return strconv.ParseUint(clockIDStr, 10, 64) } func findInternalLink(links []InternalLink, connector string) *InternalLink { @@ -218,7 +222,7 @@ func parseVpdBlock(block []byte) *map[string]string { // matching to the correct internal delay profile // Currently the fingerprint is extracted from the "Vendor Information V1" // in the hardware Vital Product Data (VPD). With more cards with different -// delay profiles are avaliable, this function might need to change depending on +// delay profiles are available, this function might need to change depending on // how manufacturers expose data relevant for delay profiles in the VPD file func GetHardwareFingerprint(device string) string { b, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/device/vpd", device)) diff --git a/addons/intel/pinConfig.go b/addons/intel/pinConfig.go new file mode 100644 index 00000000..1002f14b --- /dev/null +++ b/addons/intel/pinConfig.go @@ -0,0 +1,75 @@ +package intel + +import ( + "errors" + "fmt" + + "github.com/golang/glog" +) + +type ( + pinSet map[string]string + frqSet []string +) + +type pinConfigurer interface { + applyPinSet(device string, pins pinSet) error + applyPinFrq(device string, values frqSet) error +} + +var pinConfig pinConfigurer = realPinConfig{} + +type realPinConfig struct{} + +func (r realPinConfig) applyPinSet(device string, pins pinSet) error { + deviceDir := fmt.Sprintf("/sys/class/net/%s/device/ptp/", device) + phcs, err := filesystem.ReadDir(deviceDir) + if err != nil { + return err + } + errList := []error{} + for _, phc := range phcs { + for pin, value := range pins { + pinPath := fmt.Sprintf("/sys/class/net/%s/device/ptp/%s/pins/%s", device, phc.Name(), pin) + glog.Infof("Setting \"%s\" > %s", value, pinPath) + err = filesystem.WriteFile(pinPath, []byte(value), 0o666) + if err != nil { + glog.Errorf("e810 pin write failure: %s", err) + errList = append(errList, err) + } + } + } + return errors.Join(errList...) +} + +func hasSysfsSMAPins(device string) bool { + deviceDir := fmt.Sprintf("/sys/class/net/%s/device/ptp/", device) + phcs, err := filesystem.ReadDir(deviceDir) + if err != nil || len(phcs) == 0 { + return false + } + sma1Path := fmt.Sprintf("/sys/class/net/%s/device/ptp/%s/pins/SMA1", device, phcs[0].Name()) + _, err = filesystem.ReadFile(sma1Path) + return err == nil +} + +func (r realPinConfig) applyPinFrq(device string, values frqSet) error { + deviceDir := fmt.Sprintf("/sys/class/net/%s/device/ptp/", device) + phcs, err := filesystem.ReadDir(deviceDir) + if err != nil { + return err + } + errList := []error{} + for _, phc := range phcs { + periodPath := fmt.Sprintf("/sys/class/net/%s/device/ptp/%s/period", device, phc.Name()) + for _, value := range values { + glog.Infof("Setting \"%s\" > %s", value, periodPath) + err = filesystem.WriteFile(periodPath, []byte(value), 0o666) + if err != nil { + glog.Errorf("e810 period write failure: %s", err) + errList = append(errList, err) + } + } + } + return errors.Join(errList...) +} diff --git a/addons/intel/pinConfig_test.go b/addons/intel/pinConfig_test.go new file mode 100644 index 00000000..a157d99b --- /dev/null +++ b/addons/intel/pinConfig_test.go @@ -0,0 +1,93 @@ +package intel + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_applyPinSet(t *testing.T) { + mockFS, restoreFS := setupMockFS() + defer restoreFS() + + err := pinConfig.applyPinSet("device", pinSet{}) + assert.Error(t, err) + + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + mockFS.ExpectReadDir("", phcEntries, nil) + + err = pinConfig.applyPinSet("device", pinSet{}) + assert.NoError(t, err) + assert.Equal(t, 0, mockFS.currentWriteFile) + + mockFS.ExpectReadDir("", phcEntries, nil) + err = pinConfig.applyPinSet("device", pinSet{ + "BAD": "1 0", + }) + assert.Error(t, err) + + mockFS.ExpectReadDir("", phcEntries, nil) + mockFS.ExpectWriteFile("/sys/class/net/device/device/ptp/ptp0/pins/PIN", []byte{}, 0o666, nil) + err = pinConfig.applyPinSet("device", pinSet{ + "PIN": "1 0", + }) + assert.NoError(t, err) + assert.Equal(t, 1, mockFS.currentWriteFile) +} + +func Test_hasSysfsSMAPins(t *testing.T) { + t.Run("SMA1 exists", func(t *testing.T) { + mockFS, restoreFS := setupMockFS() + defer restoreFS() + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + mockFS.ExpectReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + mockFS.ExpectReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", []byte("0 1"), nil) + assert.True(t, hasSysfsSMAPins("ens4f0")) + }) + + t.Run("SMA1 missing", func(t *testing.T) { + mockFS, restoreFS := setupMockFS() + defer restoreFS() + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + mockFS.ExpectReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) + mockFS.ExpectReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + assert.False(t, hasSysfsSMAPins("ens4f0")) + }) + + t.Run("no PHC directory", func(t *testing.T) { + mockFS, restoreFS := setupMockFS() + defer restoreFS() + mockFS.ExpectReadDir("/sys/class/net/ens4f0/device/ptp/", nil, os.ErrNotExist) + assert.False(t, hasSysfsSMAPins("ens4f0")) + }) +} + +func Test_applyPinFrq(t *testing.T) { + mockFS, restoreFS := setupMockFS() + defer restoreFS() + + err := pinConfig.applyPinFrq("device", frqSet{}) + assert.Error(t, err) + + phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}} + mockFS.ExpectReadDir("", phcEntries, nil) + + err = pinConfig.applyPinFrq("device", frqSet{}) + assert.NoError(t, err) + assert.Equal(t, 0, mockFS.currentWriteFile) + + mockFS.ExpectReadDir("", phcEntries, nil) + err = pinConfig.applyPinFrq("device", frqSet{ + "1 0 0 1 0", + }) + assert.Error(t, err) + + mockFS.ExpectReadDir("", phcEntries, nil) + mockFS.ExpectWriteFile("/sys/class/net/device/device/ptp/ptp0/period", []byte{}, 0o666, nil) + err = pinConfig.applyPinFrq("device", frqSet{ + "1 0 0 1 0", + }) + assert.NoError(t, err) + assert.Equal(t, 1, mockFS.currentWriteFile) +} diff --git a/addons/intel/testdata/profile-t-tsc.yaml b/addons/intel/testdata/profile-t-tsc.yaml index f7c0e9b1..5f571c3c 100644 --- a/addons/intel/testdata/profile-t-tsc.yaml +++ b/addons/intel/testdata/profile-t-tsc.yaml @@ -1,6 +1,5 @@ name: tsc ptpSettings: - unitTest: /tmp/test clockId[ens4f0]: 5799633565432596414 phc2sysOpts: -a -r -n 24 -N 8 -R 16 -u 0 plugins: diff --git a/addons/intel/testdata/profile-tbc-no-input-delays.yaml b/addons/intel/testdata/profile-tbc-no-input-delays.yaml new file mode 100644 index 00000000..1352ad7d --- /dev/null +++ b/addons/intel/testdata/profile-tbc-no-input-delays.yaml @@ -0,0 +1,176 @@ +name: tbc +ptpSettings: + unitTest: /tmp/test + clockId[ens4f0]: 5799633565432596414 + clockId[ens5f0]: 5799633565433967128 + clockId[ens8f0]: 5799633565432596448 +phc2sysOpts: -a -r -n 24 -N 8 -R 16 -u 0 +plugins: + e810: + enableDefaultConfig: false + interconnections: + - id: ens4f0 + part: E810-XXVDA4T + gnssInput: false + upstreamPort: ens4f1 + phaseOutputConnectors: + - SMA1 + - SMA2 + - id: ens5f0 + part: E810-XXVDA4T + inputConnector: + connector: SMA1 + - id: ens8f0 + part: E810-XXVDA4T + inputConnector: + connector: SMA1 + phaseOutputConnectors: + - U.FL1 + pins: + ens4f0: + SMA1: 2 1 + SMA2: 2 2 + U.FL1: 0 1 + U.FL2: 0 2 + ens5f0: + SMA1: 1 1 + SMA2: 0 2 + U.FL1: 0 1 + U.FL2: 0 2 + ens8f0: + SMA1: 1 1 + SMA2: 0 2 + U.FL1: 0 1 + U.FL2: 0 2 + settings: + LocalHoldoverTimeout: 14400 + LocalMaxHoldoverOffSet: 1500 + MaxInSpecOffset: 1500 +ptp4lConf: | + [ens4f1] + masterOnly 0 + [ens7f0] + masterOnly 1 + [ens7f1] + masterOnly 1 + [global] + # + # Default Data Set + # + twoStepFlag 1 + priority1 128 + priority2 128 + domainNumber 24 + #utc_offset 37 + clockClass 6 + clockAccuracy 0x27 + offsetScaledLogVariance 0xFFFF + free_running 0 + freq_est_interval 1 + dscp_event 0 + dscp_general 0 + dataset_comparison G.8275.x + G.8275.defaultDS.localPriority 128 + # + # Port Data Set + # + logAnnounceInterval -3 + logSyncInterval -4 + logMinDelayReqInterval -4 + logMinPdelayReqInterval 0 + announceReceiptTimeout 3 + syncReceiptTimeout 0 + delayAsymmetry 0 + fault_reset_interval 4 + neighborPropDelayThresh 20000000 + masterOnly 0 + G.8275.portDS.localPriority 128 + # + # Run time options + # + assume_two_step 0 + logging_level 6 + path_trace_enabled 0 + follow_up_info 0 + hybrid_e2e 0 + inhibit_multicast_service 0 + net_sync_monitor 0 + tc_spanning_tree 0 + tx_timestamp_timeout 50 + unicast_listen 0 + unicast_master_table 0 + unicast_req_duration 3600 + use_syslog 1 + verbose 0 + summary_interval -4 + kernel_leap 1 + check_fup_sync 0 + # + # Servo Options + # + pi_proportional_const 0.0 + pi_integral_const 0.0 + pi_proportional_scale 0.0 + pi_proportional_exponent -0.3 + pi_proportional_norm_max 0.7 + pi_integral_scale 0.0 + pi_integral_exponent 0.4 + pi_integral_norm_max 0.3 + step_threshold 0.0 + first_step_threshold 0.00002 + clock_servo pi + sanity_freq_limit 200000000 + ntpshm_segment 0 + # + # Transport options + # + transportSpecific 0x0 + ptp_dst_mac 01:1B:19:00:00:00 + p2p_dst_mac 01:80:C2:00:00:0E + udp_ttl 1 + udp6_scope 0x0E + uds_address /var/run/ptp4l + # + # Default interface options + # + clock_type BC + network_transport L2 + delay_mechanism E2E + time_stamping hardware + tsproc_mode filter + delay_filter moving_median + delay_filter_length 10 + egressLatency 0 + ingressLatency 0 + boundary_clock_jbod 0 + # + # Clock description + # + productDescription ;; + revisionData ;; + manufacturerIdentity 00:00:00 + userDescription ; + timeSource 0x20 +ptp4lOpts: -2 --summary_interval -4 +ptpSchedulingPolicy: SCHED_FIFO +ptpSchedulingPriority: 10 +ts2phcConf: | + [global] + use_syslog 0 + verbose 1 + logging_level 7 + ts2phc.pulsewidth 100000000 + leapfile /usr/share/zoneinfo/leap-seconds.list + [ens7f0] + ts2phc.extts_polarity rising + ts2phc.extts_correction 0 + ts2phc.master 0 + [ens4f0] + ts2phc.extts_polarity rising + ts2phc.extts_correction 0 + ts2phc.master 0 + [ens2f0] + ts2phc.extts_polarity rising + ts2phc.extts_correction 0 + ts2phc.master 0 +ts2phcOpts: '-s generic -a --ts2phc.external_pps 1' diff --git a/addons/intel/testdata/profile-tbc.yaml b/addons/intel/testdata/profile-tbc.yaml index 51cfb5d6..93bb96d5 100644 --- a/addons/intel/testdata/profile-tbc.yaml +++ b/addons/intel/testdata/profile-tbc.yaml @@ -1,6 +1,5 @@ name: tbc ptpSettings: - unitTest: /tmp/test clockId[ens4f0]: 5799633565432596414 clockId[ens5f0]: 5799633565433967128 clockId[ens8f0]: 5799633565432596448 @@ -18,12 +17,12 @@ plugins: - SMA2 - id: ens5f0 part: E810-XXVDA4T - inputPhaseDelay: + inputConnector: connector: SMA1 delayPs: 920 - id: ens8f0 part: E810-XXVDA4T - inputPhaseDelay: + inputConnector: connector: SMA1 delayPs: 920 phaseOutputConnectors: diff --git a/addons/intel/testdata/profile-tgm-old.yaml b/addons/intel/testdata/profile-tgm-old.yaml index 7714d361..457e9bb9 100644 --- a/addons/intel/testdata/profile-tgm-old.yaml +++ b/addons/intel/testdata/profile-tgm-old.yaml @@ -1,6 +1,5 @@ name: grandmaster ptpSettings: - unitTest: /tmp/test phc2sysOpts: -a -r -n 24 -N 8 -R 16 -u 0 plugins: e810: diff --git a/addons/intel/testdata/profile-tgm.yaml b/addons/intel/testdata/profile-tgm.yaml index 83e9b551..f16b1acf 100644 --- a/addons/intel/testdata/profile-tgm.yaml +++ b/addons/intel/testdata/profile-tgm.yaml @@ -1,6 +1,5 @@ name: grandmaster ptpSettings: - unitTest: /tmp/test clockId[ens4f0]: 5799633565432596414 clockId[ens5f0]: 5799633565433967128 clockId[ens8f0]: 5799633565432596448 @@ -17,12 +16,12 @@ plugins: - SMA2 - id: ens5f0 part: E810-XXVDA4T - inputPhaseDelay: + inputConnector: connector: SMA1 delayPs: 920 - id: ens8f0 part: E810-XXVDA4T - inputPhaseDelay: + inputConnector: connector: SMA1 delayPs: 920 phaseOutputConnectors: diff --git a/addons/intel/ublx.go b/addons/intel/ublx.go new file mode 100644 index 00000000..a436aa4b --- /dev/null +++ b/addons/intel/ublx.go @@ -0,0 +1,116 @@ +package intel + +import ( + "fmt" + "os/exec" + "slices" + "strings" + + "github.com/golang/glog" +) + +// UblxCmdList is a list of UblxCmd items +type UblxCmdList []UblxCmd + +// UblxCmd represents a single ublox command +type UblxCmd struct { + ReportOutput bool `json:"reportOutput"` + Args []string `json:"args"` +} + +// A named anonymous function for ease of mocking +var execCombined = func(name string, arg ...string) ([]byte, error) { + return exec.Command(name, arg...).CombinedOutput() +} + +var ( + // Ublx command to output NAV-CLOCK every second + cfgMsgEnableNavClock = UblxCmd{ + ReportOutput: false, + Args: []string{"-p", "CFG-MSG,1,34,1"}, + } + // Ublx command to output NAV-STATUS every second + cfgMsgEnableNavStatus = UblxCmd{ + ReportOutput: false, + Args: []string{"-p", "CFG-MSG,1,3,1"}, + } + // Ublx command to disable SA messages + cfgMsgDisableSA = UblxCmd{ + ReportOutput: false, + Args: []string{"-p", "CFG-MSG,0xf0,0x02,0"}, + } + // Ublx command to disable SV messages + cfgMsgDisableSV = UblxCmd{ + ReportOutput: false, + Args: []string{"-p", "CFG-MSG,0xf0,0x03,0"}, + } + // Ublx command to save configuration to storage + cfgSave = UblxCmd{ + ReportOutput: false, + Args: []string{"-p", "SAVE"}, + } + + // All possibel NMEA bus types (according to ubxtool) + nmeaBusTypes = []string{ + "I2C", "UART1", "UART2", "USB", "SPI", + } + + // The specific NMEA messages we disable by default + nmeaDisableMsg = []string{ + "VTG", "GST", "ZDA", "GBS", + } +) + +// Generates a series of UblxCmds which disable the given message type on all bus types +func cmdDisableNmeaMsg(msg string) UblxCmdList { + result := make(UblxCmdList, len(nmeaBusTypes)) + for i, bus := range nmeaBusTypes { + result[i] = UblxCmd{Args: []string{"-z", fmt.Sprintf("CFG-MSGOUT-NMEA_ID_%s_%s,0", msg, bus)}} + } + return result +} + +func defaultUblxCmds() UblxCmdList { + cmds := UblxCmdList{ + cfgMsgEnableNavClock, cfgMsgEnableNavStatus, cfgMsgDisableSA, cfgMsgDisableSV, + } + for _, msg := range nmeaDisableMsg { + cmds = append(cmds, cmdDisableNmeaMsg(msg)...) + } + cmds = append(cmds, cfgSave) + return cmds +} + +// run a single UblxCmd and return the result +func (cmd UblxCmd) run() (string, error) { + args := cmd.Args + if !slices.Contains(cmd.Args, "-w") { + // Default wait time is 2s per command; prefer 0.1s + args = append([]string{"-w", "0.1"}, args...) + } + glog.Infof("Running ubxtool with: %s", strings.Join(args, ", ")) + stdout, err := execCombined("/usr/local/bin/ubxtool", args...) + return string(stdout), err +} + +// run a set of UblxCmds, returning the output of any that have 'ReportOutput' set. +func (cmdList UblxCmdList) runAll() []string { + results := []string{} + for _, cmd := range cmdList { + result, err := cmd.run() + if err != nil { + glog.Warningf("ubxtool error: %s", err) + } + if cmd.ReportOutput { + if err != nil { + results = append(results, err.Error()) + } else { + glog.Infof("Saving status to hwconfig: %s", result) + results = append(results, result) + } + } else if err != nil { + glog.Infof("Not saving status to hwconfig: %s", result) + } + } + return results +} diff --git a/addons/intel/ublx_test.go b/addons/intel/ublx_test.go new file mode 100644 index 00000000..0f32a1e1 --- /dev/null +++ b/addons/intel/ublx_test.go @@ -0,0 +1,134 @@ +package intel + +import ( + "errors" + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +type mockExecutor struct { + actualCalls []actualCall + expectedCalls []expectedCall + defaultResult execResult +} + +type actualCall struct { + name string + args []string +} + +type expectedCall struct { + args []string + returnData []byte + returnErr error +} + +type execResult struct { + data []byte + err error +} + +func (m *mockExecutor) run(name string, arg ...string) ([]byte, error) { + m.actualCalls = append(m.actualCalls, actualCall{name, arg}) + for _, expected := range m.expectedCalls { + if slices.Equal(expected.args, arg) { + return expected.returnData, expected.returnErr + } + } + return m.defaultResult.data, m.defaultResult.err +} + +func (m *mockExecutor) setDefaults(data string, err error) { + m.defaultResult.data = []byte(data) + m.defaultResult.err = err +} + +func (m *mockExecutor) expect(args []string, data string, err error) { + m.expectedCalls = append(m.expectedCalls, expectedCall{ + args: args, + returnData: []byte(data), + returnErr: err, + }) +} + +// setupExecMock sets up a mock executor (returns the executor and the restoration function) +func setupExecMock() (*mockExecutor, func()) { + originalExec := execCombined + execMockData := &mockExecutor{} + execCombined = execMockData.run + return execMockData, func() { execCombined = originalExec } +} + +func Test_UbxCmdRun(t *testing.T) { + execMock, execRestore := setupExecMock() + defer execRestore() + execMock.setDefaults("result", nil) + + cmd := UblxCmd{Args: []string{"arg"}} + stdout, err := cmd.run() + assert.NoError(t, err) + assert.Equal(t, "result", stdout) + assert.Equal(t, 1, len(execMock.actualCalls)) + assert.Equal(t, []string{"-w", "0.1", "arg"}, execMock.actualCalls[0].args) + + execMock.setDefaults("", errors.New("Error")) + _, err = cmd.run() + assert.Error(t, err) + assert.Equal(t, 2, len(execMock.actualCalls)) +} + +func Test_UbxCmdListRunAll(t *testing.T) { + execMock, execRestore := setupExecMock() + defer execRestore() + execMock.setDefaults("", errors.New("Unexpected call")) + execMock.expect([]string{"-w", "0.1", "arg1"}, "result1", nil) + execMock.expect([]string{"-w", "2", "arg2"}, "result2", nil) + execMock.expect([]string{"-w", "0.1", "arg3"}, "", errors.New("error3")) + execMock.expect([]string{"-w", "0.1", "arg4"}, "", errors.New("error4")) + + cmdList := UblxCmdList{ + UblxCmd{ + ReportOutput: false, + Args: []string{"arg1"}, + }, + UblxCmd{ + ReportOutput: true, + Args: []string{"-w", "2", "arg2"}, + }, + UblxCmd{ + ReportOutput: false, + Args: []string{"arg3"}, + }, + UblxCmd{ + ReportOutput: true, + Args: []string{"arg4"}, + }, + } + + results := cmdList.runAll() + assert.Equal(t, []string{"result2", "error4"}, results) +} + +func Test_CmdDisableNmeaMsg(t *testing.T) { + expected := []string{ + "CFG-MSGOUT-NMEA_ID_FOO_I2C,0", + "CFG-MSGOUT-NMEA_ID_FOO_UART1,0", + "CFG-MSGOUT-NMEA_ID_FOO_UART2,0", + "CFG-MSGOUT-NMEA_ID_FOO_USB,0", + "CFG-MSGOUT-NMEA_ID_FOO_SPI,0", + } + found := make([]string, 0, len(expected)) + cmds := cmdDisableNmeaMsg("FOO") + assert.Equal(t, len(expected), len(cmds)) + for _, cmd := range cmds { + assert.Equal(t, "-z", cmd.Args[0]) + if slices.Contains(expected, cmd.Args[1]) { + found = append(found, cmd.Args[1]) + } + } + slices.Sort(expected) + slices.Sort(found) + assert.Equal(t, expected, found) +} diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index b15eebb1..46cfa17f 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -779,12 +779,30 @@ func (dn *Daemon) applyNodePtpProfile(runID int, nodeProfile *ptpv1.PtpProfile) } else { eventSource = []event.EventSource{event.GNSS} } - // pass array of ifaces which has source + clockId - - // here we have multiple dpll objects identified by clock id - // depends on will be either PPS or GNSS, - // ONLY the one with GNSS dependency will go to HOLDOVER - dpllDaemon := dpll.NewDpll(clockId, localMaxHoldoverOffSet, localHoldoverTimeout, - maxInSpecOffset, iface.Name, eventSource, dpll.NONE, dn.GetPhaseOffsetPinFilter(nodeProfile)) + // pass array of ifaces which has source + clockId - + // here we have multiple dpll objects identified by clock id + // depends on will be either PPS or GNSS, + // ONLY the one with GNSS dependency will go to HOLDOVER + var inSyncConditionTh uint64 = dpll.MaxInSpecOffset + var inSyncConditionTimes uint64 = 1 + sInSyncConditionTh, found1 := (*nodeProfile).PtpSettings["inSyncConditionThreshold"] + if found1 { + inSyncConditionTh, err = strconv.ParseUint(sInSyncConditionTh, 0, 64) + if err != nil { + return fmt.Errorf("failed to parse inSyncConditionThreshold: %s", err) + } + } + sInSyncConditionTim, found2 := (*nodeProfile).PtpSettings["inSyncConditionTimes"] + if found2 { + inSyncConditionTimes, err = strconv.ParseUint(sInSyncConditionTim, 0, 64) + if err != nil { + return fmt.Errorf("failed to parse inSyncConditionTimes: %s", err) + } + } + dpllDaemon := dpll.NewDpll(clockId, localMaxHoldoverOffSet, localHoldoverTimeout, + maxInSpecOffset, iface.Name, eventSource, dpll.NONE, dn.GetPhaseOffsetPinFilter(nodeProfile), + // Used only in T-BC in-sync condition: + inSyncConditionTh, inSyncConditionTimes, 0) glog.Infof("depending on %s", dpllDaemon.DependsOn()) dpllDaemon.CmdInit() dprocess.depProcess = append(dprocess.depProcess, dpllDaemon) diff --git a/pkg/daemon/testdata/profile-tbc.yaml b/pkg/daemon/testdata/profile-tbc.yaml index 092a0cc5..18792770 100644 --- a/pkg/daemon/testdata/profile-tbc.yaml +++ b/pkg/daemon/testdata/profile-tbc.yaml @@ -1,6 +1,5 @@ name: tbc ptpSettings: - unitTest: /tmp/test clockId[ens4f0]: 5799633565432596414 clockId[ens5f0]: 5799633565433967128 clockId[ens8f0]: 5799633565432596448 diff --git a/pkg/dpll-netlink/dpll-uapi.go b/pkg/dpll-netlink/dpll-uapi.go index 2bc6aa24..e568400f 100644 --- a/pkg/dpll-netlink/dpll-uapi.go +++ b/pkg/dpll-netlink/dpll-uapi.go @@ -1,4 +1,4 @@ -// Port definitions from /include/uapi/linux/dpll.h and +// Definitions from /include/uapi/linux/dpll.h and // tools/net/ynl/generated/dpll-user.h package dpll_netlink @@ -6,84 +6,151 @@ package dpll_netlink import ( "encoding/json" "fmt" + "strings" + "time" ) -const DPLL_MCGRP_MONITOR = "monitor" -const DPLL_PHASE_OFFSET_DIVIDER = 1000 -const DPLL_TEMP_DIVIDER = 1000 +// DpllMCGRPMonitor defines DPLL subsystem multicast group name +const DpllMCGRPMonitor = "monitor" + +// DpllPhaseOffsetDivider phase offset divider allows userspace to calculate a value of +// measured signal phase difference between a pin and dpll device +// as a fractional value with three digit decimal precision. +// Value of (DPLL_A_PHASE_OFFSET / DPLL_PHASE_OFFSET_DIVIDER) is an +// integer part of a measured phase offset value. +// Value of (DPLL_A_PHASE_OFFSET % DPLL_PHASE_OFFSET_DIVIDER) is a +// fractional part of a measured phase offset value. +const DpllPhaseOffsetDivider = 1000 + +// DpllTemperatureDivider allows userspace to calculate the +// temperature as float with three digit decimal precision. +// Value of (DPLL_A_TEMP / DPLL_TEMP_DIVIDER) is integer part of +// temperature value. +// Value of (DPLL_A_TEMP % DPLL_TEMP_DIVIDER) is fractional part of +// temperature value. +const DpllTemperatureDivider = 1000 + +// DpllAttributes provides the dpll_a attribute-set const ( - DPLL_A_TYPES = iota - DPLL_A_ID - DPLL_A_MODULE_NAME - DPLL_A_PAD - DPLL_A_CLOCK_ID - DPLL_A_MODE - DPLL_A_MODE_SUPPORTED - DPLL_A_LOCK_STATUS - DPLL_A_TEMP - DPLL_A_TYPE + DpllAttributes = iota + DpllID + DpllModuleName + DpllAttPadding + DpllClockID + DpllMode + DpllModeSupported + DpllLockStatus + DpllTemp + DpllType + DpllLockStatusError + DpllClockQualityLevel +) - __DPLL_A_MAX - DPLL_A_MAX = __DPLL_A_MAX - 1 +// DpllPinTypes defines the attribute-set for dpll_a_pin +const ( + // attribute-set dpll_a_pin + DpllPinTypes = iota + DpllPinID + DpllPinParentID + DpllPinModuleName + DpllPinPadding + DpllPinClockID + DpllPinBoardLabel + DpllPinPanelLabel + DpllPinPackageLabel + DpllPinType + DpllPinDirection + DpllPinFrequency + DpllPinFrequencySupported + DpllPinFrequencyMin + DpllPinFrequencyMax + DpllPinPrio + DpllPinState + DpllPinCapabilities + DpllPinParentDevice + DpllPinParentPin + DpllPinPhaseAdjustMin + DpllPinPhaseAdjustMax + DpllPinPhaseAdjust + DpllPinPhaseOffset + DpllPinFractionalFrequencyOffset + DpllPinEsyncFrequency + DpllPinEsyncFrequencySupported + DpllPinEsyncPulse ) +// DpllCmds defines DPLL subsystem commands encoding const ( - DPLL_A_PIN_TYPES = iota + DpllCmds = iota + DpllCmdDeviceIDGet + DpllCmdDeviceGet + DpllCmdDeviceSet + DpllCmdDeviceCreateNtf + DpllCmdDeviceDeleteNtf + DpllCmdDeviceChangeNtf + DpllCmdPinIDGet + DpllCmdPinGet + DpllCmdPinSet + DpllCmdPinCreateNtf + DpllCmdPinDeleteNtf + DpllCmdPinChangeNtf +) - DPLL_A_PIN_ID - DPLL_A_PIN_PARENT_ID - DPLL_A_PIN_MODULE_NAME - DPLL_A_PIN_PAD - DPLL_A_PIN_CLOCK_ID - DPLL_A_PIN_BOARD_LABEL - DPLL_A_PIN_PANEL_LABEL - DPLL_A_PIN_PACKAGE_LABEL - DPLL_A_PIN_TYPE - DPLL_A_PIN_DIRECTION - DPLL_A_PIN_FREQUENCY - DPLL_A_PIN_FREQUENCY_SUPPORTED - DPLL_A_PIN_FREQUENCY_MIN - DPLL_A_PIN_FREQUENCY_MAX - DPLL_A_PIN_PRIO - DPLL_A_PIN_STATE - DPLL_A_PIN_CAPABILITIES - DPLL_A_PIN_PARENT_DEVICE - DPLL_A_PIN_PARENT_PIN - DPLL_A_PIN_PHASE_ADJUST_MIN - DPLL_A_PIN_PHASE_ADJUST_MAX - DPLL_A_PIN_PHASE_ADJUST - DPLL_A_PIN_PHASE_OFFSET - DPLL_A_PIN_FRACTIONAL_FREQUENCY_OFFSET +// DpllLockStatusAttribute defines DPLL lock status encoding +const ( + DpllLockStatusAttribute = iota + DpllLockStatusUnlocked + DpllLockStatusLocked + DpllLockStatusLockedHoldoverAcquired + DpllLockStatusHoldover +) - __DPLL_A_PIN_MAX - DPLL_A_PIN_MAX = __DPLL_A_PIN_MAX - 1 +// LockStatusErrorTypes defines device lock error types +const ( + LockStatusErrorTypes = iota + LockStatusErrorNone + LockStatusErrorUndefined + // LockStatusErrorMediaDown indicates dpll device lock status was changed because of associated + // media got down. + // This may happen for example if dpll device was previously + // locked on an input pin of type PIN_TYPE_SYNCE_ETH_PORT. + LockStatusErrorMediaDown + // LockStatusFFOTooHigh indicates the FFO (Fractional Frequency Offset) between the RX and TX + // symbol rate on the media got too high. + // This may happen for example if dpll device was previously + // locked on an input pin of type PIN_TYPE_SYNCE_ETH_PORT. + LockStatusFFOTooHigh ) + +// ClockQualityLevel defines possible clock quality levels when on holdover const ( - DPLL_CMDS = iota - DPLL_CMD_DEVICE_ID_GET - DPLL_CMD_DEVICE_GET - DPLL_CMD_DEVICE_SET - DPLL_CMD_DEVICE_CREATE_NTF - DPLL_CMD_DEVICE_DELETE_NTF - DPLL_CMD_DEVICE_CHANGE_NTF - DPLL_CMD_PIN_ID_GET - DPLL_CMD_PIN_GET - DPLL_CMD_PIN_SET - DPLL_CMD_PIN_CREATE_NTF - DPLL_CMD_PIN_DELETE_NTF - DPLL_CMD_PIN_CHANGE_NTF + ClockQualityLevel = iota + ClockQualityLevelITUOpt1PRC + ClockQualityLevelITUOpt1SSUA + ClockQualityLevelITUOpt1SSUB + ClockQualityLevelITUOpt1EEC1 + ClockQualityLevelITUOpt1PRTC + ClockQualityLevelITUOpt1EPRTC + ClockQualityLevelITUOpt1EEEC + ClockQualityLevelItuOpt1EPRC +) - __DPLL_CMD_MAX - DPLL_CMD_MAX = (__DPLL_CMD_MAX - 1) +// DpllTypeAttribute defines DPLL types +const ( + DpllTypeAttribute = iota + // DpllTypePPS indicates dpll produces Pulse-Per-Second signal + DpllTypePPS + // DpllTypeEEC indicates dpll drives the Ethernet Equipment Clock + DpllTypeEEC ) // GetLockStatus returns DPLL lock status as a string func GetLockStatus(ls uint32) string { lockStatusMap := map[uint32]string{ - 1: "unlocked", - 2: "locked", - 3: "locked-ho-acquired", - 4: "holdover", + DpllLockStatusUnlocked: "unlocked", + DpllLockStatusLocked: "locked", + DpllLockStatusLockedHoldoverAcquired: "locked-ho-acquired", + DpllLockStatusHoldover: "holdover", } status, found := lockStatusMap[ls] if found { @@ -95,8 +162,8 @@ func GetLockStatus(ls uint32) string { // GetDpllType returns DPLL type as a string func GetDpllType(tp uint32) string { typeMap := map[int]string{ - 1: "pps", - 2: "eec", + DpllTypePPS: "pps", + DpllTypeEEC: "eec", } typ, found := typeMap[int(tp)] if found { @@ -110,8 +177,6 @@ func GetMode(md uint32) string { modeMap := map[int]string{ 1: "manual", 2: "automatic", - 3: "holdover", - 4: "freerun", } mode, found := modeMap[int(md)] if found { @@ -122,31 +187,43 @@ func GetMode(md uint32) string { // DpllStatusHR represents human-readable DPLL status type DpllStatusHR struct { - Id uint32 - ModuleName string - Mode string - ModeSupported string - LockStatus string - ClockId string - Type string + Timestamp time.Time `json:"timestamp"` + ID uint32 `json:"id"` + ModuleName string `json:"moduleName"` + Mode string `json:"mode"` + ModeSupported string `json:"modeSupported"` + LockStatus string `json:"lockStatus"` + ClockID string `json:"clockId"` + Type string `json:"type"` + Temp float64 `json:"temp"` } // GetDpllStatusHR returns human-readable DPLL status -func GetDpllStatusHR(reply *DoDeviceGetReply) DpllStatusHR { - return DpllStatusHR{ - Id: reply.Id, - ModuleName: reply.ModuleName, - Mode: GetMode(reply.Mode), - LockStatus: GetLockStatus(reply.LockStatus), - ClockId: fmt.Sprintf("0x%x", reply.ClockId), - Type: GetDpllType(reply.Type), +func GetDpllStatusHR(reply *DoDeviceGetReply, timestamp time.Time) ([]byte, error) { + var modes []string + for _, md := range reply.ModeSupported { + modes = append(modes, GetMode(md)) + } + hr := DpllStatusHR{ + Timestamp: timestamp, + ID: reply.ID, + ModuleName: reply.ModuleName, + Mode: GetMode(reply.Mode), + ModeSupported: fmt.Sprint(strings.Join(modes[:], ",")), + LockStatus: GetLockStatus(reply.LockStatus), + ClockID: fmt.Sprintf("0x%x", reply.ClockID), + Type: GetDpllType(reply.Type), + Temp: float64(reply.Temp) / DpllTemperatureDivider, } + return json.Marshal(hr) } -// DoPinGetReply is used with the DoPinGet method. -type DoPinGetReplyHR struct { - Id uint32 `json:"id"` - ClockId uint64 `json:"clockId"` +// PinInfoHR is used with the DoPinGet method. +type PinInfoHR struct { + Timestamp time.Time `json:"timestamp"` + ID uint32 `json:"id"` + ModuleName string `json:"moduleName"` + ClockID string `json:"clockId"` BoardLabel string `json:"boardLabel"` PanelLabel string `json:"panelLabel"` PackageLabel string `json:"packageLabel"` @@ -160,46 +237,39 @@ type DoPinGetReplyHR struct { PhaseAdjustMax int32 `json:"phaseAdjustMax"` PhaseAdjust int32 `json:"phaseAdjust"` FractionalFrequencyOffset int `json:"fractionalFrequencyOffset"` - ModuleName string `json:"moduleName"` + EsyncFrequency int64 `json:"esyncFrequency"` + EsyncFrequencySupported []FrequencyRange `json:"esyncFrequencySupported"` + EsyncPulse int64 `json:"esyncPulse"` } -// PinParentDevice contains nested netlink attributes. +// PinParentDeviceHR contains nested netlink attributes. type PinParentDeviceHR struct { - ParentId uint32 `json:"parentId"` - Direction string `json:"direction"` - Prio uint32 `json:"prio"` - State string `json:"state"` - PhaseOffset int64 `json:"phaseOffset"` + ParentID uint32 `json:"parentId"` + Direction string `json:"direction"` + Prio *uint32 `json:"prio,omitempty"` + State string `json:"state"` + PhaseOffsetPs float64 `json:"phaseOffsetPs"` } // PinParentPin contains nested netlink attributes. type PinParentPinHR struct { - ParentId uint32 `json:"parentId"` + ParentID uint32 `json:"parentId"` State string `json:"parentState"` } +// Defines possible pin states const ( - DPLL_PIN_STATE_CONNECTED = 1 - DPLL_PIN_STATE_DISCONNECTED = 2 - DPLL_PIN_STATE_SELECTABLE = 3 - - PinStateConnected = DPLL_PIN_STATE_CONNECTED - PinStateDisconnected = DPLL_PIN_STATE_DISCONNECTED - PinStateSelectable = DPLL_PIN_STATE_SELECTABLE - - PinDirectionInput = DPLL_PIN_DIRECTION_INPUT - PinDirectionOutput = DPLL_PIN_DIRECTION_OUTPUT - - DpllTypePPS uint32 = 1 - DpllTypeEEC uint32 = 2 + PinStateConnected = 1 + PinStateDisconnected = 2 + PinStateSelectable = 3 ) // GetPinState returns DPLL pin state as a string func GetPinState(s uint32) string { stateMap := map[int]string{ - 1: "connected", - 2: "disconnected", - 3: "selectable", + PinStateConnected: "connected", + PinStateDisconnected: "disconnected", + PinStateSelectable: "selectable", } r, found := stateMap[int(s)] if found { @@ -208,14 +278,23 @@ func GetPinState(s uint32) string { return "" } +// Defines possible pin types +const ( + PinTypeMUX = 1 + PinTypeEXT = 2 + PinTypeSYNCE = 3 + PinTypeINT = 4 + PinTypeGNSS = 5 +) + // GetPinType returns DPLL pin type as a string func GetPinType(tp uint32) string { typeMap := map[int]string{ - 1: "mux", - 2: "ext", - 3: "synce-eth-port", - 4: "int-oscillator", - 5: "gnss", + PinTypeMUX: "mux", + PinTypeEXT: "ext", + PinTypeSYNCE: "synce-eth-port", + PinTypeINT: "int-oscillator", + PinTypeGNSS: "gnss", } typ, found := typeMap[int(tp)] if found { @@ -224,16 +303,17 @@ func GetPinType(tp uint32) string { return "" } +// Defines pin directions const ( - DPLL_PIN_DIRECTION_INPUT = 1 - DPLL_PIN_DIRECTION_OUTPUT = 2 + PinDirectionInput = 1 + PinDirectionOutput = 2 ) // GetPinDirection returns DPLL pin direction as a string func GetPinDirection(d uint32) string { directionMap := map[int]string{ - 1: "input", - 2: "output", + PinDirectionInput: "input", + PinDirectionOutput: "output", } dir, found := directionMap[int(d)] if found { @@ -242,59 +322,65 @@ func GetPinDirection(d uint32) string { return "" } +// Defines pin capabilities +const ( + PinCapNone = 0 + PinCapDir = (1 << 0) + PinCapPrio = (1 << 1) + PinCapState = (1 << 2) +) + // GetPinCapabilities returns DPLL pin capabilities as a csv func GetPinCapabilities(c uint32) string { - cMap := map[int]string{ - 0: "", - 1: "direction-can-change", - 2: "priority-can-change", - 3: "direction-can-change,priority-can-change", - 4: "state-can-change", - 5: "state-can-change,direction-can-change", - 6: "state-can-change,priority-can-change", - 7: "state-can-change,direction-can-change,priority-can-change", + capList := []string{} + if c&PinCapState != 0 { + capList = append(capList, "state-can-change") } - cap, found := cMap[int(c)] - if found { - return cap + if c&PinCapDir != 0 { + capList = append(capList, "direction-can-change") } - return "" + if c&PinCapPrio != 0 { + capList = append(capList, "priority-can-change") + } + return strings.Join(capList, ",") } // GetPinInfoHR returns human-readable pin status -func GetPinInfoHR(reply *PinInfo) ([]byte, error) { - hr := DoPinGetReplyHR{ - Id: reply.Id, - ClockId: reply.ClockId, +func GetPinInfoHR(reply *PinInfo, timestamp time.Time) ([]byte, error) { + hr := PinInfoHR{ + Timestamp: timestamp, + ID: reply.ID, + ClockID: fmt.Sprintf("0x%x", reply.ClockID), BoardLabel: reply.BoardLabel, PanelLabel: reply.PanelLabel, PackageLabel: reply.PackageLabel, Type: GetPinType(reply.Type), Frequency: reply.Frequency, FrequencySupported: make([]FrequencyRange, 0), - Capabilities: GetPinCapabilities(reply.Capabilities), - ParentDevice: make([]PinParentDeviceHR, 0), - ParentPin: make([]PinParentPinHR, 0), PhaseAdjustMin: reply.PhaseAdjustMin, PhaseAdjustMax: reply.PhaseAdjustMax, PhaseAdjust: reply.PhaseAdjust, FractionalFrequencyOffset: reply.FractionalFrequencyOffset, ModuleName: reply.ModuleName, + ParentDevice: make([]PinParentDeviceHR, 0), + ParentPin: make([]PinParentPinHR, 0), + Capabilities: GetPinCapabilities(reply.Capabilities), + EsyncFrequency: reply.EsyncFrequency, + EsyncFrequencySupported: make([]FrequencyRange, 0), + EsyncPulse: int64(reply.EsyncPulse), } for i := 0; i < len(reply.ParentDevice); i++ { - hr.ParentDevice = append( - hr.ParentDevice, PinParentDeviceHR{ - ParentId: reply.ParentDevice[i].ParentId, - Direction: GetPinDirection(reply.ParentDevice[i].Direction), - Prio: reply.ParentDevice[i].Prio, - State: GetPinState(reply.ParentDevice[i].State), - PhaseOffset: reply.ParentDevice[i].PhaseOffset, - }) - + hr.ParentDevice = append(hr.ParentDevice, PinParentDeviceHR{ + ParentID: reply.ParentDevice[i].ParentID, + Direction: GetPinDirection(reply.ParentDevice[i].Direction), + Prio: reply.ParentDevice[i].Prio, + State: GetPinState(reply.ParentDevice[i].State), + PhaseOffsetPs: float64(reply.ParentDevice[i].PhaseOffset) / DpllPhaseOffsetDivider, + }) } for i := 0; i < len(reply.ParentPin); i++ { hr.ParentPin = append(hr.ParentPin, PinParentPinHR{ - ParentId: reply.ParentPin[i].ParentId, + ParentID: reply.ParentPin[i].ParentID, State: GetPinState(reply.ParentPin[i].State), }) } @@ -304,6 +390,11 @@ func GetPinInfoHR(reply *PinInfo) ([]byte, error) { FrequencyMax: reply.FrequencySupported[i].FrequencyMax, }) } - + for i := 0; i < len(reply.EsyncFrequencySupported); i++ { + hr.EsyncFrequencySupported = append(hr.EsyncFrequencySupported, FrequencyRange{ + FrequencyMin: reply.EsyncFrequencySupported[i].FrequencyMin, + FrequencyMax: reply.EsyncFrequencySupported[i].FrequencyMax, + }) + } return json.Marshal(hr) } diff --git a/pkg/dpll-netlink/dpll.go b/pkg/dpll-netlink/dpll.go index 5686e6da..51d56cfa 100644 --- a/pkg/dpll-netlink/dpll.go +++ b/pkg/dpll-netlink/dpll.go @@ -27,8 +27,8 @@ func (c *Conn) GetGenetlinkFamily() genetlink.Family { return c.f } -// GetMcastGroupId finds the requested multicast group in the family and returns its ID -func (c *Conn) GetMcastGroupId(mcGroup string) (id uint32, found bool) { +// GetMcastGroupID finds the requested multicast group in the family and returns its ID +func (c *Conn) GetMcastGroupID(mcGroup string) (id uint32, found bool) { for _, group := range c.f.Groups { if group.Name == mcGroup { return group.ID, true @@ -56,16 +56,15 @@ func Dial(cfg *netlink.Config) (*Conn, error) { // Close closes the Conn's underlying netlink connection. func (c *Conn) Close() error { return c.c.Close() } -// DoDeviceIdGet wraps the "device-id-get" operation: +// DoDeviceIDGet wraps the "device-id-get" operation: // Get id of dpll device that matches given attributes -func (c *Conn) DoDeviceIdGet(req DoDeviceIdGetRequest) (*DoDeviceIdGetReply, error) { +func (c *Conn) DoDeviceIDGet(req DoDeviceIDGetRequest) (*DoDeviceIDGetReply, error) { ae := netlink.NewAttributeEncoder() - // TODO: field "req.ModuleName", type "string" - if req.ClockId != 0 { - ae.Uint64(DPLL_A_CLOCK_ID, req.ClockId) + if req.ClockID != 0 { + ae.Uint64(DpllClockID, req.ClockID) } if req.Type != 0 { - ae.Uint8(DPLL_A_TYPE, req.Type) + ae.Uint8(DpllType, req.Type) } b, err := ae.Encode() @@ -75,7 +74,7 @@ func (c *Conn) DoDeviceIdGet(req DoDeviceIdGetRequest) (*DoDeviceIdGetReply, err msg := genetlink.Message{ Header: genetlink.Header{ - Command: DPLL_CMD_DEVICE_ID_GET, + Command: DpllCmdDeviceIDGet, Version: c.f.Version, }, Data: b, @@ -86,18 +85,18 @@ func (c *Conn) DoDeviceIdGet(req DoDeviceIdGetRequest) (*DoDeviceIdGetReply, err return nil, err } - replies := make([]*DoDeviceIdGetReply, 0, len(msgs)) + replies := make([]*DoDeviceIDGetReply, 0, len(msgs)) for _, m := range msgs { ad, err := netlink.NewAttributeDecoder(m.Data) if err != nil { return nil, err } - var reply DoDeviceIdGetReply + var reply DoDeviceIDGetReply for ad.Next() { switch ad.Type() { - case DPLL_A_ID: - reply.Id = ad.Uint32() + case DpllID: + reply.ID = ad.Uint32() } } @@ -109,22 +108,22 @@ func (c *Conn) DoDeviceIdGet(req DoDeviceIdGetRequest) (*DoDeviceIdGetReply, err } if len(replies) != 1 { - return nil, errors.New("dpll: expected exactly one DoDeviceIdGetReply") + return nil, errors.New("dpll: expected exactly one DoDeviceIDGetReply") } return replies[0], nil } -// DoDeviceIdGetRequest is used with the DoDeviceIdGet method. -type DoDeviceIdGetRequest struct { +// DoDeviceIDGetRequest is used with the DoDeviceIDGet method. +type DoDeviceIDGetRequest struct { // TODO: field "ModuleName", type "string" - ClockId uint64 + ClockID uint64 Type uint8 } -// DoDeviceIdGetReply is used with the DoDeviceIdGet method. -type DoDeviceIdGetReply struct { - Id uint32 +// DoDeviceIDGetReply is used with the DoDeviceIDGet method. +type DoDeviceIDGetReply struct { + ID uint32 } func ParseDeviceReplies(msgs []genetlink.Message) ([]*DoDeviceGetReply, error) { @@ -137,22 +136,22 @@ func ParseDeviceReplies(msgs []genetlink.Message) ([]*DoDeviceGetReply, error) { var reply DoDeviceGetReply for ad.Next() { switch ad.Type() { - case DPLL_A_ID: - reply.Id = ad.Uint32() - case DPLL_A_MODULE_NAME: + case DpllID: + reply.ID = ad.Uint32() + case DpllModuleName: reply.ModuleName = ad.String() - case DPLL_A_MODE: + case DpllMode: reply.Mode = ad.Uint32() - case DPLL_A_MODE_SUPPORTED: + case DpllModeSupported: reply.ModeSupported = append(reply.ModeSupported, ad.Uint32()) - case DPLL_A_LOCK_STATUS: + case DpllLockStatus: reply.LockStatus = ad.Uint32() - case DPLL_A_PAD: - case DPLL_A_TEMP: + case DpllAttPadding: + case DpllTemp: reply.Temp = ad.Int32() - case DPLL_A_CLOCK_ID: - reply.ClockId = ad.Uint64() - case DPLL_A_TYPE: + case DpllClockID: + reply.ClockID = ad.Uint64() + case DpllType: reply.Type = ad.Uint32() default: log.Println("default", ad.Type(), len(ad.Bytes()), ad.Bytes()) @@ -170,8 +169,8 @@ func ParseDeviceReplies(msgs []genetlink.Message) ([]*DoDeviceGetReply, error) { // Get list of DPLL devices (dump) or attributes of a single dpll device func (c *Conn) DoDeviceGet(req DoDeviceGetRequest) (*DoDeviceGetReply, error) { ae := netlink.NewAttributeEncoder() - if req.Id != 0 { - ae.Uint32(DPLL_A_ID, req.Id) + if req.ID != 0 { + ae.Uint32(DpllID, req.ID) } // TODO: field "req.ModuleName", type "string" @@ -182,7 +181,7 @@ func (c *Conn) DoDeviceGet(req DoDeviceGetRequest) (*DoDeviceGetReply, error) { msg := genetlink.Message{ Header: genetlink.Header{ - Command: DPLL_CMD_DEVICE_GET, + Command: DpllCmdDeviceGet, Version: c.f.Version, }, Data: b, @@ -214,7 +213,7 @@ func (c *Conn) DumpDeviceGet() ([]*DoDeviceGetReply, error) { msg := genetlink.Message{ Header: genetlink.Header{ - Command: DPLL_CMD_DEVICE_GET, + Command: DpllCmdDeviceGet, Version: c.f.Version, }, Data: b, @@ -234,19 +233,19 @@ func (c *Conn) DumpDeviceGet() ([]*DoDeviceGetReply, error) { // DoDeviceGetRequest is used with the DoDeviceGet method. type DoDeviceGetRequest struct { - Id uint32 + ID uint32 ModuleName string } // DoDeviceGetReply is used with the DoDeviceGet method. type DoDeviceGetReply struct { - Id uint32 + ID uint32 ModuleName string Mode uint32 ModeSupported []uint32 LockStatus uint32 Temp int32 - ClockId uint64 + ClockID uint64 Type uint32 } @@ -259,40 +258,43 @@ func ParsePinReplies(msgs []genetlink.Message) ([]*PinInfo, error) { return nil, err } var reply PinInfo - for ad.Next() { switch ad.Type() { - case DPLL_A_PIN_CLOCK_ID: - reply.ClockId = ad.Uint64() - case DPLL_A_PIN_ID: - reply.Id = ad.Uint32() - case DPLL_A_PIN_BOARD_LABEL: + case DpllPinID: + reply.ID = ad.Uint32() + case DpllPinParentID: + reply.ParentID = ad.Uint32() + case DpllPinModuleName: + reply.ModuleName = ad.String() + case DpllPinClockID: + reply.ClockID = ad.Uint64() + case DpllPinBoardLabel: reply.BoardLabel = ad.String() - case DPLL_A_PIN_PANEL_LABEL: + case DpllPinPanelLabel: reply.PanelLabel = ad.String() - case DPLL_A_PIN_PACKAGE_LABEL: + case DpllPinPackageLabel: reply.PackageLabel = ad.String() - case DPLL_A_PIN_TYPE: + case DpllPinType: reply.Type = ad.Uint32() - case DPLL_A_PIN_FREQUENCY: + case DpllPinFrequency: reply.Frequency = ad.Uint64() - case DPLL_A_PIN_FREQUENCY_SUPPORTED: + case DpllPinFrequencySupported: ad.Nested(func(ad *netlink.AttributeDecoder) error { var temp FrequencyRange for ad.Next() { switch ad.Type() { - case DPLL_A_PIN_FREQUENCY_MIN: + case DpllPinFrequencyMin: temp.FrequencyMin = ad.Uint64() - case DPLL_A_PIN_FREQUENCY_MAX: + case DpllPinFrequencyMax: temp.FrequencyMax = ad.Uint64() } } reply.FrequencySupported = append(reply.FrequencySupported, temp) return nil }) - case DPLL_A_PIN_CAPABILITIES: + case DpllPinCapabilities: reply.Capabilities = ad.Uint32() - case DPLL_A_PIN_PARENT_DEVICE: + case DpllPinParentDevice: ad.Nested(func(ad *netlink.AttributeDecoder) error { temp := PinParentDevice{ // Initialize phase offset to a max value, so later we can detect it has been updated @@ -300,15 +302,16 @@ func ParsePinReplies(msgs []genetlink.Message) ([]*PinInfo, error) { } for ad.Next() { switch ad.Type() { - case DPLL_A_PIN_PARENT_ID: - temp.ParentId = ad.Uint32() - case DPLL_A_PIN_DIRECTION: + case DpllPinParentID: + temp.ParentID = ad.Uint32() + case DpllPinDirection: temp.Direction = ad.Uint32() - case DPLL_A_PIN_PRIO: - temp.Prio = ad.Uint32() - case DPLL_A_PIN_STATE: + case DpllPinPrio: + v := ad.Uint32() + temp.Prio = &v + case DpllPinState: temp.State = ad.Uint32() - case DPLL_A_PIN_PHASE_OFFSET: + case DpllPinPhaseOffset: temp.PhaseOffset = ad.Int64() } @@ -316,34 +319,50 @@ func ParsePinReplies(msgs []genetlink.Message) ([]*PinInfo, error) { reply.ParentDevice = append(reply.ParentDevice, temp) return nil }) - case DPLL_A_PIN_PARENT_PIN: + case DpllPinParentPin: ad.Nested(func(ad *netlink.AttributeDecoder) error { var temp PinParentPin for ad.Next() { - switch ad.Type() { - case DPLL_A_PIN_PARENT_ID: - temp.ParentId = ad.Uint32() - case DPLL_A_PIN_STATE: + case DpllPinParentID: + temp.ParentID = ad.Uint32() + case DpllPinState: temp.State = ad.Uint32() } - } reply.ParentPin = append(reply.ParentPin, temp) return nil }) - case DPLL_A_PIN_PHASE_ADJUST_MIN: + case DpllPinPhaseAdjustMin: reply.PhaseAdjustMin = ad.Int32() - case DPLL_A_PIN_PHASE_ADJUST_MAX: + case DpllPinPhaseAdjustMax: reply.PhaseAdjustMax = ad.Int32() - case DPLL_A_PIN_PHASE_ADJUST: + case DpllPinPhaseAdjust: reply.PhaseAdjust = ad.Int32() - case DPLL_A_PIN_FRACTIONAL_FREQUENCY_OFFSET: + case DpllPinPhaseOffset: + reply.PhaseOffset = ad.Int64() + case DpllPinFractionalFrequencyOffset: reply.FractionalFrequencyOffset = int(ad.Int32()) - case DPLL_A_PIN_MODULE_NAME: - reply.ModuleName = ad.String() + case DpllPinEsyncFrequency: + reply.EsyncFrequency = ad.Int64() + case DpllPinEsyncFrequencySupported: + ad.Nested(func(ad *netlink.AttributeDecoder) error { + var temp FrequencyRange + for ad.Next() { + switch ad.Type() { + case DpllPinFrequencyMin: + temp.FrequencyMin = ad.Uint64() + case DpllPinFrequencyMax: + temp.FrequencyMax = ad.Uint64() + } + } + reply.EsyncFrequencySupported = append(reply.EsyncFrequencySupported, temp) + return nil + }) + case DpllPinEsyncPulse: + reply.EsyncPulse = ad.Uint32() default: - log.Println(ad.Bytes()) + log.Printf("unrecognized type: %d\n", ad.Type()) } } if err := ad.Err(); err != nil { @@ -357,7 +376,7 @@ func ParsePinReplies(msgs []genetlink.Message) ([]*PinInfo, error) { // DoPinGet wraps the "pin-get" operation: func (c *Conn) DoPinGet(req DoPinGetRequest) (*PinInfo, error) { ae := netlink.NewAttributeEncoder() - ae.Uint32(DPLL_A_PIN_ID, req.Id) + ae.Uint32(DpllPinID, req.ID) b, err := ae.Encode() if err != nil { @@ -366,7 +385,7 @@ func (c *Conn) DoPinGet(req DoPinGetRequest) (*PinInfo, error) { msg := genetlink.Message{ Header: genetlink.Header{ - Command: DPLL_CMD_PIN_GET, + Command: DpllCmdPinGet, Version: c.f.Version, }, Data: b, @@ -390,7 +409,7 @@ func (c *Conn) DoPinGet(req DoPinGetRequest) (*PinInfo, error) { func (c *Conn) DumpPinGet() ([]*PinInfo, error) { msg := genetlink.Message{ Header: genetlink.Header{ - Command: DPLL_CMD_PIN_GET, + Command: DpllCmdPinGet, Version: c.f.Version, }, } @@ -410,27 +429,37 @@ func (c *Conn) DumpPinGet() ([]*PinInfo, error) { // DoPinGetRequest is used with the DoPinGet method. type DoPinGetRequest struct { - Id uint32 + ID uint32 } -// PinInfo is used with the DoPinGet method. +// PinInfo is used with the DoPinSet /DoPinGet / DumpPinGet / monitor methods. type PinInfo struct { - Id uint32 - ClockId uint64 + ID uint32 + ParentID uint32 + ModuleName string + ClockID uint64 BoardLabel string PanelLabel string PackageLabel string Type uint32 + Direction uint32 Frequency uint64 FrequencySupported []FrequencyRange + FrequencyMin uint64 + FrequencyMax uint64 + Prio uint32 + State uint32 Capabilities uint32 ParentDevice []PinParentDevice ParentPin []PinParentPin PhaseAdjustMin int32 PhaseAdjustMax int32 PhaseAdjust int32 + PhaseOffset int64 FractionalFrequencyOffset int - ModuleName string + EsyncFrequency int64 + EsyncFrequencySupported []FrequencyRange + EsyncPulse uint32 } // FrequencyRange contains nested netlink attributes. @@ -441,22 +470,22 @@ type FrequencyRange struct { // PinParentDevice contains nested netlink attributes. type PinParentDevice struct { - ParentId uint32 + ParentID uint32 Direction uint32 - Prio uint32 + Prio *uint32 State uint32 PhaseOffset int64 } // PinParentPin contains nested netlink attributes. type PinParentPin struct { - ParentId uint32 + ParentID uint32 State uint32 } // PinPhaseAdjustRequest is used with PinPhaseAdjust method. type PinPhaseAdjustRequest struct { - Id uint32 + ID uint32 PhaseAdjust int32 } @@ -464,8 +493,8 @@ type PinPhaseAdjustRequest struct { // Set PhaseAdjust of a target pin func (c *Conn) PinPhaseAdjust(req PinPhaseAdjustRequest) error { ae := netlink.NewAttributeEncoder() - ae.Uint32(DPLL_A_PIN_ID, req.Id) - ae.Int32(DPLL_A_PIN_PHASE_ADJUST, req.PhaseAdjust) + ae.Uint32(DpllPinID, req.ID) + ae.Int32(DpllPinPhaseAdjust, req.PhaseAdjust) b, err := ae.Encode() if err != nil { @@ -474,7 +503,7 @@ func (c *Conn) PinPhaseAdjust(req PinPhaseAdjustRequest) error { msg := genetlink.Message{ Header: genetlink.Header{ - Command: DPLL_CMD_PIN_SET, + Command: DpllCmdPinSet, Version: c.f.Version, }, Data: b, @@ -486,12 +515,14 @@ func (c *Conn) PinPhaseAdjust(req PinPhaseAdjustRequest) error { } type PinParentDeviceCtl struct { - Id uint32 - PhaseAdjust *int32 - PinParentCtl []PinControl + ID uint32 + Frequency *uint64 + PhaseAdjust *int32 + EsyncFrequency *uint64 + PinParentCtl []PinControl } type PinControl struct { - PinParentId uint32 + PinParentID uint32 Direction *uint32 Prio *uint32 State *uint32 @@ -499,21 +530,27 @@ type PinControl struct { func EncodePinControl(req PinParentDeviceCtl) ([]byte, error) { ae := netlink.NewAttributeEncoder() - ae.Uint32(DPLL_A_PIN_ID, req.Id) + ae.Uint32(DpllPinID, req.ID) if req.PhaseAdjust != nil { - ae.Int32(DPLL_A_PIN_PHASE_ADJUST, *req.PhaseAdjust) + ae.Int32(DpllPinPhaseAdjust, *req.PhaseAdjust) + } + if req.EsyncFrequency != nil { + ae.Uint64(DpllPinPhaseAdjust, *req.EsyncFrequency) + } + if req.Frequency != nil { + ae.Uint64(DpllPinFrequency, *req.Frequency) } for _, pp := range req.PinParentCtl { - ae.Nested(DPLL_A_PIN_PARENT_DEVICE, func(ae *netlink.AttributeEncoder) error { - ae.Uint32(DPLL_A_PIN_PARENT_ID, pp.PinParentId) + ae.Nested(DpllPinParentDevice, func(ae *netlink.AttributeEncoder) error { + ae.Uint32(DpllPinParentID, pp.PinParentID) if pp.State != nil { - ae.Uint32(DPLL_A_PIN_STATE, *pp.State) + ae.Uint32(DpllPinState, *pp.State) } if pp.Prio != nil { - ae.Uint32(DPLL_A_PIN_PRIO, *pp.Prio) + ae.Uint32(DpllPinPrio, *pp.Prio) } if pp.Direction != nil { - ae.Uint32(DPLL_A_PIN_DIRECTION, *pp.Direction) + ae.Uint32(DpllPinDirection, *pp.Direction) } return nil }) diff --git a/pkg/dpll-netlink/dpll_test.go b/pkg/dpll-netlink/dpll_test.go index 9b0b8979..c3df7b90 100644 --- a/pkg/dpll-netlink/dpll_test.go +++ b/pkg/dpll-netlink/dpll_test.go @@ -17,7 +17,7 @@ func Test_EncodePinControl(t *testing.T) { assert.New(t) // test phase adjustment pc := PinParentDeviceCtl{ - Id: 88, + ID: 88, PhaseAdjust: func() *int32 { t := int32(math.MinInt32) return &t @@ -33,14 +33,14 @@ func Test_EncodePinControl(t *testing.T) { pc.PhaseAdjust = nil pc.PinParentCtl = []PinControl{ { - PinParentId: 8, + PinParentID: 8, Prio: func() *uint32 { t := uint32(math.MaxUint32) return &t }(), }, { - PinParentId: 8, + PinParentID: 8, Prio: func() *uint32 { t := uint32(math.MaxUint8) return &t @@ -56,7 +56,7 @@ func Test_EncodePinControl(t *testing.T) { // Test setting the connection state pc.PinParentCtl = []PinControl{ { - PinParentId: 0, + PinParentID: 0, Prio: nil, Direction: func() *uint32 { t := uint32(2) @@ -64,7 +64,7 @@ func Test_EncodePinControl(t *testing.T) { }(), }, { - PinParentId: 1, + PinParentID: 1, Prio: nil, Direction: func() *uint32 { t := uint32(1) diff --git a/pkg/dpll/dpll.go b/pkg/dpll/dpll.go index 5917419a..108a8609 100644 --- a/pkg/dpll/dpll.go +++ b/pkg/dpll/dpll.go @@ -29,9 +29,9 @@ const ( DPLL_LOCKED_HO_ACQ = 3 DPLL_HOLDOVER = 4 - LocalMaxHoldoverOffSet = 1500 //ns - LocalHoldoverTimeout = 14400 //secs - MaxInSpecOffset = 1500 //ns + LocalMaxHoldoverOffSet = 1500 // ns + LocalHoldoverTimeout = 14400 // secs + MaxInSpecOffset = 1500 // ns monitoringInterval = 1 * time.Second LocalMaxHoldoverOffSetStr = "LocalMaxHoldoverOffSet" @@ -44,6 +44,38 @@ const ( PPS_PIN_INDEX = 1 ) +// Flag is a bitmask which changes the default DPLL monitoeing behavior +type Flag uint64 + +const ( + // FlagNoPhaseOffset allows skipping phase offset monitoring + FlagNoPhaseOffset Flag = (1 << 0) + // FlagNoPhaseStatus allows skipping phase status (pps lock/unlock) monitoring + FlagNoPhaseStatus Flag = (1 << 1) + // FlagNoFreqencyStatus allows skipping frequency status (eec lock/unlock) monitoring + FlagNoFreqencyStatus Flag = (1 << 2) + + // FlagOnlyPhaseStatus represents a DPll with only phase status (pps lock/unlock) + FlagOnlyPhaseStatus Flag = FlagNoFreqencyStatus | FlagNoPhaseOffset +) + +func stateName(state int64) string { + switch state { + case DPLL_INVALID: + return "INVALID" + case DPLL_FREERUN: + return "FREERUN" + case DPLL_LOCKED: + return "LOCKED" + case DPLL_LOCKED_HO_ACQ: + return "LOCKED_HO_ACQ" + case DPLL_HOLDOVER: + return "HOLDOVER" + default: + return "UNKNOWN" + } +} + type dpllApiType string var MockDpllReplies chan *nl.DoDeviceGetReply @@ -96,11 +128,12 @@ type DpllConfig struct { iface string name string slope float64 - timer int64 //secs + timer int64 // secs inSpec bool frequencyTraceable bool state event.PTPState onHoldover bool + closing bool sourceLost bool processConfig config.ProcessConfig dependsOn []event.EventSource @@ -116,6 +149,7 @@ type DpllConfig struct { phaseStatus int64 frequencyStatus int64 phaseOffset int64 + // clockId is needed to distinguish between DPLL associated with the particular // iface from other DPLL units that might be present on the system. Clock ID implementation // is driver-specific and vendor-specific. @@ -126,6 +160,12 @@ type DpllConfig struct { phaseOffsetPinFilter map[string]map[string]string inSyncConditionThreshold uint64 inSyncConditionTimes uint64 + // hardwareConfigHandler is called when device notifications are received + // All logic for processing device notifications is handled by the hardwareconfig layer + hardwareConfigHandler func(devices []*nl.DoDeviceGetReply) error + + // Some DPLLs (Carter Flats, for one) do not have both pps (phase) and eec (frequency) states. + flags Flag // devices holds the cache of DPLL device replies devices []*nl.DoDeviceGetReply @@ -156,7 +196,7 @@ func (d *DpllConfig) State() event.PTPState { // The units are picoseconds. // We further divide it by 1000 to report nanoseconds func (d *DpllConfig) SetPhaseOffset(phaseOffset int64) { - d.phaseOffset = int64(math.Round(float64(phaseOffset / nl.DPLL_PHASE_OFFSET_DIVIDER / 1000))) + d.phaseOffset = int64(math.Round(float64(phaseOffset / nl.DpllPhaseOffsetDivider / 1000))) } // SourceLost ... get source status @@ -169,11 +209,42 @@ func (d *DpllConfig) SetSourceLost(sourceLost bool) { d.sourceLost = sourceLost } +// SetHardwareConfigHandler sets the callback function to be invoked when device notifications are received. +// The handler receives all device notifications and is responsible for all matching logic. +func (d *DpllConfig) SetHardwareConfigHandler(handler func(devices []*nl.DoDeviceGetReply) error) { + d.hardwareConfigHandler = handler +} + // PhaseOffset ... get phase offset func (d *DpllConfig) PhaseOffset() int64 { return d.phaseOffset } +func (d *DpllConfig) hasFlag(flag Flag) bool { + return (d.flags & flag) == flag +} + +func (d *DpllConfig) flagsToStrings() []string { + result := make([]string, 0) + if d.hasFlag(FlagNoFreqencyStatus) { + result = append(result, "NoFrequencyStatus") + } + if d.hasFlag(FlagNoPhaseStatus) { + result = append(result, "NoPhaseStatus") + } + if d.hasFlag(FlagNoPhaseOffset) { + result = append(result, "NoPhaseOffset") + } + return result +} + +func (d *DpllConfig) phaseOffsetStr() string { + if d.hasFlag(FlagNoPhaseOffset) { + return "UNKNOWN" + } + return fmt.Sprintf("%d", d.phaseOffset) +} + // FrequencyStatus ... get frequency status func (d *DpllConfig) FrequencyStatus() int64 { return d.frequencyStatus @@ -232,7 +303,7 @@ func (d *DpllConfig) Name() string { // Stopped ... stopped func (d *DpllConfig) Stopped() bool { - //TODO implement me + // TODO implement me panic("implement me") } @@ -241,7 +312,7 @@ func (d *DpllConfig) ExitCh() chan struct{} { return d.exitCh } -// ExitCh ... exit channel +// hasGNSSSAsSource returns whether or not DPLL has GNSS as a source func (d *DpllConfig) hasGNSSAsSource() bool { if d.dependsOn[0] == event.GNSS { return true @@ -249,12 +320,19 @@ func (d *DpllConfig) hasGNSSAsSource() bool { return false } -// ExitCh ... exit channel +// hasPPSAsSource returns whether or not DPLL has PPS as a source func (d *DpllConfig) hasPPSAsSource() bool { - if d.dependsOn[0] == event.PPS { - return true - } - return false + return d.dependsOn[0] == event.PPS +} + +// hasPTPAsSource returns whether or not DPLL has PTP as a source +func (d *DpllConfig) hasPTPAsSource() bool { + return d.dependsOn[0] == event.PTP4l +} + +// hasLeadingSource returns whether or not DPLL is a leading source +func (d *DpllConfig) hasLeadingSource() bool { + return d.hasPTPAsSource() || d.hasGNSSAsSource() } // CmdStop ... stop command @@ -293,7 +371,9 @@ func (d *DpllConfig) unRegisterAll() { // NewDpll ... create new DPLL process func NewDpll(clockId uint64, localMaxHoldoverOffSet, localHoldoverTimeout, maxInSpecOffset uint64, - iface string, dependsOn []event.EventSource, apiType dpllApiType, phaseOffsetPinFilter map[string]map[string]string) *DpllConfig { + iface string, dependsOn []event.EventSource, apiType dpllApiType, phaseOffsetPinFilter map[string]map[string]string, + inSyncConditionTh uint64, inSyncConditionTimes uint64, dpllFlags Flag, +) *DpllConfig { glog.Infof("Calling NewDpll with clockId %x, localMaxHoldoverOffSet=%d, localHoldoverTimeout=%d, maxInSpecOffset=%d, iface=%s, phase offset pin filter=%v", clockId, localMaxHoldoverOffSet, localHoldoverTimeout, maxInSpecOffset, iface, phaseOffsetPinFilter) d := &DpllConfig{ clockId: clockId, @@ -303,19 +383,28 @@ func NewDpll(clockId uint64, localMaxHoldoverOffSet, localHoldoverTimeout, maxIn slope: func() float64 { return float64(localMaxHoldoverOffSet) / float64(localHoldoverTimeout) }(), - timer: 0, - state: event.PTP_FREERUN, - iface: iface, - onHoldover: false, - sourceLost: false, - frequencyTraceable: false, - dependsOn: dependsOn, - exitCh: make(chan struct{}), - ticker: time.NewTicker(monitoringInterval), - isMonitoring: false, - apiType: apiType, - phaseOffsetPinFilter: phaseOffsetPinFilter, - phaseOffset: FaultyPhaseOffset, + timer: 0, + state: event.PTP_FREERUN, + iface: iface, + onHoldover: false, + closing: false, + sourceLost: false, + frequencyTraceable: false, + dependsOn: dependsOn, + exitCh: make(chan struct{}), + ticker: time.NewTicker(monitoringInterval), + isMonitoring: false, + apiType: apiType, + phaseOffsetPinFilter: phaseOffsetPinFilter, + phaseOffset: FaultyPhaseOffset, + inSyncConditionThreshold: inSyncConditionTh, + inSyncConditionTimes: inSyncConditionTimes, + flags: dpllFlags, + } + + if d.flags != 0 { + flagStrings := d.flagsToStrings() + glog.Warningf("Partial monitoring detected for %s clockId %#x: %v", iface, clockId, flagStrings) } // time to reach maxnInSpecOffset @@ -324,6 +413,7 @@ func NewDpll(clockId uint64, localMaxHoldoverOffSet, localHoldoverTimeout, maxIn d.slope, float64(d.MaxInSpecOffset), d.timer, int64(d.LocalHoldoverTimeout)) return d } + func (d *DpllConfig) Slope() float64 { return d.slope } @@ -335,7 +425,7 @@ func (d *DpllConfig) Timer() int64 { // ActivePhaseOffsetPin checks whether the given pin is actively connected // and feeds the relevant PPS DPLL matched by clock ID func (d *DpllConfig) ActivePhaseOffsetPin(pin *nl.PinInfo) (int, bool) { - if pin.ClockId != d.clockId { + if pin.ClockID != d.clockId { return -1, false } for i, p := range pin.ParentDevice { @@ -343,7 +433,7 @@ func (d *DpllConfig) ActivePhaseOffsetPin(pin *nl.PinInfo) (int, bool) { continue } for _, dev := range d.devices { - if dev.Id == p.ParentId && dev.ClockId == d.clockId && nl.GetDpllType(dev.Type) == "pps" { + if dev.ID == p.ParentID && dev.ClockID == d.clockId && nl.GetDpllType(dev.Type) == "pps" { return i, true } } @@ -356,22 +446,23 @@ func (d *DpllConfig) nlUpdateState(devices []*nl.DoDeviceGetReply, pins []*nl.Pi valid := false for _, reply := range devices { - if reply.ClockId == d.clockId { - if reply.LockStatus == DPLL_INVALID { - glog.Info("discarding on invalid lock status: ", nl.GetDpllStatusHR(reply)) + if reply.ClockID == d.clockId { + replyHr, err := nl.GetDpllStatusHR(reply, time.Now()) + if err != nil || reply.LockStatus == DPLL_INVALID { + glog.Info("discarding on invalid lock status: ", replyHr) continue } - glog.Info(nl.GetDpllStatusHR(reply), " ", d.iface) + glog.Info(string(replyHr), " ", d.iface) switch nl.GetDpllType(reply.Type) { case "eec": d.frequencyStatus = int64(reply.LockStatus) + glog.Infof("%s (%#x) updating eec to %s (%d)", d.iface, d.clockId, stateName(d.frequencyStatus), d.frequencyStatus) valid = true case "pps": d.phaseStatus = int64(reply.LockStatus) + glog.Infof("%s (%#x) updating pps to %s (%d)", d.iface, d.clockId, stateName(d.phaseStatus), d.phaseStatus) valid = true } - } else { - glog.Infof("discarding on clock ID %v (%s): ", nl.GetDpllStatusHR(reply), d.iface) } } for _, pin := range pins { @@ -401,13 +492,13 @@ func (d *DpllConfig) monitorNtf(c *genetlink.Conn) { for _, msg := range msgs { devices, pins = []*nl.DoDeviceGetReply{}, []*nl.PinInfo{} switch msg.Header.Command { - case nl.DPLL_CMD_DEVICE_CHANGE_NTF: + case nl.DpllCmdDeviceChangeNtf: devices, err = nl.ParseDeviceReplies([]genetlink.Message{msg}) if err != nil { glog.Error(err) return } - case nl.DPLL_CMD_PIN_CHANGE_NTF: + case nl.DpllCmdPinChangeNtf: pins, err = nl.ParsePinReplies([]genetlink.Message{msg}) if err != nil { glog.Error(err) @@ -418,6 +509,13 @@ func (d *DpllConfig) monitorNtf(c *genetlink.Conn) { } } + // Pass device notifications to hardwareconfig handler if present + // All logic (clock ID matching, lock status checking) happens in hardwareconfig layer + if len(devices) > 0 && d.hardwareConfigHandler != nil { + if err = d.hardwareConfigHandler(devices); err != nil { + glog.Errorf("hardwareconfig handler error: %v", err) + } + } if d.nlUpdateState(devices, pins) { d.stateDecision() } @@ -484,9 +582,9 @@ func (d *DpllConfig) MonitorDpllNetlink() { } c := d.conn.GetGenetlinkConn() - mcastId, found := d.conn.GetMcastGroupId(nl.DPLL_MCGRP_MONITOR) + mcastID, found := d.conn.GetMcastGroupID(nl.DpllMCGRPMonitor) if !found { - glog.Warning("multicast ID ", nl.DPLL_MCGRP_MONITOR, " not found") + glog.Warning("multicast ID ", nl.DpllMCGRPMonitor, " not found") goto abort } @@ -501,7 +599,7 @@ func (d *DpllConfig) MonitorDpllNetlink() { d.stateDecision() } - err = c.JoinGroup(mcastId) + err = c.JoinGroup(mcastID) if err != nil { goto abort } @@ -542,13 +640,16 @@ func (d *DpllConfig) MonitorDpllNetlink() { // unregister from event notification from other processes d.unRegisterAllSubscriber() + d.stopDpll() + // Allow generated events some time to get processed + time.Sleep(time.Second) if d.onHoldover { close(d.holdoverCloseCh) glog.Infof("closing holdover for %s", d.iface) d.onHoldover = false + d.closing = true } - d.stopDpll() return default: @@ -588,7 +689,7 @@ func (d *DpllConfig) MonitorProcess(processCfg config.ProcessConfig) { d.processConfig = processCfg // register to event notification from other processes for _, dep := range d.dependsOn { - if dep == event.GNSS { //TODO: fow now no subscription for pps + if dep == event.GNSS { // TODO: fow now no subscription for pps dependingProcessStateMap.states[dep] = event.PTP_UNKNOWN // register to event notification from other processes d.subscriber = append(d.subscriber, &DpllSubscriber{source: dep, dpll: d, id: fmt.Sprintf("%s-%x", event.DPLL, d.clockId)}) @@ -635,14 +736,14 @@ func (d *DpllConfig) MonitorDpll() { // stateDecision func (d *DpllConfig) stateDecision() { - dpllStatus := d.getWorseState(d.phaseStatus, d.frequencyStatus) + dpllStatus := d.getDpllState() switch dpllStatus { case DPLL_FREERUN, DPLL_INVALID, DPLL_UNKNOWN: d.inSpec = false d.sourceLost = true - glog.Infof("on holdover %t has GNSS source %t", d.onHoldover, d.hasGNSSAsSource()) - if d.hasGNSSAsSource() && d.onHoldover { + glog.Infof("%s dpll with %s source is in FREERUN", d.iface, d.dependsOn[0]) + if d.hasLeadingSource() && d.onHoldover { glog.Infof("trying to close holdover (%s)", d.iface) select { case d.holdoverCloseCh <- true: @@ -652,8 +753,6 @@ func (d *DpllConfig) stateDecision() { } d.state = event.PTP_FREERUN d.phaseOffset = FaultyPhaseOffset - glog.Infof("dpll is in FREERUN, state is FREERUN (%s)", d.iface) - d.sendDpllEvent() case DPLL_HOLDOVER: switch { @@ -677,22 +776,31 @@ func (d *DpllConfig) stateDecision() { d.state = event.PTP_HOLDOVER glog.Infof("starting holdover (%s)", d.iface) go d.holdover() - + } + case d.hasPTPAsSource(): + if d.PhaseOffset() > LocalMaxHoldoverOffSet { + glog.Infof("dpll offset is above MaxHoldoverOffSet, state is FREERUN(%s)", d.iface) + d.state = event.PTP_FREERUN + d.phaseOffset = FaultyPhaseOffset + d.sourceLost = true + select { + case d.holdoverCloseCh <- true: + glog.Infof("closing holdover for %s since offset if above MaxHoldoverOffSet", d.iface) + default: + } + } else if !d.onHoldover && !d.closing { + d.holdoverCloseCh = make(chan bool) + d.onHoldover = true + d.state = event.PTP_HOLDOVER + glog.Infof("starting holdover (%s)", d.iface) + go d.holdover() } } - d.sendDpllEvent() - case DPLL_LOCKED_HO_ACQ: - if d.isOffsetInRange() { - d.state = event.PTP_LOCKED - d.inSpec = true - } else { - d.state = event.PTP_FREERUN - d.sourceLost = false // phase offset will be the one that was read - } + case DPLL_LOCKED_HO_ACQ, DPLL_LOCKED: if d.isOffsetInRange() { - glog.Infof("dpll is locked, source is not lost, offset is in range, state is DPLL_LOCKED_HO_ACQ(%s)", d.iface) - if d.hasGNSSAsSource() && d.onHoldover { + glog.Infof("%s dpll is locked, source is not lost, offset is in range, state is DPLL_LOCKED_HO_ACQ", d.iface) + if d.hasLeadingSource() && d.onHoldover { select { case d.holdoverCloseCh <- true: glog.Infof("closing holdover for %s since source is restored and locked ", d.iface) @@ -700,9 +808,10 @@ func (d *DpllConfig) stateDecision() { } } d.inSpec = true + d.sourceLost = false d.state = event.PTP_LOCKED } else { - glog.Infof("dpll is not in spec, state is DPLL_LOCKED_HO_ACQ, offset is out of range, state is FREERUN(%s)", d.iface) + glog.Infof("%s dpll is not in spec, state is DPLL_LOCKED_HO_ACQ, offset is out of range, state is FREERUN", d.iface) d.state = event.PTP_FREERUN d.inSpec = false d.phaseOffset = FaultyPhaseOffset @@ -712,19 +821,8 @@ func (d *DpllConfig) stateDecision() { default: } } - d.sendDpllEvent() - } - // log the decision - if d.hasPPSAsSource() { - glog.Infof("%s-dpll decision: Status %d, Offset %d, In spec %v, Source %v lost %v", - d.iface, dpllStatus, d.phaseOffset, d.inSpec, "pps", d.sourceLost) - d.sourceLost = false - //TODO: do not have a handler to catch pps source , so we will set to false - // and to true if state changes to holdover for source PPS based DPLL - } else if d.hasGNSSAsSource() { - glog.Infof("%s-dpll decision: Status %d, Offset %d, In spec %v, Source %v lost %v, On holdover %v", - d.iface, dpllStatus, d.phaseOffset, d.inSpec, "GNSS", d.sourceLost, d.onHoldover) } + d.sendDpllEvent() } // sendDpllEvent sends DPLL event to the event channel @@ -739,15 +837,17 @@ func (d *DpllConfig) sendDpllEvent() { IFace: d.iface, CfgName: d.processConfig.ConfigName, Values: map[event.ValueType]interface{}{ - event.FREQUENCY_STATUS: d.frequencyStatus, - event.OFFSET: d.phaseOffset, - event.PHASE_STATUS: d.phaseStatus, event.PPS_STATUS: func() int { if d.sourceLost { return 0 } return 1 }(), + event.LeadingSource: d.hasLeadingSource(), + event.InSyncConditionThreshold: d.inSyncConditionThreshold, + event.InSyncConditionTimes: d.inSyncConditionTimes, + event.ToFreeRunThreshold: d.LocalMaxHoldoverOffSet, + event.MaxInSpecOffset: d.MaxInSpecOffset, }, ClockType: d.processConfig.ClockType, Time: time.Now().UnixMilli(), @@ -757,9 +857,19 @@ func (d *DpllConfig) sendDpllEvent() { WriteToLog: true, Reset: false, } + if !d.hasFlag(FlagNoFreqencyStatus) { + eventData.Values[event.FREQUENCY_STATUS] = d.frequencyStatus + } + if !d.hasFlag(FlagNoPhaseStatus) { + eventData.Values[event.PHASE_STATUS] = d.phaseStatus + } + if !d.hasFlag(FlagNoPhaseOffset) { + eventData.Values[event.OFFSET] = d.phaseOffset + } select { case d.processConfig.EventChannel <- eventData: - glog.Infof("dpll event sent for (%s)", d.iface) + glog.Infof("dpll event sent for (%s): state %v, Offset %s, In spec %v, Source %v lost %v, On holdover %v", + d.iface, d.state, d.phaseOffsetStr(), d.inSpec, d.dependsOn[0], d.sourceLost, d.onHoldover) default: glog.Infof("failed to send dpll event, retying.(%s)", d.iface) } @@ -816,6 +926,23 @@ func (d *DpllConfig) sendDpllTerminationEvent() { d.unRegisterAllSubscriber() } +func (d *DpllConfig) getDpllState() int64 { + switch { + case d.hasPTPAsSource(): + // For T-BC EEC DPLL state is not taken into account + return d.phaseStatus + case d.hasFlag(FlagNoPhaseStatus): + // Special case if there is no Phase Status (pps) for this DPLL + return d.frequencyStatus + case d.hasFlag(FlagNoFreqencyStatus): + // Special case if there is no Frequency Status (eec) for this DPLL + return d.phaseStatus + default: + // Normal case: Worst state of phase or frequency status + return d.getWorseState(d.phaseStatus, d.frequencyStatus) + } +} + // getStateQuality maps the state with relatively worse signal quality with // a lower number for easy comparison // Ref: ITU-T G.781 section 6.3.1 Auto selection operation @@ -854,17 +981,26 @@ func (d *DpllConfig) holdover() { case <-ticker.C: d.phaseOffset = int64(math.Round((d.slope) * time.Since(start).Seconds())) glog.Infof("(%s) time since holdover start %f, offset %d nanosecond holdover %s", d.iface, time.Since(start).Seconds(), d.phaseOffset, strconv.FormatBool(d.onHoldover)) - if d.frequencyTraceable { - //TODO: not implemented : add when syncE is handled here - // use !d.isInSpecOffsetInRange() to declare HOLDOVER with clockClass 140 - // !d.isMaxHoldoverOffsetInRange() for clock class to move from 140 to 248 and event to FREERUN - } else if !d.isInSpecOffsetInRange() { // when holdover verify with local max holdover not with regular threshold - d.inSpec = false // will be in HO, Out of spec only if frequency is traceable - d.state = event.PTP_FREERUN + if d.hasGNSSAsSource() { + //nolint:all + if d.frequencyTraceable { + // TODO: not implemented : add when syncE is handled here + // use !d.isInSpecOffsetInRange() to declare HOLDOVER with clockClass 140 + // !d.isMaxHoldoverOffsetInRange() for clock class to move from 140 to 248 and event to FREERUN + } else if !d.isInSpecOffsetInRange() { // when holdover verify with local max holdover not with regular threshold + d.inSpec = false // will be in HO, Out of spec only if frequency is traceable + d.state = event.PTP_FREERUN + d.sendDpllEvent() + return + } + d.sendDpllEvent() + } else { + if !d.isInSpecOffsetInRange() { + d.inSpec = false + } + d.state = event.PTP_HOLDOVER d.sendDpllEvent() - return } - d.sendDpllEvent() case <-timeout: // since ts2phc has same timer , ts2phc should also move out of holdover d.inSpec = false // not in HO, Out of spec d.state = event.PTP_FREERUN @@ -881,6 +1017,10 @@ func (d *DpllConfig) holdover() { } func (d *DpllConfig) isMaxHoldoverOffsetInRange() bool { + if d.hasFlag(FlagNoPhaseOffset) { + // Special case when the DPLL has no reported phase offset + return true + } if d.phaseOffset <= int64(d.LocalMaxHoldoverOffSet) { return true } @@ -888,7 +1028,12 @@ func (d *DpllConfig) isMaxHoldoverOffsetInRange() bool { d.LocalMaxHoldoverOffSet, d.phaseOffset) return false } + func (d *DpllConfig) isInSpecOffsetInRange() bool { + if d.hasFlag(FlagNoPhaseOffset) { + // Special case when the DPLL has no reported phase offset + return true + } if d.phaseOffset <= int64(d.MaxInSpecOffset) { return true } @@ -898,6 +1043,10 @@ func (d *DpllConfig) isInSpecOffsetInRange() bool { } func (d *DpllConfig) isOffsetInRange() bool { + if d.hasFlag(FlagNoPhaseOffset) { + // Special case when the DPLL has no reported phase offset + return true + } if d.phaseOffset <= d.processConfig.GMThreshold.Max && d.phaseOffset >= d.processConfig.GMThreshold.Min { return true } @@ -978,3 +1127,13 @@ func CalculateTimer(nodeProfile *ptpv1.PtpProfile) (int64, int64, int64, int64, inSpecTimer := int64(math.Round(float64(maxInSpecOffset) / slope)) return int64(maxInSpecOffset), int64(localMaxHoldoverOffSet), int64(localHoldoverTimeout), inSpecTimer, false } + +// PtpSettingsDpllIgnoreKey returns the PtpSettings key to ignore DPLL for the given interface name: +func PtpSettingsDpllIgnoreKey(iface string) string { + return fmt.Sprintf("dpll.%s.ignore", iface) +} + +// PtpSettingsDpllFlagsKey returns the PtpSettings key to set DPLL behavioral flags for the given interface name: +func PtpSettingsDpllFlagsKey(iface string) string { + return fmt.Sprintf("dpll.%s.flags", iface) +} diff --git a/pkg/dpll/dpll_internal_test.go b/pkg/dpll/dpll_internal_test.go index 740f5dcf..96223119 100644 --- a/pkg/dpll/dpll_internal_test.go +++ b/pkg/dpll/dpll_internal_test.go @@ -17,18 +17,18 @@ func TestActivePhaseOffsetPin(t *testing.T) { ) ppsDevice := &nl.DoDeviceGetReply{ - Id: ppsDeviceID, - ClockId: testClockID, + ID: ppsDeviceID, + ClockID: testClockID, Type: nl.DpllTypePPS, } eecDevice := &nl.DoDeviceGetReply{ - Id: eecDeviceID, - ClockId: testClockID, + ID: eecDeviceID, + ClockID: testClockID, Type: nl.DpllTypeEEC, } otherClockPPS := &nl.DoDeviceGetReply{ - Id: otherDeviceID, - ClockId: otherClockID, + ID: otherDeviceID, + ClockID: otherClockID, Type: nl.DpllTypePPS, } @@ -45,9 +45,9 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: []*nl.DoDeviceGetReply{ppsDevice}, pin: &nl.PinInfo{ - ClockId: otherClockID, + ClockID: otherClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: ppsDeviceID, State: nl.PinStateConnected}, + {ParentID: ppsDeviceID, State: nl.PinStateConnected}, }, }, expectedIndex: -1, @@ -58,9 +58,9 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: []*nl.DoDeviceGetReply{ppsDevice, eecDevice}, pin: &nl.PinInfo{ - ClockId: testClockID, + ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, + {ParentID: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, }, }, expectedIndex: 0, @@ -71,9 +71,9 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: []*nl.DoDeviceGetReply{ppsDevice}, pin: &nl.PinInfo{ - ClockId: testClockID, + ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: ppsDeviceID, State: nl.PinStateDisconnected}, + {ParentID: ppsDeviceID, State: nl.PinStateDisconnected}, }, }, expectedIndex: -1, @@ -84,9 +84,9 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: []*nl.DoDeviceGetReply{eecDevice}, pin: &nl.PinInfo{ - ClockId: testClockID, + ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: eecDeviceID, State: nl.PinStateConnected}, + {ParentID: eecDeviceID, State: nl.PinStateConnected}, }, }, expectedIndex: -1, @@ -97,9 +97,9 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: []*nl.DoDeviceGetReply{otherClockPPS}, pin: &nl.PinInfo{ - ClockId: testClockID, + ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: otherDeviceID, State: nl.PinStateConnected}, + {ParentID: otherDeviceID, State: nl.PinStateConnected}, }, }, expectedIndex: -1, @@ -110,10 +110,10 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: []*nl.DoDeviceGetReply{ppsDevice, eecDevice}, pin: &nl.PinInfo{ - ClockId: testClockID, + ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: eecDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, - {ParentId: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, + {ParentID: eecDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, + {ParentID: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, }, }, expectedIndex: 1, @@ -124,9 +124,9 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: []*nl.DoDeviceGetReply{ppsDevice}, pin: &nl.PinInfo{ - ClockId: testClockID, + ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: ppsDeviceID, State: nl.PinStateSelectable}, + {ParentID: ppsDeviceID, State: nl.PinStateSelectable}, }, }, expectedIndex: -1, @@ -137,7 +137,7 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: []*nl.DoDeviceGetReply{ppsDevice}, pin: &nl.PinInfo{ - ClockId: testClockID, + ClockID: testClockID, ParentDevice: []nl.PinParentDevice{}, }, expectedIndex: -1, @@ -148,9 +148,9 @@ func TestActivePhaseOffsetPin(t *testing.T) { clockID: testClockID, devices: nil, pin: &nl.PinInfo{ - ClockId: testClockID, + ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentId: ppsDeviceID, State: nl.PinStateConnected}, + {ParentID: ppsDeviceID, State: nl.PinStateConnected}, }, }, expectedIndex: -1, diff --git a/pkg/dpll/dpll_test.go b/pkg/dpll/dpll_test.go index 2c8f7397..087174bf 100644 --- a/pkg/dpll/dpll_test.go +++ b/pkg/dpll/dpll_test.go @@ -41,12 +41,12 @@ type DpllTestCase struct { func getTestData(source event.EventSource, pinType uint32) []DpllTestCase { return []DpllTestCase{{ reply: &nl.DoDeviceGetReply{ - Id: id, + ID: id, ModuleName: moduleName, Mode: 1, ModeSupported: []uint32{0}, LockStatus: 3, //LHAQ, - ClockId: clockid, + ClockID: clockid, Type: 2, //1 pps 2 eec }, sourceLost: false, @@ -61,12 +61,12 @@ func getTestData(source event.EventSource, pinType uint32) []DpllTestCase { desc: fmt.Sprintf("1.LHAQ frequency status, unknown Phase status : pin %d ", pinType), }, { reply: &nl.DoDeviceGetReply{ - Id: id, + ID: id, ModuleName: moduleName, Mode: 1, ModeSupported: []uint32{0}, LockStatus: 3, //LHAQ, - ClockId: clockid, + ClockID: clockid, Type: 1, //1 pps 2 eec }, sourceLost: false, @@ -82,7 +82,7 @@ func getTestData(source event.EventSource, pinType uint32) []DpllTestCase { }, { reply: &nl.DoDeviceGetReply{ - Id: id, + ID: id, ModuleName: moduleName, Mode: 1, ModeSupported: []uint32{0}, @@ -93,7 +93,7 @@ func getTestData(source event.EventSource, pinType uint32) []DpllTestCase { return 4 // holdover } }(), // holdover, - ClockId: clockid, + ClockID: clockid, Type: pinType, //1 pps 2 eec }, sourceLost: true, @@ -128,12 +128,12 @@ func getTestData(source event.EventSource, pinType uint32) []DpllTestCase { }, { reply: &nl.DoDeviceGetReply{ - Id: id, + ID: id, ModuleName: moduleName, Mode: 1, ModeSupported: []uint32{0}, LockStatus: 4, // holdover, - ClockId: clockid, + ClockID: clockid, Type: pinType, //1 pps 2 eec }, sourceLost: true, @@ -177,7 +177,7 @@ func TestDpllConfig_MonitorProcessGNSS(t *testing.T) { // event has to be running before dpll is started eventProcessor := event.Init("node", false, "/tmp/go.sock", eChannel, closeChn, nil, nil, nil) d := dpll.NewDpll(clockid, 10, 2, 5, "ens01", - []event.EventSource{event.GNSS}, dpll.MOCK, map[string]map[string]string{}) + []event.EventSource{event.GNSS}, dpll.MOCK, map[string]map[string]string{}, 0, 0, 0) d.CmdInit() eventChannel := make(chan event.EventChannel, 10) go eventProcessor.ProcessEvents() @@ -220,7 +220,7 @@ func TestDpllConfig_MonitorProcessPPS(t *testing.T) { // event has to be running before dpll is started eventProcessor := event.Init("node", false, "/tmp/go.sock", eChannel, closeChn, nil, nil, nil) d := dpll.NewDpll(clockid, 10, 2, 5, "ens01", - []event.EventSource{event.GNSS}, dpll.MOCK, map[string]map[string]string{}) + []event.EventSource{event.GNSS}, dpll.MOCK, map[string]map[string]string{}, 0, 0, 0) d.CmdInit() eventChannel := make(chan event.EventChannel, 10) go eventProcessor.ProcessEvents() @@ -284,7 +284,7 @@ func TestSlopeAndTimer(t *testing.T) { } for _, tt := range testCase { d := dpll.NewDpll(100, tt.localMaxHoldoverOffSet, tt.localHoldoverTimeout, tt.maxInSpecOffset, - "test", []event.EventSource{}, dpll.MOCK, map[string]map[string]string{}) + "test", []event.EventSource{}, dpll.MOCK, map[string]map[string]string{}, 0, 0, 0) assert.Equal(t, tt.localMaxHoldoverOffSet, d.LocalMaxHoldoverOffSet, "localMaxHoldover offset") assert.Equal(t, tt.localHoldoverTimeout, d.LocalHoldoverTimeout, "Local holdover timeout") assert.Equal(t, tt.maxInSpecOffset, d.MaxInSpecOffset, "Max In Spec Offset") diff --git a/pkg/event/event.go b/pkg/event/event.go index 5502f1a5..dc10aad3 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -44,6 +44,11 @@ const ( CLOCK_QUALITY ValueType = "clock_quality" NETWORK_OPTION ValueType = "network_option" EEC_STATE = "eec_state" + LeadingSource ValueType = "leading_source" + InSyncConditionThreshold ValueType = "in_sync_condition_threshold" + InSyncConditionTimes ValueType = "in_sync_condition_times" + ToFreeRunThreshold ValueType = "to_freerun_threshold" + MaxInSpecOffset ValueType = "max_in_spec_offset" ) var valueTypeHelpTxt = map[ValueType]string{ From 51a932b5979532b868062df5ca661f8af30ac144 Mon Sep 17 00:00:00 2001 From: nocturnalastro Date: Thu, 5 Mar 2026 13:10:57 +0000 Subject: [PATCH 4/9] Remove pointer to pin commands Assisted-by: Cursor --- addons/intel/clock-chain.go | 15 ++++++++------- addons/intel/clock-chain_test.go | 14 +++++++------- addons/intel/dpllPins.go | 2 +- addons/intel/dpllPins_test.go | 2 +- addons/intel/mock_test.go | 14 +++++--------- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/addons/intel/clock-chain.go b/addons/intel/clock-chain.go index 59c883f8..10394c2a 100644 --- a/addons/intel/clock-chain.go +++ b/addons/intel/clock-chain.go @@ -232,7 +232,8 @@ func writeSysFs(path string, val string) error { return nil } -func (c *ClockChain) SetPinsControl(pins []PinControl) (*[]dpll.PinParentDeviceCtl, error) { +// SetPinsControl builds DPLL netlink commands for the given pins on the leading NIC. +func (c *ClockChain) SetPinsControl(pins []PinControl) ([]dpll.PinParentDeviceCtl, error) { pinCommands := []dpll.PinParentDeviceCtl{} for _, pinCtl := range pins { dpllPin := c.DpllPins.GetByLabel(pinCtl.Label, c.LeadingNIC.DpllClockID) @@ -242,12 +243,12 @@ func (c *ClockChain) SetPinsControl(pins []PinControl) (*[]dpll.PinParentDeviceC } pinCommands = append(pinCommands, SetPinControlData(*dpllPin, pinCtl.ParentControl)...) } - return &pinCommands, nil + return pinCommands, nil } // SetPinsControlForAllNICs sets pins across all NICs (leading + other NICs) // This is used specifically for initialization functions like SetPinDefaults -func (c *ClockChain) SetPinsControlForAllNICs(pins []PinControl) (*[]dpll.PinParentDeviceCtl, error) { +func (c *ClockChain) SetPinsControlForAllNICs(pins []PinControl) ([]dpll.PinParentDeviceCtl, error) { pinCommands := []dpll.PinParentDeviceCtl{} errs := make([]error, 0) @@ -266,7 +267,7 @@ func (c *ClockChain) SetPinsControlForAllNICs(pins []PinControl) (*[]dpll.PinPar } } - return &pinCommands, errors.Join(errs...) + return pinCommands, errors.Join(errs...) } // buildDirectionCmd checks if any parent device direction is changing. @@ -437,7 +438,7 @@ func (c *ClockChain) InitPinsTBC() error { if err != nil { glog.Error("failed to set pins control: ", err) } - *commands = append(*commands, *commandsGnss...) + commands = append(commands, commandsGnss...) err = BatchPinSet(commands) // even if there was an error we still need to refresh the pin state. @@ -603,14 +604,14 @@ func (c *ClockChain) SetPinDefaults() error { // BatchPinSet function pointer allows mocking of BatchPinSet var BatchPinSet = batchPinSet -func batchPinSet(commands *[]dpll.PinParentDeviceCtl) error { +func batchPinSet(commands []dpll.PinParentDeviceCtl) error { conn, err := dpll.Dial(nil) if err != nil { return fmt.Errorf("failed to dial DPLL: %v", err) } //nolint:errcheck defer conn.Close() - for _, command := range *commands { + for _, command := range commands { glog.Infof("DPLL pin command %#v", command) b, err := dpll.EncodePinControl(command) if err != nil { diff --git a/addons/intel/clock-chain_test.go b/addons/intel/clock-chain_test.go index f37b361e..446adb99 100644 --- a/addons/intel/clock-chain_test.go +++ b/addons/intel/clock-chain_test.go @@ -44,19 +44,19 @@ func Test_ProcessProfileTbcClockChain(t *testing.T) { assert.Equal(t, 0, mockPinConfig.actualPinSetCount) assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) assert.NotNil(t, mockPinSet.commands, "DPLL commands should have been issued") - assert.Greater(t, len(*mockPinSet.commands), 0, "should have DPLL pin commands") + assert.Greater(t, len(mockPinSet.commands), 0, "should have DPLL pin commands") // Test holdover entry mockPinSet.reset() err = clockChain.EnterHoldoverTBC() assert.NoError(t, err) - assert.Equal(t, 2, len(*mockPinSet.commands)) + assert.Equal(t, 2, len(mockPinSet.commands)) // Test holdover exit mockPinSet.reset() err = clockChain.EnterNormalTBC() assert.NoError(t, err) - assert.Equal(t, 2, len(*mockPinSet.commands)) + assert.Equal(t, 2, len(mockPinSet.commands)) // Ensure switching back to TGM resets any pins mockPinSet.reset() @@ -109,13 +109,13 @@ func Test_ProcessProfileTtscClockChain(t *testing.T) { mockPinSet.reset() err = clockChain.EnterHoldoverTBC() assert.NoError(t, err) - assert.Equal(t, 2, len(*mockPinSet.commands)) + assert.Equal(t, 2, len(mockPinSet.commands)) // Test holdover exit mockPinSet.reset() err = clockChain.EnterNormalTBC() assert.NoError(t, err) - assert.Equal(t, 2, len(*mockPinSet.commands)) + assert.Equal(t, 2, len(mockPinSet.commands)) } func Test_SetPinDefaults_AllNICs(t *testing.T) { @@ -163,14 +163,14 @@ func Test_SetPinDefaults_AllNICs(t *testing.T) { // SetPinDefaults configures 9 different pin types, and we have 3 NICs total // Each pin type should have a command for each NIC that has that pin - assert.Equal(t, len(*mockPinSet.commands), 27, "should have exactly 27 pin commands") + assert.Equal(t, len(mockPinSet.commands), 27, "should have exactly 27 pin commands") // Verify that commands include pins from multiple clock IDs clockIDsSeen := make(map[uint64]bool) pinLabelsSeen := make(map[string]bool) mockPins := ccData.DpllPins.(*mockedDPLLPins) - for _, cmd := range *mockPinSet.commands { + for _, cmd := range mockPinSet.commands { // Find which pin this command refers to by searching all pins for _, pin := range mockPins.pins { if pin.ID == cmd.ID { diff --git a/addons/intel/dpllPins.go b/addons/intel/dpllPins.go index 1220e7d8..8a5a3058 100644 --- a/addons/intel/dpllPins.go +++ b/addons/intel/dpllPins.go @@ -136,7 +136,7 @@ func (d *dpllPins) GetCommandsForPluginPinSet(clockID uint64, pinset pinSet) []d } func (d *dpllPins) ApplyPinCommands(commands []dpll.PinParentDeviceCtl) error { - err := BatchPinSet(&commands) + err := BatchPinSet(commands) // event if there was an error we still need to refresh the pin state. fetchErr := d.FetchPins() return errors.Join(err, fetchErr) diff --git a/addons/intel/dpllPins_test.go b/addons/intel/dpllPins_test.go index 880b7cd4..e4d99cac 100644 --- a/addons/intel/dpllPins_test.go +++ b/addons/intel/dpllPins_test.go @@ -459,5 +459,5 @@ func TestApplyPinCommands(t *testing.T) { err := DpllPins.ApplyPinCommands(cmds) assert.NoError(t, err) assert.NotNil(t, mockPinSet.commands) - assert.Len(t, *mockPinSet.commands, 1) + assert.Len(t, mockPinSet.commands, 1) } diff --git a/addons/intel/mock_test.go b/addons/intel/mock_test.go index 6afb1cb8..2e751f3c 100644 --- a/addons/intel/mock_test.go +++ b/addons/intel/mock_test.go @@ -16,20 +16,16 @@ import ( // mockBatchPinSet is a simple mock to unit-test pin set operations type mockBatchPinSet struct { - commands *[]dpll.PinParentDeviceCtl + commands []dpll.PinParentDeviceCtl } -func (m *mockBatchPinSet) mock(commands *[]dpll.PinParentDeviceCtl) error { - if m.commands == nil { - cmds := make([]dpll.PinParentDeviceCtl, 0) - m.commands = &cmds - } - *m.commands = append(*m.commands, *commands...) +func (m *mockBatchPinSet) mock(commands []dpll.PinParentDeviceCtl) error { + m.commands = append(m.commands, commands...) return nil } func (m *mockBatchPinSet) reset() { - m.commands = nil + m.commands = m.commands[:0] } func setupBatchPinSetMock() (*mockBatchPinSet, func()) { @@ -349,7 +345,7 @@ func (m *mockedDPLLPins) GetCommandsForPluginPinSet(clockID uint64, pinset pinSe } func (m *mockedDPLLPins) ApplyPinCommands(commands []dpll.PinParentDeviceCtl) error { - return BatchPinSet(&commands) + return BatchPinSet(commands) } func setupMockDPLLPins(pins ...*dpll.PinInfo) (*mockedDPLLPins, func()) { From bba0b3e0b390ca6b45cecc5399430bf39557e641 Mon Sep 17 00:00:00 2001 From: nocturnalastro Date: Mon, 9 Mar 2026 10:01:45 +0000 Subject: [PATCH 5/9] Add ts2phc.pin_index 1 when sysfs SMA pins are missing --- addons/intel/e810.go | 43 +++++++++++++++ addons/intel/e810_test.go | 110 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/addons/intel/e810.go b/addons/intel/e810.go index fe08a566..1f279a97 100644 --- a/addons/intel/e810.go +++ b/addons/intel/e810.go @@ -44,6 +44,7 @@ func OnPTPConfigChangeE810(data *interface{}, nodeProfile *ptpv1.PtpProfile) err glog.Info("calling onPTPConfigChange for e810 plugin") autoDetectGNSSSerialPort(nodeProfile) + checkPinIndex(nodeProfile) var e810Opts E810Opts var err error @@ -243,3 +244,45 @@ func pinSetHasSMAInput(pins pinSet) bool { } return false } + +func checkPinIndex(nodeProfile *ptpv1.PtpProfile) { + if nodeProfile.Ts2PhcConf == nil { + return + } + + profileName := "" + if nodeProfile.Name != nil { + profileName = *nodeProfile.Name + } + + lines := strings.Split(*nodeProfile.Ts2PhcConf, "\n") + result := make([]string, 0, len(lines)+1) + shouldAddPinIndex := false + for _, line := range lines { + trimedLine := strings.TrimSpace(line) + if strings.HasPrefix(trimedLine, "[") && strings.HasSuffix(trimedLine, "]") { + // We went through the previous entry and didn't find a pin index + if shouldAddPinIndex { + glog.Infof("Adding 'ts2phc.pin_index 1' to ts2phc for profile name %s", profileName) + result = append(result, "ts2phc.pin_index 1") + shouldAddPinIndex = false + } + + ifName := strings.TrimSpace(strings.TrimRight(strings.TrimLeft(trimedLine, "["), "]")) + if ifName != "global" && ifName != "nmea" && !hasSysfsSMAPins(ifName) { + shouldAddPinIndex = true + } + } + if strings.HasPrefix(trimedLine, "ts2phc.pin_index") || strings.HasPrefix(trimedLine, "ts2phc.pin_name") { + shouldAddPinIndex = false + } + result = append(result, line) + } + if shouldAddPinIndex { + glog.Infof("Adding 'ts2phc.pin_index 1' to ts2phc for profile name %s", profileName) + result = append(result, "ts2phc.pin_index 1") + } + + updatedTs2phcConfig := strings.Join(result, "\n") + nodeProfile.Ts2PhcConf = &updatedTs2phcConfig +} diff --git a/addons/intel/e810_test.go b/addons/intel/e810_test.go index 0903bfe4..445789b1 100644 --- a/addons/intel/e810_test.go +++ b/addons/intel/e810_test.go @@ -470,6 +470,116 @@ func TestDevicePins_DPLL_NoSMAInput_NoGNSSCommand(t *testing.T) { } } +func Test_checkPinIndex(t *testing.T) { + strPtr := func(s string) *string { return &s } + + tests := []struct { + name string + ts2phcConf *string + setupMock func(*MockFileSystem) + expectedConf *string + }{ + { + name: "nil Ts2PhcConf is a no-op", + ts2phcConf: nil, + expectedConf: nil, + }, + { + name: "interface section without pin_index and no SMA pins gets pin_index added", + ts2phcConf: strPtr("[global]\nts2phc.nmea_serialport /dev/gnss0\n[ens4f0]\nts2phc.extts_polarity rising"), + setupMock: func(m *MockFileSystem) { + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}, nil) + m.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + }, + expectedConf: strPtr("[global]\nts2phc.nmea_serialport /dev/gnss0\n[ens4f0]\nts2phc.extts_polarity rising\nts2phc.pin_index 1"), + }, + { + name: "interface section with existing pin_index is not duplicated", + ts2phcConf: strPtr("[global]\n[ens4f0]\nts2phc.pin_index 0\nts2phc.extts_polarity rising"), + setupMock: func(m *MockFileSystem) { + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}, nil) + m.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + }, + expectedConf: strPtr("[global]\n[ens4f0]\nts2phc.pin_index 0\nts2phc.extts_polarity rising"), + }, + { + name: "global section does not get pin_index", + ts2phcConf: strPtr("[global]\nts2phc.nmea_serialport /dev/gnss0"), + expectedConf: strPtr("[global]\nts2phc.nmea_serialport /dev/gnss0"), + }, + { + name: "nmea section does not get pin_index", + ts2phcConf: strPtr("[nmea]\nts2phc.master 1"), + expectedConf: strPtr("[nmea]\nts2phc.master 1"), + }, + { + name: "interface with SMA pins does not get pin_index", + ts2phcConf: strPtr("[ens4f0]\nts2phc.extts_polarity rising"), + setupMock: func(m *MockFileSystem) { + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}, nil) + m.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", []byte("0 1"), nil) + }, + expectedConf: strPtr("[ens4f0]\nts2phc.extts_polarity rising"), + }, + { + name: "multiple interfaces - pin_index added only where needed", + ts2phcConf: strPtr("[global]\nts2phc.nmea_serialport /dev/gnss0\n[ens4f0]\nts2phc.extts_polarity rising\n[ens4f1]\nts2phc.pin_index 0\nts2phc.extts_polarity rising"), + setupMock: func(m *MockFileSystem) { + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}, nil) + m.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + m.AllowReadDir("/sys/class/net/ens4f1/device/ptp/", []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}, nil) + m.AllowReadFile("/sys/class/net/ens4f1/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + }, + expectedConf: strPtr("[global]\nts2phc.nmea_serialport /dev/gnss0\n[ens4f0]\nts2phc.extts_polarity rising\nts2phc.pin_index 1\n[ens4f1]\nts2phc.pin_index 0\nts2phc.extts_polarity rising"), + }, + { + name: "empty config string is unchanged", + ts2phcConf: strPtr(""), + expectedConf: strPtr(""), + }, + { + name: "pin_index added to last section when at end of file", + ts2phcConf: strPtr("[global]\n[ens4f0]\nts2phc.extts_polarity rising"), + setupMock: func(m *MockFileSystem) { + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}, nil) + m.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + }, + expectedConf: strPtr("[global]\n[ens4f0]\nts2phc.extts_polarity rising\nts2phc.pin_index 1"), + }, + { + name: "hasSysfsSMAPins returns false when ReadDir fails", + ts2phcConf: strPtr("[ens4f0]\nts2phc.extts_polarity rising"), + setupMock: func(m *MockFileSystem) { + m.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", nil, fmt.Errorf("no such directory")) + }, + expectedConf: strPtr("[ens4f0]\nts2phc.extts_polarity rising\nts2phc.pin_index 1"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockFS, restoreFs := setupMockFS() + defer restoreFs() + if tt.setupMock != nil { + tt.setupMock(mockFS) + } + + profile := &ptpv1.PtpProfile{ + Ts2PhcConf: tt.ts2phcConf, + } + + checkPinIndex(profile) + + if tt.expectedConf == nil { + assert.Nil(t, profile.Ts2PhcConf) + } else { + assert.NotNil(t, profile.Ts2PhcConf) + assert.Equal(t, *tt.expectedConf, *profile.Ts2PhcConf) + } + }) + } +} + func Test_PopulateHwConfdigE810(t *testing.T) { p, d := E810("e810") data := (*d).(*E810PluginData) From 0630b3a28746cb3150eb0e02d6eaa73a825a9804 Mon Sep 17 00:00:00 2001 From: Micky Costa Date: Mon, 16 Mar 2026 14:31:14 +0000 Subject: [PATCH 6/9] Merge pull request #167 from nocturnalastro/set_pin_index Set pin index --- addons/intel/clockID.go | 3 +-- addons/intel/clockID_test.go | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/addons/intel/clockID.go b/addons/intel/clockID.go index e96cc774..4ca9366a 100644 --- a/addons/intel/clockID.go +++ b/addons/intel/clockID.go @@ -38,8 +38,7 @@ func getPCIClockID(device string) uint64 { var offset uint16 = pciConfigSpaceSize var id uint16 for { - // TODO: Add test for == case - if len(b) <= int(offset) { + if len(b) < int(offset)+pciExtendedCapabilityDataOffset { glog.Errorf("PCI config space too short (%d bytes) for device %s", len(b), device) return 0 } diff --git a/addons/intel/clockID_test.go b/addons/intel/clockID_test.go index a3e21ed4..678ec680 100644 --- a/addons/intel/clockID_test.go +++ b/addons/intel/clockID_test.go @@ -77,6 +77,17 @@ func Test_getPCIClockID(t *testing.T) { clockID = getPCIClockID("truncated") assert.Equal(t, notFound, clockID) mfs.VerifyAllCalls(t) + + // Config space barely holds capability ID but not the next-offset field; + // without a proper bounds check this would panic in Uint16(b[offset+2:]). + shortCap := make([]byte, pciConfigSpaceSize+2) + binary.LittleEndian.PutUint16(shortCap[pciConfigSpaceSize:], uint16(pciExtendedCapabilityDsnID+1)) + mfs.ExpectReadFile("/sys/class/net/short_cap/device/config", shortCap, nil) + assert.NotPanics(t, func() { + clockID = getPCIClockID("short_cap") + }) + assert.Equal(t, notFound, clockID) + mfs.VerifyAllCalls(t) } func Test_getClockIDByModule(t *testing.T) { From 51072e40f96eae1a3c1f9c00bd821e65dd5aa5b0 Mon Sep 17 00:00:00 2001 From: Vitaly Grinberg Date: Tue, 24 Mar 2026 15:02:08 +0200 Subject: [PATCH 7/9] Improve DPLL pin commands printing --- addons/intel/clock-chain.go | 2 +- pkg/dpll-netlink/dpll-uapi.go | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/addons/intel/clock-chain.go b/addons/intel/clock-chain.go index 10394c2a..34b282f2 100644 --- a/addons/intel/clock-chain.go +++ b/addons/intel/clock-chain.go @@ -612,7 +612,7 @@ func batchPinSet(commands []dpll.PinParentDeviceCtl) error { //nolint:errcheck defer conn.Close() for _, command := range commands { - glog.Infof("DPLL pin command %#v", command) + glog.Infof("DPLL pin command %s", command.String()) b, err := dpll.EncodePinControl(command) if err != nil { return err diff --git a/pkg/dpll-netlink/dpll-uapi.go b/pkg/dpll-netlink/dpll-uapi.go index e568400f..b88c6c90 100644 --- a/pkg/dpll-netlink/dpll-uapi.go +++ b/pkg/dpll-netlink/dpll-uapi.go @@ -322,6 +322,59 @@ func GetPinDirection(d uint32) string { return "" } +// String returns a concise debug representation aligned with PinParentDeviceHR +// field semantics (direction/state as names when known). +func (p PinControl) String() string { + var b strings.Builder + fmt.Fprintf(&b, "parentID=%d", p.PinParentID) + if p.Direction != nil { + if d := GetPinDirection(*p.Direction); d != "" { + fmt.Fprintf(&b, " direction=%s", d) + } else { + fmt.Fprintf(&b, " direction=%d", *p.Direction) + } + } + if p.Prio != nil { + fmt.Fprintf(&b, " prio=%d", *p.Prio) + } + if p.State != nil { + if s := GetPinState(*p.State); s != "" { + fmt.Fprintf(&b, " state=%s", s) + } else { + fmt.Fprintf(&b, " state=%d", *p.State) + } + } + return b.String() +} + +// String returns a concise debug representation of the pin parent device control request. +func (p PinParentDeviceCtl) String() string { + var b strings.Builder + b.WriteString("PinParentDeviceCtl{") + fmt.Fprintf(&b, "id=%d", p.ID) + if p.Frequency != nil { + fmt.Fprintf(&b, " frequency=%d", *p.Frequency) + } + if p.PhaseAdjust != nil { + fmt.Fprintf(&b, " phaseAdjust=%d", *p.PhaseAdjust) + } + if p.EsyncFrequency != nil { + fmt.Fprintf(&b, " esyncFrequency=%d", *p.EsyncFrequency) + } + if len(p.PinParentCtl) > 0 { + b.WriteString(" pinParent=[") + for i := range p.PinParentCtl { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(p.PinParentCtl[i].String()) + } + b.WriteString("]") + } + b.WriteString("}") + return b.String() +} + // Defines pin capabilities const ( PinCapNone = 0 From 3f5e2bf79d180bbcba5ad74e749e531488aee0c5 Mon Sep 17 00:00:00 2001 From: Vitaly Grinberg Date: Tue, 24 Mar 2026 16:31:59 +0200 Subject: [PATCH 8/9] add SDP22 channel assignment --- addons/intel/clock-chain.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/intel/clock-chain.go b/addons/intel/clock-chain.go index 34b282f2..b42ad86a 100644 --- a/addons/intel/clock-chain.go +++ b/addons/intel/clock-chain.go @@ -367,6 +367,11 @@ func (c *ClockChain) EnableE810Outputs() error { glog.Errorf("failed to set SMA2 pin via sysfs: %s", err) } } else { + // This is to assign channel 2 to SDP22. The period command below will fail without it + err = pinConfig.applyPinSet(c.LeadingNIC.Name, pinSet{"SDP22": "2 2"}) + if err != nil { + glog.Errorf("failed to set SDP22 pin via sysfs: %s", err) + } sma2Cmds := c.DpllPins.GetCommandsForPluginPinSet(c.LeadingNIC.DpllClockID, map[string]string{"SMA2": "2 2"}) err = c.DpllPins.ApplyPinCommands(sma2Cmds) if err != nil { From 246c78ba75441ecab7f678792ef38e44efc85ca0 Mon Sep 17 00:00:00 2001 From: Vitaly Grinberg Date: Wed, 25 Mar 2026 10:51:38 +0200 Subject: [PATCH 9/9] fix unit tests for pin compatibility Made-with: Cursor --- addons/intel/clock-chain_test.go | 7 ++--- addons/intel/e810_test.go | 2 +- addons/intel/mock_test.go | 26 ++++++++++++++++++- pkg/daemon/daemon.go | 44 ++++++++++++++++---------------- pkg/dpll/dpll_internal_test.go | 6 ++--- pkg/event/event.go | 32 +++++++++++------------ 6 files changed, 71 insertions(+), 46 deletions(-) diff --git a/addons/intel/clock-chain_test.go b/addons/intel/clock-chain_test.go index 446adb99..0aba0669 100644 --- a/addons/intel/clock-chain_test.go +++ b/addons/intel/clock-chain_test.go @@ -22,6 +22,7 @@ func Test_ProcessProfileTbcClockChain(t *testing.T) { // EnableE810Outputs is called for the leading NIC (ens4f0) - needs specific paths mockFS.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil) mockFS.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist) + // mock applyPinSet does not hit the filesystem; only the period write from EnableE810Outputs is real. mockFS.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil) mockPinConfig, restorePins := setupMockPinConfig() @@ -41,7 +42,7 @@ func Test_ProcessProfileTbcClockChain(t *testing.T) { assert.Equal(t, ClockTypeTBC, ccData.Type, "identified a wrong clock type") assert.Equal(t, uint64(5799633565432596414), ccData.LeadingNIC.DpllClockID, "identified a wrong clock ID ") assert.Equal(t, "ens4f1", ccData.LeadingNIC.UpstreamPort, "wrong upstream port") - assert.Equal(t, 0, mockPinConfig.actualPinSetCount) + assert.Equal(t, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS") assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) assert.NotNil(t, mockPinSet.commands, "DPLL commands should have been issued") assert.Greater(t, len(mockPinSet.commands), 0, "should have DPLL pin commands") @@ -102,7 +103,7 @@ func Test_ProcessProfileTtscClockChain(t *testing.T) { assert.Equal(t, uint64(5799633565432596414), ccData.LeadingNIC.DpllClockID, "identified a wrong clock ID ") assert.Equal(t, "ens4f1", ccData.LeadingNIC.UpstreamPort, "wrong upstream port") assert.NotNil(t, mockPinSet.commands, "Ensure some pins were set") - assert.Equal(t, 0, mockPinConfig.actualPinSetCount) + assert.Equal(t, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS") assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) // Test holdover entry @@ -146,7 +147,7 @@ func Test_SetPinDefaults_AllNICs(t *testing.T) { // Initialize the clock chain with multiple NICs err = OnPTPConfigChangeE810(nil, profile) assert.NoError(t, err) - assert.Equal(t, 0, mockPinConfig.actualPinSetCount) + assert.Equal(t, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS") assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) // Verify we have the expected clock chain structure diff --git a/addons/intel/e810_test.go b/addons/intel/e810_test.go index 445789b1..7271f9ae 100644 --- a/addons/intel/e810_test.go +++ b/addons/intel/e810_test.go @@ -126,7 +126,7 @@ func Test_ProcessProfileTBCNoPhaseInputs(t *testing.T) { err = p.OnPTPConfigChange(d, profile) assert.NoError(t, err) - assert.Equal(t, 0, mockPinConfig.actualPinSetCount) + assert.Equal(t, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS") assert.Equal(t, 0, mockPinConfig.actualPinFrqCount) // Verify that clockChain was initialized (SetPinDefaults is called as part of InitClockChain) diff --git a/addons/intel/mock_test.go b/addons/intel/mock_test.go index 2e751f3c..7e2c186a 100644 --- a/addons/intel/mock_test.go +++ b/addons/intel/mock_test.go @@ -325,7 +325,6 @@ func mockClockIDsFromProfile(mfs *MockFileSystem, profile *ptpv1.PtpProfile) { } } - type mockedDPLLPins struct { pins dpllPins } @@ -355,6 +354,30 @@ func setupMockDPLLPins(pins ...*dpll.PinInfo) (*mockedDPLLPins, func()) { return mock, func() { DpllPins = orig } } +// expandPinsForPluginYAMLCompatibility adds in-memory PinInfo clones so tests can keep legacy +// plugin YAML keys (SMA2, U.FL1, U.FL2) while dpll-pins.json reports boardLabel "SMA2/U.FL2" +// and no separate U.FL1 DPLL pin (tests only; production JSON and YAML unchanged). +func expandPinsForPluginYAMLCompatibility(pins []*dpll.PinInfo) []*dpll.PinInfo { + out := make([]*dpll.PinInfo, 0, len(pins)+32) + out = append(out, pins...) + for _, p := range pins { + if p.BoardLabel == "SMA2/U.FL2" { + s2 := *p + s2.BoardLabel = "SMA2" + out = append(out, &s2) + u2 := *p + u2.BoardLabel = "U.FL2" + out = append(out, &u2) + } + if p.BoardLabel == "SMA1" && p.ModuleName == "ice" { + u1 := *p + u1.BoardLabel = "U.FL1" + out = append(out, &u1) + } + } + return out +} + func setupMockDPLLPinsFromJSON(path string) (*mockedDPLLPins, func()) { //nolint: unparam // it may be used for other pin files in the future it doesn't make the code overly complex pins := []dpll.PinInfo{} data, err := os.ReadFile(path) @@ -368,6 +391,7 @@ func setupMockDPLLPinsFromJSON(path string) (*mockedDPLLPins, func()) { //nolint for i := range pins { ptrs[i] = &pins[i] } + ptrs = expandPinsForPluginYAMLCompatibility(ptrs) return setupMockDPLLPins(ptrs...) } diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index 46cfa17f..533ad241 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -779,30 +779,30 @@ func (dn *Daemon) applyNodePtpProfile(runID int, nodeProfile *ptpv1.PtpProfile) } else { eventSource = []event.EventSource{event.GNSS} } - // pass array of ifaces which has source + clockId - - // here we have multiple dpll objects identified by clock id - // depends on will be either PPS or GNSS, - // ONLY the one with GNSS dependency will go to HOLDOVER - var inSyncConditionTh uint64 = dpll.MaxInSpecOffset - var inSyncConditionTimes uint64 = 1 - sInSyncConditionTh, found1 := (*nodeProfile).PtpSettings["inSyncConditionThreshold"] - if found1 { - inSyncConditionTh, err = strconv.ParseUint(sInSyncConditionTh, 0, 64) - if err != nil { - return fmt.Errorf("failed to parse inSyncConditionThreshold: %s", err) + // pass array of ifaces which has source + clockId - + // here we have multiple dpll objects identified by clock id + // depends on will be either PPS or GNSS, + // ONLY the one with GNSS dependency will go to HOLDOVER + var inSyncConditionTh uint64 = dpll.MaxInSpecOffset + var inSyncConditionTimes uint64 = 1 + sInSyncConditionTh, found1 := (*nodeProfile).PtpSettings["inSyncConditionThreshold"] + if found1 { + inSyncConditionTh, err = strconv.ParseUint(sInSyncConditionTh, 0, 64) + if err != nil { + return fmt.Errorf("failed to parse inSyncConditionThreshold: %s", err) + } } - } - sInSyncConditionTim, found2 := (*nodeProfile).PtpSettings["inSyncConditionTimes"] - if found2 { - inSyncConditionTimes, err = strconv.ParseUint(sInSyncConditionTim, 0, 64) - if err != nil { - return fmt.Errorf("failed to parse inSyncConditionTimes: %s", err) + sInSyncConditionTim, found2 := (*nodeProfile).PtpSettings["inSyncConditionTimes"] + if found2 { + inSyncConditionTimes, err = strconv.ParseUint(sInSyncConditionTim, 0, 64) + if err != nil { + return fmt.Errorf("failed to parse inSyncConditionTimes: %s", err) + } } - } - dpllDaemon := dpll.NewDpll(clockId, localMaxHoldoverOffSet, localHoldoverTimeout, - maxInSpecOffset, iface.Name, eventSource, dpll.NONE, dn.GetPhaseOffsetPinFilter(nodeProfile), - // Used only in T-BC in-sync condition: - inSyncConditionTh, inSyncConditionTimes, 0) + dpllDaemon := dpll.NewDpll(clockId, localMaxHoldoverOffSet, localHoldoverTimeout, + maxInSpecOffset, iface.Name, eventSource, dpll.NONE, dn.GetPhaseOffsetPinFilter(nodeProfile), + // Used only in T-BC in-sync condition: + inSyncConditionTh, inSyncConditionTimes, 0) glog.Infof("depending on %s", dpllDaemon.DependsOn()) dpllDaemon.CmdInit() dprocess.depProcess = append(dprocess.depProcess, dpllDaemon) diff --git a/pkg/dpll/dpll_internal_test.go b/pkg/dpll/dpll_internal_test.go index 96223119..319256d8 100644 --- a/pkg/dpll/dpll_internal_test.go +++ b/pkg/dpll/dpll_internal_test.go @@ -60,7 +60,7 @@ func TestActivePhaseOffsetPin(t *testing.T) { pin: &nl.PinInfo{ ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentID: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, + {ParentID: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, }, }, expectedIndex: 0, @@ -112,8 +112,8 @@ func TestActivePhaseOffsetPin(t *testing.T) { pin: &nl.PinInfo{ ClockID: testClockID, ParentDevice: []nl.PinParentDevice{ - {ParentID: eecDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, - {ParentID: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, + {ParentID: eecDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, + {ParentID: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, }, }, expectedIndex: 1, diff --git a/pkg/event/event.go b/pkg/event/event.go index dc10aad3..ed5488c2 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -32,23 +32,23 @@ const ( STATE ValueType = "state" GPS_STATUS ValueType = "gnss_status" //Status ValueType = "status" - PHASE_STATUS ValueType = "phase_status" - FREQUENCY_STATUS ValueType = "frequency_status" - NMEA_STATUS ValueType = "nmea_status" - PROCESS_STATUS ValueType = "process_status" - PPS_STATUS ValueType = "pps_status" - GM_INTERFACE_UNKNOWN string = "unknown" - DEVICE ValueType = "device" - QL ValueType = "ql" - EXT_QL ValueType = "ext_ql" - CLOCK_QUALITY ValueType = "clock_quality" - NETWORK_OPTION ValueType = "network_option" - EEC_STATE = "eec_state" - LeadingSource ValueType = "leading_source" + PHASE_STATUS ValueType = "phase_status" + FREQUENCY_STATUS ValueType = "frequency_status" + NMEA_STATUS ValueType = "nmea_status" + PROCESS_STATUS ValueType = "process_status" + PPS_STATUS ValueType = "pps_status" + GM_INTERFACE_UNKNOWN string = "unknown" + DEVICE ValueType = "device" + QL ValueType = "ql" + EXT_QL ValueType = "ext_ql" + CLOCK_QUALITY ValueType = "clock_quality" + NETWORK_OPTION ValueType = "network_option" + EEC_STATE = "eec_state" + LeadingSource ValueType = "leading_source" InSyncConditionThreshold ValueType = "in_sync_condition_threshold" - InSyncConditionTimes ValueType = "in_sync_condition_times" - ToFreeRunThreshold ValueType = "to_freerun_threshold" - MaxInSpecOffset ValueType = "max_in_spec_offset" + InSyncConditionTimes ValueType = "in_sync_condition_times" + ToFreeRunThreshold ValueType = "to_freerun_threshold" + MaxInSpecOffset ValueType = "max_in_spec_offset" ) var valueTypeHelpTxt = map[ValueType]string{