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 bcmCmdSetJumbo uint16 = 0x801C bcmRegECDCtrl uint16 = 0x4006 bcmRegECDResult uint16 = 0xA896 bcmRegECDLen uint16 = 0xA897 bcmPHYIDHi = 0x3590 bcmPHYIDLo = 0x5081 bcmCmdResidentTemp uint16 = 0x0031 bcmReadDelayUs = 3000 bcmRetryDelayUs = 10000 bcmWindow = 3400 * time.Millisecond bcmWindowFit = 100 * time.Millisecond bcmFlipPoll = 10 * time.Millisecond bcmFlipWait = 5 * time.Second bcmStatusPoll = 100 * time.Millisecond // Covers the handler's documented 2 s freeze during 10GBASE-T training. bcmStatusTimeout = 3 * time.Second ecdPoll = 200 * time.Millisecond ecdDeadline = 50 * time.Second pairIdentityMap = 0xE4 fsVendorPN = "SFP-10G-T-100" wiitekVendorPN = "UF-RJ45-10G-100" ) const ( pairOK = 1 pairOpen = 2 pairShort = 3 pairXtalk = 4 ) var pairVerdicts = map[int]string{ pairOK: "ok", pairOpen: "OPEN", pairShort: "SHORT", pairXtalk: "XTALK", } type sff struct { ifname string path string // Every transport touch happens on the loop goroutine: requests execute // one at a time, each admitted by the module type's own gate first. reqs chan func() admit func() } func openSFF(ifname string) (*sff, error) { 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) } s := &sff{ ifname: ifname, path: "/sys/kernel/debug/ixgbe/" + filepath.Base(devLink) + "/sff_i2c", reqs: make(chan func()), admit: func() {}, } if _, err := os.Stat(s.path); err != nil { return nil, fmt.Errorf("%s: %w (patched ixgbe?)", ifname, err) } 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 } 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) if err != nil { return "", fmt.Errorf("%s: %w", s.path, err) } defer unix.Close(fd) if _, err := unix.Write(fd, []byte(cmd)); err != nil { return "", fmt.Errorf("%s %q: %w", s.ifname, cmd, err) } buf := make([]byte, 256) n, err := unix.Read(fd, buf) if err != nil { return "", fmt.Errorf("%s %q: %w", s.ifname, cmd, err) } resp := strings.TrimSpace(string(buf[:n])) if !strings.HasPrefix(resp, "ok") { return "", fmt.Errorf("%s %q: %s", s.ifname, cmd, resp) } 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 } // A single bus hold; split write/read ops would let the driver's own SFP // traffic consume the bridge's pending read. func (s *sff) compound(waddr, raddr byte, delayUs, n int, wdata []byte) ([]byte, error) { 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) } resp, err := s.op(sb.String()) if err != nil { return nil, err } return parseHexBytes(resp, n) } 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)) } } 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 } // 0x0000 is also the bridge's not-ready signature, so a zero is read again at // a longer delay before being believed. func (b *bcm) mdioRead(devad, reg uint16) (uint16, error) { v, err := b.mdioReadDelay(devad, reg, bcmReadDelayUs) if err != nil || v != 0 { return v, err } return b.mdioReadDelay(devad, reg, bcmRetryDelayUs) } 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 } // The datasheet's completion handshake: poll STATUS on its 100 ms cadence // until the wanted state, bounded by a deadline. func (b *bcm) waitStatus(want func(uint16) bool) (uint16, error) { deadline := time.Now().Add(bcmStatusTimeout) for { st, err := b.mdioRead(bcmMMDVendor, bcmRegStatus) if err != nil { return 0, err } if want(st) { return st, nil } if time.Now().After(deadline) { return 0, fmt.Errorf("%s: command handler stuck, status %#04x", b.ifname, st) } time.Sleep(bcmStatusPoll) } } // 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 // executes stale DATA). 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 } for i, p := range params { if err = b.mdioWrite(bcmMMDVendor, bcmRegData1+uint16(i), p); err != nil { return } } 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 } 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 } func (b *bcm) forceEEEOff() error { _, err := b.command(bcmCmdSetEEEMode, 0x0000, 0x0000, 0x7A12, 0x0480, 0x0000) return err } func (b *bcm) forceJumbo() error { _, err := b.command(bcmCmdSetJumbo, 1, 0, 0, 0, 0) return err } // 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. 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 } func (b *bcm) pairMap() (byte, error) { d, err := b.command(bcmCmdGetPairSwap) if err != nil { return 0, err } return byte(d[1]), nil } 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) } _, 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 } return r.i2cWrite(rbOffPassword, 0xFF, 0xFF, 0xFF, 0xFF) } 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 } } 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) } } 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)) } } 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 } 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 { return } if lo, err = r.mdioRead(1, 3); err != nil { return } if hi != rbPHYIDHi || lo != rbPHYIDLo { err = fmt.Errorf("%s: PHY ID %#04x:%#04x, want %#04x:%#04x", r.ifname, hi, lo, rbPHYIDHi, rbPHYIDLo) return } if err = r.unlock(); err != nil { return } 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 { return } 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 } }) return } const ( phyInterval = time.Second phyStale = 5 * time.Second phyMaxDark = 30 linkWaitSpan = 25 * time.Second linkWaitPoll = time.Second snrGoodMargin = 3.0 snrWarnMargin = 1.0 ) type phyModule struct { dev phyDev busy atomic.Bool mu sync.Mutex sampled bool lastOK time.Time link bool haveSNR bool margins [4]float64 blocks uint64 ber uint64 retrains uint64 recentDelta uint64 primed bool retrainCount uint16 notes []string } // Silent while a measure owns the module. func (m *phyModule) poll() error { if m.busy.Load() { return nil } link, linkRaw, err := devLinkUp(m.dev) if err != nil { return err } var margins [4]float64 haveSNR := false if src, ok := m.dev.(snrSource); ok && link { if margins, err = src.snrMargins(); err != nil { return err } haveSNR = true } blocks, ber, pcsRaw, err := devPCSLatch(m.dev) if err != nil { return err } count, frRaw, err := devFastRetrain(m.dev) if err != nil { return err } m.mu.Lock() if m.link && !link { m.notes = append(m.notes, fmt.Sprintf("%s link read down: 1.1=0x%04x", m.dev.name(), linkRaw)) } m.sampled = true m.lastOK = time.Now() m.link = link m.haveSNR = haveSNR m.margins = margins // 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. if m.primed { 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", m.dev.name(), delta, pcsRaw, blocks, ber, frRaw, rt, linkRaw)) } m.blocks += blocks m.ber += ber m.retrains += rt m.recentDelta = delta } else { m.recentDelta = 0 m.primed = true } m.retrainCount = count m.mu.Unlock() return nil } func (m *phyModule) takeNotes() []string { m.mu.Lock() defer m.mu.Unlock() n := m.notes m.notes = nil return n } 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 haveSNR bool 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, link: m.link, haveSNR: m.haveSNR, margins: m.margins, blocks: m.blocks, ber: m.ber, retrain: m.retrains, } if v.fresh { v.recent = m.recentDelta } return v } type cableInfo struct { ecd ecdResult maps [2]byte haveMaps [2]bool } 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)) } // 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 } 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 } if pairSwapped(i, cable) { anySwap = true } } s := cable.metresString() switch { case !anyData: return "-", clsNone case anyFault: return s, clsBad case anySwap: return s, clsWarn } return s, clsGood } // 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. func phyDisplayFrom(cable cableInfo, measuring bool, a, b phyModView) phyDisplay { d := phyDisplay{ haveSNR: a.fresh && b.fresh && a.link && b.link && (a.haveSNR || b.haveSNR), corrected: a.blocks + a.ber + a.retrain + b.blocks + b.ber + b.retrain, recent: a.recent + b.recent, } if d.haveSNR { 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 } } } } d.metres, d.metresClass = cableSummary(cable, measuring) return d } 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") } // The pollers are held silent throughout; pair maps are read after the // relink, so the MDI resolution is the fresh one. func measureCable(mods []*phyModule, waitRelink bool, done *atomic.Bool) (cableInfo, bool, error) { for _, m := range mods { m.busy.Store(true) } defer func() { for _, m := range mods { m.busy.Store(false) } }() var c cableInfo var err error end := bcmEnd(mods) c.ecd, err = end.cableDiag() if err != nil { return c, false, err } if err = devRestartAN(end); err != nil { return c, false, err } relinked := false if waitRelink { names := [2]string{mods[0].dev.name(), mods[1].dev.name()} _, relinked = waitCarrier(names, done) } for i, m := range mods { b, ok := m.dev.(*bcm) if !ok { continue } if c.maps[i], err = b.pairMap(); err != nil { return c, false, err } c.haveMaps[i] = true } return c, relinked, nil } type cableDiag struct { mods []*phyModule completed chan error mu sync.Mutex info cableInfo running bool } func newCableDiag(mods []*phyModule, info cableInfo) *cableDiag { return &cableDiag{mods: mods, completed: make(chan error, 1), info: info} } 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 { info = cableInfo{} } c.mu.Lock() c.info = info c.running = false c.mu.Unlock() select { case c.completed <- err: default: } }() return true } func mapString(m byte, have bool) string { if !have { return "unread" } 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]) } func openModules(names [2]string) ([]*phyModule, [2]string, error) { mods := make([]*phyModule, 0, 2) var idents [2]string for i, name := range names { t, err := openSFF(name) if err != nil { return nil, idents, err } pn, err := t.vendorPN() if err != nil { return nil, idents, err } 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} // 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) } return mods, idents, nil } 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. // Never waits for a link: there may be no cable, and forcing config needs // none — the AN restart applies it whenever training next happens. 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}) } // 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 for i, m := range mods { b, ok := m.dev.(*bcm) if !ok { out = append(out, checkResult{item: names[i] + " role", state: "auto"}) continue } res := checkResult{item: names[i] + " eee", state: "forced off"} if err := b.forceEEEOff(); err != nil { return fail(res.item, err) } out = append(out, res) res = checkResult{item: names[i] + " jumbo", state: "forced on"} if err := b.forceJumbo(); err != nil { return fail(res.item, err) } out = append(out, res) res = checkResult{item: names[i] + " role", state: "forced master"} if !master { res.state = "forced slave" } if err := b.forceRole(master); err != nil { return fail(res.item, err) } master = false out = append(out, res) } // 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. res := checkResult{item: "eee advert"} var adv [2]uint16 var advs [2]string for i, m := range mods { v, err := devEEEAdvert(m.dev) if err != nil { return fail(res.item, err) } adv[i] = v advs[i] = fmt.Sprintf("%#04x", v) if _, ok := m.dev.(*bcm); ok && v != 0 { res.err = fmt.Errorf("%s still advertises EEE %#04x", names[i], v) } } 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] out = append(out, res) for i, m := range mods { if err := devRestartAN(m.dev); err != nil { return fail(names[i]+" retrain", err) } } return out } func cableLine(c cableInfo) string { return fmt.Sprintf("%s; map %s / %s", verdictString(c.ecd), mapString(c.maps[0], c.haveMaps[0]), mapString(c.maps[1], c.haveMaps[1])) }