diff --git a/addons/intel/clock-chain.go b/addons/intel/clock-chain.go index 8c3f32f7..b42ad86a 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,344 +165,474 @@ 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()) } 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) + if dpllPin == nil { + glog.Errorf("pin not found with label %s for clockID %d", pinCtl.Label, c.LeadingNIC.DpllClockID) + continue + } + pinCommands = append(pinCommands, SetPinControlData(*dpllPin, pinCtl.ParentControl)...) + } + 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) { pinCommands := []dpll.PinParentDeviceCtl{} + errs := make([]error, 0) + 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) + 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)...) } - pinCommand := SetPinControlData(dpllPin, pinCtl.ParentControl) - pinCommands = append(pinCommands, *pinCommand) } - return &pinCommands, nil + + return pinCommands, errors.Join(errs...) } -func SetPinControlData(pin dpll.PinInfo, control PinParentControl) *dpll.PinParentDeviceCtl { - Pin := dpll.PinParentDeviceCtl{ - Id: pin.Id, - PinParentCtl: make([]dpll.PinControl, 0), +// 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 + + 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 { + glog.Errorf("failed to set SMA2 pin via sysfs: %s", err) + } } else { - deviceDir := fmt.Sprintf("/sys/class/net/%s/device/ptp/", c.LeadingNIC.Name) - phcs, err := filesystem.ReadDir(deviceDir) + // 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 { - return fmt.Errorf("e810 failed to read " + deviceDir + ": " + err.Error()) + glog.Errorf("failed to set SDP22 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()) - } + 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) + for _, command := range commands { + glog.Infof("DPLL pin command %s", command.String()) 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..0aba0669 --- /dev/null +++ b/addons/intel/clock-chain_test.go @@ -0,0 +1,196 @@ +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) + // 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() + 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, 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") + + // 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, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS") + 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, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS") + 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..4ca9366a --- /dev/null +++ b/addons/intel/clockID.go @@ -0,0 +1,121 @@ +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 { + if len(b) < int(offset)+pciExtendedCapabilityDataOffset { + 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..678ec680 --- /dev/null +++ b/addons/intel/clockID_test.go @@ -0,0 +1,155 @@ +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) + + // 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) { + 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..8a5a3058 --- /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..e4d99cac --- /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..1f279a97 100644 --- a/addons/intel/e810.go +++ b/addons/intel/e810.go @@ -1,209 +1,167 @@ 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{} +var ( + clockChain ClockChainInterface = &ClockChain{DpllPins: DpllPins} -// 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"}, - } - - // Ublx command to disable SA messages - cfgMsgDisableSA := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-p", "CFG-MSG,0xf0,0x02,0"}, - } - // Ublx command to disable SV messages - cfgMsgDisableSV := E810UblxCmds{ - ReportOutput: false, - Args: []string{"-p", "CFG-MSG,0xf0,0x03,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 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") autoDetectGNSSSerialPort(nodeProfile) + checkPinIndex(nodeProfile) 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 +169,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 +186,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 +216,73 @@ 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:]) + return false } -func loadPins(path string) (*[]dpll_netlink.PinInfo, error) { - pins := &[]dpll_netlink.PinInfo{} - ptext, err := os.ReadFile(path) - if err != nil { - return pins, err +func checkPinIndex(nodeProfile *ptpv1.PtpProfile) { + if nodeProfile.Ts2PhcConf == nil { + return } - err = json.Unmarshal([]byte(ptext), pins) - return pins, err + + 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 new file mode 100644 index 00000000..7271f9ae --- /dev/null +++ b/addons/intel/e810_test.go @@ -0,0 +1,612 @@ +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, 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) + // 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_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) + 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..7e2c186a --- /dev/null +++ b/addons/intel/mock_test.go @@ -0,0 +1,404 @@ +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 { + m.commands = append(m.commands, commands...) + return nil +} + +func (m *mockBatchPinSet) reset() { + m.commands = m.commands[:0] +} + +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 } +} + +// 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) + 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] + } + ptrs = expandPinsForPluginYAMLCompatibility(ptrs) + 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..533ad241 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -783,8 +783,26 @@ func (dn *Daemon) applyNodePtpProfile(runID int, nodeProfile *ptpv1.PtpProfile) // 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)) + 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 acb300d3..b88c6c90 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,36 +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 = 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 { @@ -198,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 { @@ -214,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 { @@ -232,59 +322,118 @@ 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 + 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), }) } @@ -294,6 +443,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 979298e3..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,14 +149,26 @@ 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. 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 + // 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 } func (d *DpllConfig) InSpec() bool { @@ -151,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 @@ -164,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 @@ -227,7 +303,7 @@ func (d *DpllConfig) Name() string { // Stopped ... stopped func (d *DpllConfig) Stopped() bool { - //TODO implement me + // TODO implement me panic("implement me") } @@ -236,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 @@ -244,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 @@ -288,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, @@ -298,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 @@ -319,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 } @@ -327,26 +422,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 || p.Direction != nl.PinDirectionInput { + 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. @@ -354,27 +446,28 @@ 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 { - 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 } @@ -399,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) @@ -416,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() } @@ -482,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 } @@ -493,11 +593,13 @@ func (d *DpllConfig) MonitorDpllNetlink() { goto abort } + d.devices = replies + if d.nlUpdateState(replies, []*nl.PinInfo{}) { d.stateDecision() } - err = c.JoinGroup(mcastId) + err = c.JoinGroup(mcastID) if err != nil { goto abort } @@ -538,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: @@ -584,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)}) @@ -631,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: @@ -648,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 { @@ -673,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) @@ -696,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 @@ -708,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 @@ -735,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(), @@ -753,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) } @@ -812,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 @@ -850,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 @@ -877,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 } @@ -884,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 } @@ -894,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 } @@ -974,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 new file mode 100644 index 00000000..319256d8 --- /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, Direction: nl.PinDirectionInput}, + }, + }, + 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, Direction: nl.PinDirectionInput}, + {ParentID: ppsDeviceID, State: nl.PinStateConnected, Direction: nl.PinDirectionInput}, + }, + }, + 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") + }) + } +} 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..ed5488c2 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -32,18 +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" + 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" ) var valueTypeHelpTxt = map[ValueType]string{