2026-08-13 07:10:39 -07:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
bcmI2CWrite = 0xAC
|
|
|
|
|
|
bcmI2CRead = 0xAD
|
|
|
|
|
|
|
|
|
|
|
|
bcmMMDVendor uint16 = 0x1E
|
|
|
|
|
|
bcmRegCmd uint16 = 0x4005
|
|
|
|
|
|
bcmRegStatus uint16 = 0x4037
|
|
|
|
|
|
bcmRegData1 uint16 = 0x4038
|
|
|
|
|
|
|
|
|
|
|
|
bcmStInProgress uint16 = 0x0002
|
|
|
|
|
|
bcmStPass uint16 = 0x0004
|
|
|
|
|
|
bcmStError uint16 = 0x0008
|
|
|
|
|
|
bcmStBusy uint16 = 0xBBBB
|
|
|
|
|
|
|
|
|
|
|
|
bcmCmdGetPairSwap uint16 = 0x8000
|
|
|
|
|
|
bcmCmdSetEEEMode uint16 = 0x8009
|
2026-08-13 08:04:26 -07:00
|
|
|
|
bcmCmdSetJumbo uint16 = 0x801C
|
2026-08-13 07:10:39 -07:00
|
|
|
|
|
|
|
|
|
|
bcmRegECDCtrl uint16 = 0x4006
|
|
|
|
|
|
bcmRegECDResult uint16 = 0xA896
|
|
|
|
|
|
bcmRegECDLen uint16 = 0xA897
|
|
|
|
|
|
|
|
|
|
|
|
bcmPHYIDHi = 0x3590
|
|
|
|
|
|
bcmPHYIDLo = 0x5081
|
|
|
|
|
|
|
2026-08-13 14:04:16 -07:00
|
|
|
|
bcmCmdResidentTemp uint16 = 0x0031
|
|
|
|
|
|
|
2026-08-13 10:19:32 -07:00
|
|
|
|
bcmReadDelayUs = 3000
|
|
|
|
|
|
bcmRetryDelayUs = 10000
|
2026-08-13 07:10:39 -07:00
|
|
|
|
|
2026-08-13 14:04:16 -07:00
|
|
|
|
bcmWindow = 3400 * time.Millisecond
|
|
|
|
|
|
bcmWindowFit = 100 * time.Millisecond
|
|
|
|
|
|
bcmFlipPoll = 10 * time.Millisecond
|
|
|
|
|
|
bcmFlipWait = 5 * time.Second
|
|
|
|
|
|
|
2026-08-13 10:49:01 -07:00
|
|
|
|
bcmStatusPoll = 100 * time.Millisecond
|
|
|
|
|
|
// Covers the handler's documented 2 s freeze during 10GBASE-T training.
|
|
|
|
|
|
bcmStatusTimeout = 3 * time.Second
|
2026-08-13 08:26:22 -07:00
|
|
|
|
|
2026-08-13 07:10:39 -07:00
|
|
|
|
ecdPoll = 200 * time.Millisecond
|
|
|
|
|
|
ecdDeadline = 50 * time.Second
|
|
|
|
|
|
|
|
|
|
|
|
pairIdentityMap = 0xE4
|
2026-08-14 23:35:19 -07:00
|
|
|
|
|
|
|
|
|
|
fsVendorPN = "SFP-10G-T-100"
|
|
|
|
|
|
wiitekVendorPN = "UF-RJ45-10G-100"
|
2026-08-13 07:10:39 -07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
pairOK = 1
|
|
|
|
|
|
pairOpen = 2
|
|
|
|
|
|
pairShort = 3
|
|
|
|
|
|
pairXtalk = 4
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var pairVerdicts = map[int]string{
|
|
|
|
|
|
pairOK: "ok", pairOpen: "OPEN", pairShort: "SHORT", pairXtalk: "XTALK",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
type sff struct {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
ifname string
|
|
|
|
|
|
path string
|
|
|
|
|
|
|
2026-08-13 16:32:14 -07:00
|
|
|
|
// Every transport touch happens on the loop goroutine: requests execute
|
2026-08-14 23:35:19 -07:00
|
|
|
|
// one at a time, each admitted by the module type's own gate first.
|
|
|
|
|
|
reqs chan func()
|
|
|
|
|
|
admit func()
|
2026-08-13 14:04:16 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func openSFF(ifname string) (*sff, error) {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
devLink, err := os.Readlink("/sys/class/net/" + ifname + "/device")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("%s: %w", ifname, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
drv, err := ifDriver(ifname)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("%s: %w", ifname, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if drv != "ixgbe" {
|
|
|
|
|
|
return nil, fmt.Errorf("%s: no module I2C transport for driver %s", ifname, drv)
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
s := &sff{
|
2026-08-13 07:10:39 -07:00
|
|
|
|
ifname: ifname,
|
|
|
|
|
|
path: "/sys/kernel/debug/ixgbe/" + filepath.Base(devLink) + "/sff_i2c",
|
2026-08-13 16:32:14 -07:00
|
|
|
|
reqs: make(chan func()),
|
2026-08-14 23:35:19 -07:00
|
|
|
|
admit: func() {},
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if _, err := os.Stat(s.path); err != nil {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
return nil, fmt.Errorf("%s: %w (patched ixgbe?)", ifname, err)
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
go s.loop()
|
|
|
|
|
|
return s, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *sff) loop() {
|
|
|
|
|
|
defer holdPanic()
|
|
|
|
|
|
for fn := range s.reqs {
|
|
|
|
|
|
s.admit()
|
|
|
|
|
|
fn()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *sff) exec(fn func()) {
|
|
|
|
|
|
done := make(chan struct{})
|
|
|
|
|
|
s.reqs <- func() { fn(); close(done) }
|
|
|
|
|
|
<-done
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func (s *sff) name() string { return s.ifname }
|
|
|
|
|
|
|
|
|
|
|
|
func (s *sff) op(cmd string) (string, error) {
|
|
|
|
|
|
fd, err := unix.Open(s.path, unix.O_RDWR, 0)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
return "", fmt.Errorf("%s: %w", s.path, err)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
defer unix.Close(fd)
|
|
|
|
|
|
if _, err := unix.Write(fd, []byte(cmd)); err != nil {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
return "", fmt.Errorf("%s %q: %w", s.ifname, cmd, err)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
buf := make([]byte, 256)
|
|
|
|
|
|
n, err := unix.Read(fd, buf)
|
|
|
|
|
|
if err != nil {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
return "", fmt.Errorf("%s %q: %w", s.ifname, cmd, err)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
resp := strings.TrimSpace(string(buf[:n]))
|
|
|
|
|
|
if !strings.HasPrefix(resp, "ok") {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
return "", fmt.Errorf("%s %q: %s", s.ifname, cmd, resp)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
return strings.TrimSpace(resp[2:]), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func parseHexBytes(s string, n int) ([]byte, error) {
|
|
|
|
|
|
fields := strings.Fields(s)
|
|
|
|
|
|
if len(fields) != n {
|
|
|
|
|
|
return nil, fmt.Errorf("want %d bytes, got %q", n, s)
|
|
|
|
|
|
}
|
|
|
|
|
|
out := make([]byte, n)
|
|
|
|
|
|
for i, f := range fields {
|
|
|
|
|
|
var v byte
|
|
|
|
|
|
if _, err := fmt.Sscanf(f, "%x", &v); err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("byte %q in %q", f, s)
|
|
|
|
|
|
}
|
|
|
|
|
|
out[i] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
return out, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 08:04:26 -07:00
|
|
|
|
// A single bus hold; split write/read ops would let the driver's own SFP
|
|
|
|
|
|
// traffic consume the bridge's pending read.
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func (s *sff) compound(waddr, raddr byte, delayUs, n int, wdata []byte) ([]byte, error) {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
var sb strings.Builder
|
|
|
|
|
|
fmt.Fprintf(&sb, "x %02x %02x %d %x", waddr, raddr, delayUs, n)
|
|
|
|
|
|
for _, v := range wdata {
|
|
|
|
|
|
fmt.Fprintf(&sb, " %02x", v)
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
resp, err := s.op(sb.String())
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return parseHexBytes(resp, n)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func (s *sff) eeprom(off byte, n int) ([]byte, error) {
|
|
|
|
|
|
return s.compound(0xA0, 0xA1, 500, n, []byte{off})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *sff) vendorPN() (string, error) {
|
|
|
|
|
|
pn, err := s.eeprom(40, 16)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.TrimSpace(string(pn)), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type mdioDev interface {
|
|
|
|
|
|
name() string
|
|
|
|
|
|
exec(func())
|
|
|
|
|
|
mdioRead(devad, reg uint16) (uint16, error)
|
|
|
|
|
|
mdioWrite(devad, reg, val uint16) error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type phyDev interface {
|
|
|
|
|
|
mdioDev
|
|
|
|
|
|
identify() (string, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type snrSource interface {
|
|
|
|
|
|
snrMargins() ([4]float64, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PMA 1.1 latches low, so the first read reports any drop since it was last
|
|
|
|
|
|
// read and the second reports the wire as it is now.
|
|
|
|
|
|
func devLinkUp(d mdioDev) (up bool, raw uint16, err error) {
|
|
|
|
|
|
d.exec(func() {
|
|
|
|
|
|
if _, err = d.mdioRead(1, 1); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if raw, err = d.mdioRead(1, 1); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
up = raw&0x0004 != 0
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func devPCSLatch(d mdioDev) (blocks, ber uint64, raw uint16, err error) {
|
|
|
|
|
|
d.exec(func() {
|
|
|
|
|
|
if raw, err = d.mdioRead(3, 33); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
blocks, ber = uint64(raw&0xFF), uint64((raw>>8)&0x3F)
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func devFastRetrain(d mdioDev) (count, raw uint16, err error) {
|
|
|
|
|
|
d.exec(func() {
|
|
|
|
|
|
if raw, err = d.mdioRead(1, 147); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
count = raw >> 11
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AN enable is forced alongside the restart: the ECD can leave the BCM with
|
|
|
|
|
|
// 7.0.12 cleared (proven live — no AN pulses, both ends deaf, link down until
|
|
|
|
|
|
// power cycle), and a bare restart bit preserves the cleared enable.
|
|
|
|
|
|
func devRestartAN(d mdioDev) (err error) {
|
|
|
|
|
|
d.exec(func() {
|
|
|
|
|
|
var v uint16
|
|
|
|
|
|
if v, err = d.mdioRead(7, 0); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
err = d.mdioWrite(7, 0, v|0x1200)
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func devEEEAdvert(d mdioDev) (v uint16, err error) {
|
|
|
|
|
|
d.exec(func() { v, err = d.mdioRead(7, 60) })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type bcm struct {
|
|
|
|
|
|
*sff
|
|
|
|
|
|
windowEnd time.Time
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func newBCM(t *sff) *bcm {
|
|
|
|
|
|
b := &bcm{sff: t}
|
|
|
|
|
|
t.exec(func() { t.admit = b.window })
|
|
|
|
|
|
return b
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The firmware's internal temp poll serves stale bridge reads for ~50 ms
|
|
|
|
|
|
// around it; work stays inside 3.4 s of an observed poll. A resident 0x0031 at
|
|
|
|
|
|
// expiry means the phase is unknown, so re-lock: arm, then take the true edge.
|
|
|
|
|
|
// Each taken edge immediately re-arms — the one CMD write per window lands at
|
|
|
|
|
|
// the start of the quiet period, maximally far from the next poll (writes near
|
|
|
|
|
|
// the poll are the µC-wedge risk), and every later expiry reads the phase
|
|
|
|
|
|
// without writing.
|
|
|
|
|
|
func (b *bcm) window() {
|
|
|
|
|
|
if time.Now().Add(bcmWindowFit).Before(b.windowEnd) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
armed := false
|
|
|
|
|
|
deadline := time.Now().Add(bcmFlipWait)
|
|
|
|
|
|
for {
|
|
|
|
|
|
v, err := b.mdioRead(bcmMMDVendor, bcmRegCmd)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("%s: heartbeat poll: %v", b.ifname, err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if v == bcmCmdResidentTemp {
|
|
|
|
|
|
if armed {
|
|
|
|
|
|
b.windowEnd = time.Now().Add(bcmWindow)
|
|
|
|
|
|
b.rearm()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
b.rearm()
|
|
|
|
|
|
armed = true
|
|
|
|
|
|
deadline = time.Now().Add(bcmFlipWait)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
armed = true
|
|
|
|
|
|
if time.Now().After(deadline) {
|
|
|
|
|
|
b.windowEnd = time.Now().Add(bcmWindow)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(bcmFlipPoll)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (b *bcm) rearm() {
|
|
|
|
|
|
if _, err := b.waitStatus(func(st uint16) bool {
|
|
|
|
|
|
return st != bcmStInProgress && st != bcmStBusy
|
|
|
|
|
|
}); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("%s: rearm: %v", b.ifname, err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := b.mdioWrite(bcmMMDVendor, bcmRegCmd, bcmCmdGetPairSwap); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("%s: rearm: %v", b.ifname, err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := b.waitStatus(func(st uint16) bool {
|
|
|
|
|
|
return st == bcmStPass || st == bcmStError
|
|
|
|
|
|
}); err != nil {
|
|
|
|
|
|
panic(fmt.Sprintf("%s: rearm: %v", b.ifname, err))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func (b *bcm) mdioReadDelay(devad, reg uint16, delayUs int) (uint16, error) {
|
|
|
|
|
|
d, err := b.compound(bcmI2CWrite, bcmI2CRead, delayUs, 2,
|
|
|
|
|
|
[]byte{0x20 | byte(devad), byte(reg >> 8), byte(reg)})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return uint16(d[0])<<8 | uint16(d[1]), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 10:19:32 -07:00
|
|
|
|
// 0x0000 is also the bridge's not-ready signature, so a zero is read again at
|
|
|
|
|
|
// a longer delay before being believed.
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func (b *bcm) mdioRead(devad, reg uint16) (uint16, error) {
|
|
|
|
|
|
v, err := b.mdioReadDelay(devad, reg, bcmReadDelayUs)
|
2026-08-13 10:19:32 -07:00
|
|
|
|
if err != nil || v != 0 {
|
|
|
|
|
|
return v, err
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-13 10:19:32 -07:00
|
|
|
|
return b.mdioReadDelay(devad, reg, bcmRetryDelayUs)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (b *bcm) mdioWrite(devad, reg, val uint16) error {
|
|
|
|
|
|
_, err := b.op(fmt.Sprintf("w %02x %02x %02x %02x %02x %02x",
|
|
|
|
|
|
bcmI2CWrite, byte(devad), byte(reg>>8), byte(reg), byte(val>>8), byte(val)))
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 10:49:01 -07:00
|
|
|
|
// The datasheet's completion handshake: poll STATUS on its 100 ms cadence
|
|
|
|
|
|
// until the wanted state, bounded by a deadline.
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func (b *bcm) waitStatus(want func(uint16) bool) (uint16, error) {
|
2026-08-13 10:49:01 -07:00
|
|
|
|
deadline := time.Now().Add(bcmStatusTimeout)
|
|
|
|
|
|
for {
|
|
|
|
|
|
st, err := b.mdioRead(bcmMMDVendor, bcmRegStatus)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if want(st) {
|
|
|
|
|
|
return st, nil
|
|
|
|
|
|
}
|
2026-08-13 10:49:01 -07:00
|
|
|
|
if time.Now().After(deadline) {
|
|
|
|
|
|
return 0, fmt.Errorf("%s: command handler stuck, status %#04x", b.ifname, st)
|
|
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
time.Sleep(bcmStatusPoll)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 10:32:24 -07:00
|
|
|
|
// GETs must be invoked bare (pre-writing any DATA register leaves the handler
|
|
|
|
|
|
// executing as a no-op); SETs must pass their full parameter set (the handler
|
2026-08-13 10:49:01 -07:00
|
|
|
|
// executes stale DATA).
|
2026-08-13 16:32:14 -07:00
|
|
|
|
func (b *bcm) command(code uint16, params ...uint16) (data [5]uint16, err error) {
|
|
|
|
|
|
b.exec(func() {
|
|
|
|
|
|
if _, err = b.waitStatus(func(st uint16) bool {
|
|
|
|
|
|
return st != bcmStInProgress && st != bcmStBusy
|
|
|
|
|
|
}); err != nil {
|
|
|
|
|
|
return
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-13 16:32:14 -07:00
|
|
|
|
for i, p := range params {
|
|
|
|
|
|
if err = b.mdioWrite(bcmMMDVendor, bcmRegData1+uint16(i), p); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-13 10:19:32 -07:00
|
|
|
|
}
|
2026-08-13 16:32:14 -07:00
|
|
|
|
if err = b.mdioWrite(bcmMMDVendor, bcmRegCmd, code); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
var st uint16
|
|
|
|
|
|
if st, err = b.waitStatus(func(st uint16) bool {
|
|
|
|
|
|
return st == bcmStPass || st == bcmStError
|
|
|
|
|
|
}); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if st == bcmStError {
|
|
|
|
|
|
err = fmt.Errorf("%s: command %#04x returned ERROR", b.ifname, code)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(params) > 0 {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := range data {
|
|
|
|
|
|
if data[i], err = b.mdioRead(bcmMMDVendor, bcmRegData1+uint16(i)); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 16:32:14 -07:00
|
|
|
|
func (b *bcm) identify() (ident string, err error) {
|
|
|
|
|
|
b.exec(func() {
|
|
|
|
|
|
var hi, lo uint16
|
|
|
|
|
|
if hi, err = b.mdioRead(1, 2); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if lo, err = b.mdioRead(1, 3); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if hi != bcmPHYIDHi || lo != bcmPHYIDLo {
|
|
|
|
|
|
err = fmt.Errorf("%s: PHY ID %#04x:%#04x, want %#04x:%#04x",
|
|
|
|
|
|
b.ifname, hi, lo, bcmPHYIDHi, bcmPHYIDLo)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
var sn []byte
|
|
|
|
|
|
if sn, err = b.eeprom(68, 16); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
ident = "BCM84891L sn " + strings.TrimSpace(string(sn))
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (b *bcm) forceEEEOff() error {
|
|
|
|
|
|
_, err := b.command(bcmCmdSetEEEMode, 0x0000, 0x0000, 0x7A12, 0x0480, 0x0000)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 08:04:26 -07:00
|
|
|
|
func (b *bcm) forceJumbo() error {
|
|
|
|
|
|
_, err := b.command(bcmCmdSetJumbo, 1, 0, 0, 0, 0)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 14:23:57 -07:00
|
|
|
|
// Left to AN, master/slave is a per-training lottery and each training's DSP
|
|
|
|
|
|
// convergence moves per-pair SNR by up to ~3.6 dB; pinned roles at least keep
|
|
|
|
|
|
// every session measured under identical conditions.
|
2026-08-13 16:32:14 -07:00
|
|
|
|
func (b *bcm) forceRole(master bool) (err error) {
|
|
|
|
|
|
b.exec(func() {
|
|
|
|
|
|
var v uint16
|
|
|
|
|
|
if v, err = b.mdioRead(7, 32); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
v |= 0x8000
|
|
|
|
|
|
if master {
|
|
|
|
|
|
v |= 0x4000
|
|
|
|
|
|
} else {
|
|
|
|
|
|
v &^= 0x4000
|
|
|
|
|
|
}
|
|
|
|
|
|
err = b.mdioWrite(7, 32, v)
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
2026-08-13 14:23:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func (b *bcm) pairMap() (byte, error) {
|
|
|
|
|
|
d, err := b.command(bcmCmdGetPairSwap)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return byte(d[1]), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
const (
|
|
|
|
|
|
rbI2CWrite = 0xA2
|
|
|
|
|
|
rbI2CRead = 0xA3
|
|
|
|
|
|
|
|
|
|
|
|
rbOffPassword byte = 0x7B
|
|
|
|
|
|
rbOffPage byte = 0x7F
|
|
|
|
|
|
rbOffCmd byte = 0x80
|
|
|
|
|
|
rbOffDevad byte = 0x81
|
|
|
|
|
|
rbOffValHi byte = 0x84
|
|
|
|
|
|
rbOffPartNum byte = 0xFA
|
|
|
|
|
|
|
|
|
|
|
|
rbPageMailbox byte = 3
|
|
|
|
|
|
|
|
|
|
|
|
rbCmdWrite byte = 0x01
|
|
|
|
|
|
rbCmdRead byte = 0x02
|
|
|
|
|
|
rbCmdDone byte = 0x04
|
|
|
|
|
|
|
|
|
|
|
|
rbReadDelayUs = 500
|
|
|
|
|
|
rbCmdPoll = 20 * time.Millisecond
|
|
|
|
|
|
// Matches the BCM allowance for a handler frozen by 10GBASE-T training.
|
|
|
|
|
|
rbCmdTimeout = 3 * time.Second
|
|
|
|
|
|
|
|
|
|
|
|
rbPHYIDHi uint16 = 0x002B
|
|
|
|
|
|
rbPHYIDLo uint16 = 0x0BF4
|
|
|
|
|
|
|
|
|
|
|
|
// IEEE margins land near 7-9 dB on a healthy short cable; far outside is
|
|
|
|
|
|
// another register's data.
|
|
|
|
|
|
rbGhostLow = -10.0
|
|
|
|
|
|
rbGhostHigh = 25.0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Only the registers proven safe on this PHY (docs/modules/wiitek/): single
|
|
|
|
|
|
// reads in the vendor windows brick the µC permanently, so everything else
|
|
|
|
|
|
// refuses before touching hardware.
|
|
|
|
|
|
var rbReadSafe = map[uint16]map[uint16]bool{
|
|
|
|
|
|
1: {1: true, 2: true, 3: true, 133: true, 134: true, 135: true, 136: true, 147: true},
|
|
|
|
|
|
3: {32: true, 33: true},
|
|
|
|
|
|
7: {0: true, 33: true, 60: true},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var rbWriteSafe = map[uint16]map[uint16]bool{
|
|
|
|
|
|
7: {0: true},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type rollball struct {
|
|
|
|
|
|
*sff
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *rollball) i2cWrite(off byte, data ...byte) error {
|
|
|
|
|
|
var sb strings.Builder
|
|
|
|
|
|
fmt.Fprintf(&sb, "w %02x %02x", rbI2CWrite, off)
|
|
|
|
|
|
for _, v := range data {
|
|
|
|
|
|
fmt.Fprintf(&sb, " %02x", v)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
_, err := r.op(sb.String())
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *rollball) i2cRead(off byte, n int) ([]byte, error) {
|
|
|
|
|
|
return r.compound(rbI2CWrite, rbI2CRead, rbReadDelayUs, n, []byte{off})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *rollball) unlock() error {
|
|
|
|
|
|
if err := r.i2cWrite(rbOffPage, rbPageMailbox); err != nil {
|
|
|
|
|
|
return err
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
return r.i2cWrite(rbOffPassword, 0xFF, 0xFF, 0xFF, 0xFF)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func (r *rollball) mbox(cmd byte, devad, reg, val uint16) error {
|
|
|
|
|
|
if err := r.unlock(); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := r.i2cWrite(rbOffDevad, byte(devad), byte(reg>>8), byte(reg)); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if cmd == rbCmdWrite {
|
|
|
|
|
|
if err := r.i2cWrite(rbOffValHi, byte(val>>8), byte(val)); err != nil {
|
|
|
|
|
|
return err
|
2026-08-13 16:32:14 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
}
|
|
|
|
|
|
if err := r.i2cWrite(rbOffCmd, cmd); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
deadline := time.Now().Add(rbCmdTimeout)
|
|
|
|
|
|
for {
|
|
|
|
|
|
d, err := r.i2cRead(rbOffCmd, 1)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if d[0] == rbCmdDone {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if time.Now().After(deadline) {
|
|
|
|
|
|
return fmt.Errorf("%s: mailbox %d.%#04x stuck at %#02x", r.ifname, devad, reg, d[0])
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(rbCmdPoll)
|
|
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func rbGuard(safe map[uint16]map[uint16]bool, ifname, what string, devad, reg uint16) {
|
|
|
|
|
|
if !safe[devad][reg] {
|
|
|
|
|
|
panic(fmt.Sprintf("%s: refusing MDIO %s %d.%#04x: outside the proven-safe set",
|
|
|
|
|
|
ifname, what, devad, reg))
|
|
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func (r *rollball) mdioRead(devad, reg uint16) (uint16, error) {
|
|
|
|
|
|
rbGuard(rbReadSafe, r.ifname, "read", devad, reg)
|
|
|
|
|
|
if err := r.mbox(rbCmdRead, devad, reg, 0); err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
d, err := r.i2cRead(rbOffValHi, 2)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return uint16(d[0])<<8 | uint16(d[1]), nil
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func (r *rollball) mdioWrite(devad, reg, val uint16) error {
|
|
|
|
|
|
rbGuard(rbWriteSafe, r.ifname, "write", devad, reg)
|
|
|
|
|
|
return r.mbox(rbCmdWrite, devad, reg, val)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *rollball) identify() (ident string, err error) {
|
|
|
|
|
|
r.exec(func() {
|
|
|
|
|
|
var hi, lo uint16
|
|
|
|
|
|
if hi, err = r.mdioRead(1, 2); err != nil {
|
2026-08-13 16:32:14 -07:00
|
|
|
|
return
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if lo, err = r.mdioRead(1, 3); err != nil {
|
2026-08-13 16:32:14 -07:00
|
|
|
|
return
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if hi != rbPHYIDHi || lo != rbPHYIDLo {
|
|
|
|
|
|
err = fmt.Errorf("%s: PHY ID %#04x:%#04x, want %#04x:%#04x",
|
|
|
|
|
|
r.ifname, hi, lo, rbPHYIDHi, rbPHYIDLo)
|
|
|
|
|
|
return
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if err = r.unlock(); err != nil {
|
2026-08-13 16:32:14 -07:00
|
|
|
|
return
|
2026-08-13 10:49:01 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
var part []byte
|
|
|
|
|
|
if part, err = r.i2cRead(rbOffPartNum, 1); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
var sn []byte
|
|
|
|
|
|
if sn, err = r.eeprom(68, 16); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
ident = fmt.Sprintf("CUX3610 sn %s (A2.250=%d)", strings.TrimSpace(string(sn)), part[0])
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *rollball) snrMargins() (out [4]float64, err error) {
|
|
|
|
|
|
r.exec(func() {
|
|
|
|
|
|
for i := range out {
|
|
|
|
|
|
var v uint16
|
|
|
|
|
|
if v, err = r.mdioRead(1, uint16(133+i)); err != nil {
|
2026-08-13 16:32:14 -07:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
m := (float64(v) - 0x8000) / 10
|
|
|
|
|
|
if m < rbGhostLow || m > rbGhostHigh {
|
|
|
|
|
|
panic(fmt.Sprintf("%s: ghost SNR margin %.1f dB (1.%d=%#04x)", r.ifname, m, 133+i, v))
|
|
|
|
|
|
}
|
|
|
|
|
|
out[i] = m
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-13 16:32:14 -07:00
|
|
|
|
})
|
|
|
|
|
|
return
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
phyInterval = time.Second
|
|
|
|
|
|
phyStale = 5 * time.Second
|
|
|
|
|
|
phyMaxDark = 30
|
|
|
|
|
|
linkWaitSpan = 25 * time.Second
|
|
|
|
|
|
linkWaitPoll = time.Second
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
snrGoodMargin = 3.0
|
|
|
|
|
|
snrWarnMargin = 1.0
|
2026-08-13 07:10:39 -07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type phyModule struct {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
dev phyDev
|
2026-08-13 10:49:01 -07:00
|
|
|
|
busy atomic.Bool
|
2026-08-13 07:10:39 -07:00
|
|
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
|
sampled bool
|
|
|
|
|
|
lastOK time.Time
|
|
|
|
|
|
link bool
|
|
|
|
|
|
haveSNR bool
|
2026-08-14 23:35:19 -07:00
|
|
|
|
margins [4]float64
|
2026-08-13 07:10:39 -07:00
|
|
|
|
blocks uint64
|
|
|
|
|
|
ber uint64
|
|
|
|
|
|
retrains uint64
|
|
|
|
|
|
recentDelta uint64
|
|
|
|
|
|
primed bool
|
|
|
|
|
|
retrainCount uint16
|
2026-08-13 16:32:14 -07:00
|
|
|
|
notes []string
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 10:49:01 -07:00
|
|
|
|
// Silent while a measure owns the module.
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func (m *phyModule) poll() error {
|
2026-08-13 10:19:32 -07:00
|
|
|
|
if m.busy.Load() {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
link, linkRaw, err := devLinkUp(m.dev)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
var margins [4]float64
|
|
|
|
|
|
haveSNR := false
|
|
|
|
|
|
if src, ok := m.dev.(snrSource); ok && link {
|
|
|
|
|
|
if margins, err = src.snrMargins(); err != nil {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
haveSNR = true
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
blocks, ber, pcsRaw, err := devPCSLatch(m.dev)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
count, frRaw, err := devFastRetrain(m.dev)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m.mu.Lock()
|
2026-08-13 16:32:14 -07:00
|
|
|
|
if m.link && !link {
|
|
|
|
|
|
m.notes = append(m.notes,
|
2026-08-14 23:35:19 -07:00
|
|
|
|
fmt.Sprintf("%s link read down: 1.1=0x%04x", m.dev.name(), linkRaw))
|
2026-08-13 16:32:14 -07:00
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
m.sampled = true
|
|
|
|
|
|
m.lastOK = time.Now()
|
|
|
|
|
|
m.link = link
|
2026-08-14 23:35:19 -07:00
|
|
|
|
m.haveSNR = haveSNR
|
|
|
|
|
|
m.margins = margins
|
2026-08-13 08:04:26 -07:00
|
|
|
|
// The first poll after a baseline drains latches from the bringup/diag
|
|
|
|
|
|
// retrain era, so it only sets the origin; the retrain counter is 5 bits.
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if m.primed {
|
2026-08-13 16:32:14 -07:00
|
|
|
|
rt := uint64((count - m.retrainCount) & 0x1F)
|
|
|
|
|
|
delta := blocks + ber + rt
|
|
|
|
|
|
if delta > 0 {
|
|
|
|
|
|
m.notes = append(m.notes, fmt.Sprintf(
|
|
|
|
|
|
"%s corrected +%d raw: 3.33=0x%04x (blocks %d ber %d) 1.147=0x%04x (retrain +%d) 1.1=0x%04x",
|
2026-08-14 23:35:19 -07:00
|
|
|
|
m.dev.name(), delta, pcsRaw, blocks, ber, frRaw, rt, linkRaw))
|
2026-08-13 16:32:14 -07:00
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
m.blocks += blocks
|
|
|
|
|
|
m.ber += ber
|
2026-08-13 16:32:14 -07:00
|
|
|
|
m.retrains += rt
|
2026-08-13 07:10:39 -07:00
|
|
|
|
m.recentDelta = delta
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m.recentDelta = 0
|
|
|
|
|
|
m.primed = true
|
|
|
|
|
|
}
|
|
|
|
|
|
m.retrainCount = count
|
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 16:32:14 -07:00
|
|
|
|
func (m *phyModule) takeNotes() []string {
|
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
|
n := m.notes
|
|
|
|
|
|
m.notes = nil
|
|
|
|
|
|
return n
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func (m *phyModule) run(done *atomic.Bool) {
|
|
|
|
|
|
tick := time.NewTicker(phyInterval)
|
|
|
|
|
|
defer tick.Stop()
|
|
|
|
|
|
|
|
|
|
|
|
dark := 0
|
|
|
|
|
|
var lastErr error
|
|
|
|
|
|
for !done.Load() {
|
|
|
|
|
|
<-tick.C
|
|
|
|
|
|
if err := m.poll(); err != nil {
|
|
|
|
|
|
dark++
|
|
|
|
|
|
lastErr = err
|
|
|
|
|
|
if dark >= phyMaxDark {
|
|
|
|
|
|
panic(fmt.Sprintf("module diagnostics dark for %d polls: %v", dark, lastErr))
|
|
|
|
|
|
}
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
dark = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *phyModule) reset() {
|
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
|
m.blocks, m.ber, m.retrains, m.recentDelta = 0, 0, 0, 0
|
|
|
|
|
|
m.primed = false
|
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type phyModView struct {
|
|
|
|
|
|
fresh bool
|
|
|
|
|
|
link bool
|
2026-08-14 23:35:19 -07:00
|
|
|
|
haveSNR bool
|
2026-08-13 07:10:39 -07:00
|
|
|
|
margins [4]float64
|
|
|
|
|
|
blocks uint64
|
|
|
|
|
|
ber uint64
|
|
|
|
|
|
retrain uint64
|
|
|
|
|
|
recent uint64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *phyModule) view() phyModView {
|
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
|
v := phyModView{
|
|
|
|
|
|
fresh: m.sampled && time.Since(m.lastOK) < phyStale,
|
2026-08-14 23:35:19 -07:00
|
|
|
|
link: m.link,
|
|
|
|
|
|
haveSNR: m.haveSNR,
|
|
|
|
|
|
margins: m.margins,
|
2026-08-13 07:10:39 -07:00
|
|
|
|
blocks: m.blocks,
|
|
|
|
|
|
ber: m.ber,
|
|
|
|
|
|
retrain: m.retrains,
|
|
|
|
|
|
}
|
|
|
|
|
|
if v.fresh {
|
|
|
|
|
|
v.recent = m.recentDelta
|
|
|
|
|
|
}
|
|
|
|
|
|
return v
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type cableInfo struct {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
ecd ecdResult
|
|
|
|
|
|
maps [2]byte
|
|
|
|
|
|
haveMaps [2]bool
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c cableInfo) metresString() string {
|
|
|
|
|
|
sum, n := 0, 0
|
|
|
|
|
|
for i, v := range c.ecd.verdicts {
|
|
|
|
|
|
if v == pairOK {
|
|
|
|
|
|
sum += c.ecd.metres[i]
|
|
|
|
|
|
n++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if n == 0 {
|
|
|
|
|
|
return "-"
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Sprintf("%d", (sum+n/2)/n)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
clsNone = iota
|
|
|
|
|
|
clsGood
|
|
|
|
|
|
clsWarn
|
|
|
|
|
|
clsBad
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func snrClass(margin float64) int {
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case margin >= snrGoodMargin:
|
|
|
|
|
|
return clsGood
|
|
|
|
|
|
case margin >= snrWarnMargin:
|
|
|
|
|
|
return clsWarn
|
|
|
|
|
|
default:
|
|
|
|
|
|
return clsBad
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type phyDisplay struct {
|
|
|
|
|
|
haveSNR bool
|
|
|
|
|
|
worstMargin float64
|
|
|
|
|
|
corrected uint64
|
|
|
|
|
|
recent uint64
|
|
|
|
|
|
metres string
|
|
|
|
|
|
metresClass int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func pairLetter(i int) string { return string(rune('A' + i)) }
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
// Each end resolves MDI on its own, so a swap at either known end counts; an
|
|
|
|
|
|
// end with no readable map abstains.
|
|
|
|
|
|
func pairSwapped(i int, c cableInfo) bool {
|
|
|
|
|
|
for e := range c.maps {
|
|
|
|
|
|
if c.haveMaps[e] && int(c.maps[e]>>(2*i))&3 != i {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func cableSummary(cable cableInfo, measuring bool) (string, int) {
|
|
|
|
|
|
if measuring {
|
|
|
|
|
|
return "...", clsNone
|
|
|
|
|
|
}
|
|
|
|
|
|
anyData, anyFault, anySwap := false, false, false
|
|
|
|
|
|
for i, v := range cable.ecd.verdicts {
|
|
|
|
|
|
if v != 0 {
|
|
|
|
|
|
anyData = true
|
|
|
|
|
|
}
|
|
|
|
|
|
if v != 0 && v != pairOK {
|
|
|
|
|
|
anyFault = true
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if pairSwapped(i, cable) {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
anySwap = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
s := cable.metresString()
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case !anyData:
|
|
|
|
|
|
return "-", clsNone
|
|
|
|
|
|
case anyFault:
|
|
|
|
|
|
return s, clsBad
|
|
|
|
|
|
case anySwap:
|
|
|
|
|
|
return s, clsWarn
|
|
|
|
|
|
}
|
|
|
|
|
|
return s, clsGood
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
// The margin is the worst pair across the ends that measure SNR (the Wiitek's
|
|
|
|
|
|
// IEEE 1.133–136), gated on the whole pair being fresh and linked.
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func phyDisplayFrom(cable cableInfo, measuring bool, a, b phyModView) phyDisplay {
|
|
|
|
|
|
d := phyDisplay{
|
2026-08-14 23:35:19 -07:00
|
|
|
|
haveSNR: a.fresh && b.fresh && a.link && b.link && (a.haveSNR || b.haveSNR),
|
2026-08-13 07:10:39 -07:00
|
|
|
|
corrected: a.blocks + a.ber + a.retrain + b.blocks + b.ber + b.retrain,
|
|
|
|
|
|
recent: a.recent + b.recent,
|
|
|
|
|
|
}
|
|
|
|
|
|
if d.haveSNR {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
first := true
|
|
|
|
|
|
for _, v := range []phyModView{a, b} {
|
|
|
|
|
|
if !v.haveSNR {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, m := range v.margins {
|
|
|
|
|
|
if first || m < d.worstMargin {
|
|
|
|
|
|
d.worstMargin = m
|
|
|
|
|
|
first = false
|
|
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
d.metres, d.metresClass = cableSummary(cable, measuring)
|
|
|
|
|
|
return d
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
type ecdResult struct {
|
|
|
|
|
|
verdicts [4]int
|
|
|
|
|
|
metres [4]int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (b *bcm) cableDiag() (res ecdResult, err error) {
|
|
|
|
|
|
b.exec(func() {
|
|
|
|
|
|
var ctrl uint16
|
|
|
|
|
|
if ctrl, err = b.mdioRead(bcmMMDVendor, bcmRegECDCtrl); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if err = b.mdioWrite(bcmMMDVendor, bcmRegECDCtrl, ctrl&^0xF400|0x8400); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
deadline := time.Now().Add(ecdDeadline)
|
|
|
|
|
|
for {
|
|
|
|
|
|
if ctrl, err = b.mdioRead(bcmMMDVendor, bcmRegECDCtrl); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if ctrl&0x0800 == 0 {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
if time.Now().After(deadline) {
|
|
|
|
|
|
err = fmt.Errorf("%s: cable diag still busy after %s", b.ifname, ecdDeadline)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(ecdPoll)
|
|
|
|
|
|
}
|
|
|
|
|
|
b.window()
|
|
|
|
|
|
var v uint16
|
|
|
|
|
|
if v, err = b.mdioRead(1, bcmRegECDResult); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := range res.verdicts {
|
|
|
|
|
|
res.verdicts[i] = int(v>>(4*i)) & 0xF
|
|
|
|
|
|
if res.verdicts[i] > pairXtalk {
|
|
|
|
|
|
panic(fmt.Sprintf("%s: ghost ECD verdict %#04x", b.ifname, v))
|
|
|
|
|
|
}
|
|
|
|
|
|
var m uint16
|
|
|
|
|
|
if m, err = b.mdioRead(1, bcmRegECDLen+uint16(i)); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
res.metres[i] = int(m)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func bcmEnd(mods []*phyModule) *bcm {
|
|
|
|
|
|
for _, m := range mods {
|
|
|
|
|
|
if b, ok := m.dev.(*bcm); ok {
|
|
|
|
|
|
return b
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
panic("no BCM module in the pair: the ECD is the only length path")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 10:49:01 -07:00
|
|
|
|
// The pollers are held silent throughout; pair maps are read after the
|
|
|
|
|
|
// relink, so the MDI resolution is the fresh one.
|
2026-08-13 10:19:32 -07:00
|
|
|
|
func measureCable(mods []*phyModule, waitRelink bool, done *atomic.Bool) (cableInfo, bool, error) {
|
|
|
|
|
|
for _, m := range mods {
|
|
|
|
|
|
m.busy.Store(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer func() {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
for _, m := range mods {
|
2026-08-13 10:19:32 -07:00
|
|
|
|
m.busy.Store(false)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-13 10:19:32 -07:00
|
|
|
|
}()
|
2026-08-13 07:10:39 -07:00
|
|
|
|
|
|
|
|
|
|
var c cableInfo
|
|
|
|
|
|
var err error
|
2026-08-14 23:35:19 -07:00
|
|
|
|
end := bcmEnd(mods)
|
|
|
|
|
|
c.ecd, err = end.cableDiag()
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return c, false, err
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if err = devRestartAN(end); err != nil {
|
|
|
|
|
|
return c, false, err
|
|
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
relinked := false
|
|
|
|
|
|
if waitRelink {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
names := [2]string{mods[0].dev.name(), mods[1].dev.name()}
|
2026-08-13 10:19:32 -07:00
|
|
|
|
_, relinked = waitCarrier(names, done)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
for i, m := range mods {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
b, ok := m.dev.(*bcm)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if c.maps[i], err = b.pairMap(); err != nil {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
return c, false, err
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
c.haveMaps[i] = true
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
return c, relinked, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type cableDiag struct {
|
|
|
|
|
|
mods []*phyModule
|
2026-08-13 10:19:32 -07:00
|
|
|
|
completed chan error
|
2026-08-13 07:10:39 -07:00
|
|
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
|
info cableInfo
|
|
|
|
|
|
running bool
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func newCableDiag(mods []*phyModule, info cableInfo) *cableDiag {
|
2026-08-13 10:19:32 -07:00
|
|
|
|
return &cableDiag{mods: mods, completed: make(chan error, 1), info: info}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *cableDiag) snapshot() (cableInfo, bool) {
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
|
defer c.mu.Unlock()
|
|
|
|
|
|
return c.info, c.running
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *cableDiag) kick(done *atomic.Bool) bool {
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
|
if c.running {
|
|
|
|
|
|
c.mu.Unlock()
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
c.running = true
|
|
|
|
|
|
c.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer holdPanic()
|
|
|
|
|
|
info, _, err := measureCable(c.mods, true, done)
|
|
|
|
|
|
if err != nil {
|
2026-08-13 10:19:32 -07:00
|
|
|
|
info = cableInfo{}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
|
c.info = info
|
|
|
|
|
|
c.running = false
|
|
|
|
|
|
c.mu.Unlock()
|
|
|
|
|
|
select {
|
2026-08-13 10:19:32 -07:00
|
|
|
|
case c.completed <- err:
|
2026-08-13 07:10:39 -07:00
|
|
|
|
default:
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
func mapString(m byte, have bool) string {
|
|
|
|
|
|
if !have {
|
|
|
|
|
|
return "unread"
|
|
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if m == pairIdentityMap {
|
|
|
|
|
|
return "straight"
|
|
|
|
|
|
}
|
|
|
|
|
|
out := make([]string, 4)
|
|
|
|
|
|
for i := range out {
|
|
|
|
|
|
out[i] = pairLetter(int(m>>(2*i)) & 3)
|
|
|
|
|
|
}
|
|
|
|
|
|
return "swapped to " + strings.Join(out, "")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func verdictString(r ecdResult) string {
|
|
|
|
|
|
bad := []string{}
|
|
|
|
|
|
for i, v := range r.verdicts {
|
|
|
|
|
|
if v != pairOK {
|
|
|
|
|
|
s, ok := pairVerdicts[v]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
s = fmt.Sprintf("%d", v)
|
|
|
|
|
|
}
|
|
|
|
|
|
bad = append(bad, fmt.Sprintf("%s %s at %dm", pairLetter(i), s, r.metres[i]))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(bad) > 0 {
|
|
|
|
|
|
return strings.Join(bad, ", ")
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Sprintf("all pairs ok, %d/%d/%d/%d m",
|
|
|
|
|
|
r.metres[0], r.metres[1], r.metres[2], r.metres[3])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 08:04:26 -07:00
|
|
|
|
func openModules(names [2]string) ([]*phyModule, [2]string, error) {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
mods := make([]*phyModule, 0, 2)
|
2026-08-13 08:04:26 -07:00
|
|
|
|
var idents [2]string
|
|
|
|
|
|
for i, name := range names {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
t, err := openSFF(name)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
2026-08-13 08:04:26 -07:00
|
|
|
|
return nil, idents, err
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
pn, err := t.vendorPN()
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
2026-08-13 08:04:26 -07:00
|
|
|
|
return nil, idents, err
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
var dev phyDev
|
|
|
|
|
|
switch pn {
|
|
|
|
|
|
case fsVendorPN:
|
|
|
|
|
|
dev = newBCM(t)
|
|
|
|
|
|
case wiitekVendorPN:
|
|
|
|
|
|
dev = &rollball{sff: t}
|
|
|
|
|
|
default:
|
|
|
|
|
|
return nil, idents, fmt.Errorf("%s: unknown module PN %q", name, pn)
|
|
|
|
|
|
}
|
|
|
|
|
|
idents[i], err = dev.identify()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, idents, err
|
|
|
|
|
|
}
|
|
|
|
|
|
m := &phyModule{dev: dev}
|
2026-08-13 10:19:32 -07:00
|
|
|
|
// Born busy: the pollers stay silent through bringup's SETs and
|
|
|
|
|
|
// retrains until the first measure completes and lifts the gate.
|
|
|
|
|
|
m.busy.Store(true)
|
|
|
|
|
|
mods = append(mods, m)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-13 08:04:26 -07:00
|
|
|
|
return mods, idents, nil
|
|
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
|
2026-08-13 10:19:32 -07:00
|
|
|
|
func waitCarrier(names [2]string, done *atomic.Bool) (time.Duration, bool) {
|
|
|
|
|
|
start := time.Now()
|
|
|
|
|
|
deadline := start.Add(linkWaitSpan)
|
|
|
|
|
|
for {
|
|
|
|
|
|
if carrierUp(names[0]) && carrierUp(names[1]) {
|
|
|
|
|
|
return time.Since(start), true
|
|
|
|
|
|
}
|
|
|
|
|
|
if time.Now().After(deadline) || (done != nil && done.Load()) {
|
|
|
|
|
|
return time.Since(start), false
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(linkWaitPoll)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// No trustworthy config readback exists (DATA1 is firmware scratch) and no
|
|
|
|
|
|
// cable is guaranteed at bringup, so both settings are forced every boot: the
|
|
|
|
|
|
// one deterministic assurance. The handler freezes during training, so the
|
|
|
|
|
|
// carrier settles — the host checks just reset the links — before any command.
|
2026-08-13 10:49:01 -07:00
|
|
|
|
// Never waits for a link: there may be no cable, and forcing config needs
|
|
|
|
|
|
// none — the AN restart applies it whenever training next happens.
|
2026-08-13 08:04:26 -07:00
|
|
|
|
func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
|
|
|
|
|
|
var out []checkResult
|
|
|
|
|
|
fail := func(item string, err error) []checkResult {
|
|
|
|
|
|
return append(out, checkResult{item: item, err: err})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 23:35:19 -07:00
|
|
|
|
// With a non-BCM partner the BCM is forced slave: the partner's manual
|
|
|
|
|
|
// config is unreachable, and auto-resolves-master against manual-slave is
|
|
|
|
|
|
// the combination proven to link.
|
|
|
|
|
|
mixed := false
|
|
|
|
|
|
for _, m := range mods {
|
|
|
|
|
|
if _, ok := m.dev.(*bcm); !ok {
|
|
|
|
|
|
mixed = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
master := !mixed
|
2026-08-13 10:19:32 -07:00
|
|
|
|
for i, m := range mods {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
b, ok := m.dev.(*bcm)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
out = append(out, checkResult{item: names[i] + " role", state: "auto"})
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 10:19:32 -07:00
|
|
|
|
res := checkResult{item: names[i] + " eee", state: "forced off"}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if err := b.forceEEEOff(); err != nil {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
return fail(res.item, err)
|
|
|
|
|
|
}
|
2026-08-13 08:04:26 -07:00
|
|
|
|
out = append(out, res)
|
|
|
|
|
|
|
2026-08-13 10:19:32 -07:00
|
|
|
|
res = checkResult{item: names[i] + " jumbo", state: "forced on"}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if err := b.forceJumbo(); err != nil {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
return fail(res.item, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, res)
|
2026-08-13 08:04:26 -07:00
|
|
|
|
|
2026-08-13 14:23:57 -07:00
|
|
|
|
res = checkResult{item: names[i] + " role", state: "forced master"}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if !master {
|
2026-08-13 14:23:57 -07:00
|
|
|
|
res.state = "forced slave"
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if err := b.forceRole(master); err != nil {
|
2026-08-13 14:23:57 -07:00
|
|
|
|
return fail(res.item, err)
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
master = false
|
2026-08-13 14:23:57 -07:00
|
|
|
|
out = append(out, res)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 16:32:14 -07:00
|
|
|
|
// Both modules configured and verified before either AN restart: the
|
|
|
|
|
|
// modules link to each other, so one restart puts both µCs into training,
|
|
|
|
|
|
// and no read should race that. The restarts fire last, nothing after.
|
2026-08-13 10:49:01 -07:00
|
|
|
|
res := checkResult{item: "eee advert"}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
var adv [2]uint16
|
|
|
|
|
|
var advs [2]string
|
2026-08-13 10:19:32 -07:00
|
|
|
|
for i, m := range mods {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
v, err := devEEEAdvert(m.dev)
|
2026-08-13 08:04:26 -07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fail(res.item, err)
|
|
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
adv[i] = v
|
|
|
|
|
|
advs[i] = fmt.Sprintf("%#04x", v)
|
|
|
|
|
|
if _, ok := m.dev.(*bcm); ok && v != 0 {
|
2026-08-13 10:19:32 -07:00
|
|
|
|
res.err = fmt.Errorf("%s still advertises EEE %#04x", names[i], v)
|
2026-08-13 08:04:26 -07:00
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if res.err == nil && adv[0]&adv[1] != 0 {
|
|
|
|
|
|
res.err = fmt.Errorf("EEE would negotiate: common ability %#04x", adv[0]&adv[1])
|
|
|
|
|
|
}
|
|
|
|
|
|
res.state = advs[0] + "/" + advs[1]
|
2026-08-13 16:32:14 -07:00
|
|
|
|
out = append(out, res)
|
|
|
|
|
|
|
|
|
|
|
|
for i, m := range mods {
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if err := devRestartAN(m.dev); err != nil {
|
2026-08-13 16:32:14 -07:00
|
|
|
|
return fail(names[i]+" retrain", err)
|
|
|
|
|
|
}
|
2026-08-13 14:04:16 -07:00
|
|
|
|
}
|
2026-08-13 16:32:14 -07:00
|
|
|
|
return out
|
2026-08-13 08:04:26 -07:00
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
|
2026-08-13 08:04:26 -07:00
|
|
|
|
func cableLine(c cableInfo) string {
|
|
|
|
|
|
return fmt.Sprintf("%s; map %s / %s",
|
2026-08-14 23:35:19 -07:00
|
|
|
|
verdictString(c.ecd), mapString(c.maps[0], c.haveMaps[0]), mapString(c.maps[1], c.haveMaps[1]))
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|