2026-08-13 07:10:39 -07:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
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 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 11:25:31 -07:00
|
|
|
|
// Latches drained after a measure belong to its blip: un-priming makes the
|
|
|
|
|
|
// next poll an origin only, without discarding the pre-measure totals.
|
|
|
|
|
|
func (m *phyModule) forgive() {
|
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
|
m.primed = false
|
|
|
|
|
|
m.recentDelta = 0
|
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 07:10:39 -07:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 11:25:31 -07:00
|
|
|
|
// The whole cable verdict in one cell: the length when healthy, the first
|
2026-08-17 18:56:55 -07:00
|
|
|
|
// faulted pair's verdict when not, "xover" for a pair swap.
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func cableSummary(cable cableInfo, measuring bool) (string, int) {
|
|
|
|
|
|
if measuring {
|
2026-08-17 11:25:31 -07:00
|
|
|
|
return "-", clsNone
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-17 11:25:31 -07:00
|
|
|
|
anyData, anySwap := false, false
|
2026-08-13 07:10:39 -07:00
|
|
|
|
for i, v := range cable.ecd.verdicts {
|
|
|
|
|
|
if v != 0 {
|
|
|
|
|
|
anyData = true
|
|
|
|
|
|
}
|
|
|
|
|
|
if v != 0 && v != pairOK {
|
2026-08-17 11:25:31 -07:00
|
|
|
|
return pairVerdicts[v], clsBad
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if pairSwapped(i, cable) {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
anySwap = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case !anyData:
|
|
|
|
|
|
return "-", clsNone
|
|
|
|
|
|
case anySwap:
|
2026-08-17 11:25:31 -07:00
|
|
|
|
return "xover", clsWarn
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-17 11:25:31 -07:00
|
|
|
|
return cable.metresString(), clsGood
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
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 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-17 18:56:55 -07:00
|
|
|
|
func measureCable(mods []*phyModule, done *atomic.Bool) (cableInfo, error) {
|
2026-08-13 10:19:32 -07:00
|
|
|
|
for _, m := range mods {
|
|
|
|
|
|
m.busy.Store(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer func() {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
for _, m := range mods {
|
2026-08-17 11:25:31 -07:00
|
|
|
|
m.forgive()
|
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 {
|
2026-08-17 18:56:55 -07:00
|
|
|
|
return c, err
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
if err = devRestartAN(end); err != nil {
|
2026-08-17 18:56:55 -07:00
|
|
|
|
return c, err
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-17 18:56:55 -07:00
|
|
|
|
waitCarrier([2]string{mods[0].dev.name(), mods[1].dev.name()}, 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-17 18:56:55 -07:00
|
|
|
|
return c, err
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-14 23:35:19 -07:00
|
|
|
|
c.haveMaps[i] = true
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
2026-08-17 18:56:55 -07:00
|
|
|
|
return c, nil
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type cableDiag struct {
|
2026-08-17 11:25:31 -07:00
|
|
|
|
mods []*phyModule
|
|
|
|
|
|
|
|
|
|
|
|
// While set, every failure counter is suppressed at its source: the
|
|
|
|
|
|
// measure's own link blip is never counted anywhere, rather than counted,
|
|
|
|
|
|
// hidden and reverted.
|
|
|
|
|
|
measuring atomic.Bool
|
2026-08-13 07:10:39 -07:00
|
|
|
|
|
2026-08-17 11:25:31 -07:00
|
|
|
|
mu sync.Mutex
|
|
|
|
|
|
info cableInfo
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func newCableDiag(mods []*phyModule, info cableInfo) *cableDiag {
|
2026-08-17 11:25:31 -07:00
|
|
|
|
return &cableDiag{mods: mods, info: info}
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *cableDiag) snapshot() (cableInfo, bool) {
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
|
defer c.mu.Unlock()
|
2026-08-17 11:25:31 -07:00
|
|
|
|
return c.info, c.measuring.Load()
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 18:56:55 -07:00
|
|
|
|
// A measure that errors means the diag transport or the µC is broken, and a
|
|
|
|
|
|
// tester that cannot run its diagnostics must not keep testing: the failure is
|
|
|
|
|
|
// fatal, and the reboot's driver reload is also the wedged-µC recovery.
|
2026-08-13 07:10:39 -07:00
|
|
|
|
func (c *cableDiag) kick(done *atomic.Bool) bool {
|
2026-08-17 11:25:31 -07:00
|
|
|
|
if !c.measuring.CompareAndSwap(false, true) {
|
2026-08-13 07:10:39 -07:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer holdPanic()
|
2026-08-17 18:56:55 -07:00
|
|
|
|
info, err := measureCable(c.mods, done)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
if err != nil {
|
2026-08-17 18:56:55 -07:00
|
|
|
|
panic(fmt.Sprintf("cable measure: %v", err))
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
|
c.info = info
|
|
|
|
|
|
c.mu.Unlock()
|
2026-08-17 11:25:31 -07:00
|
|
|
|
c.measuring.Store(false)
|
2026-08-13 07:10:39 -07:00
|
|
|
|
}()
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-17 18:56:55 -07:00
|
|
|
|
func waitCarrier(names [2]string, done *atomic.Bool) {
|
|
|
|
|
|
deadline := time.Now().Add(linkWaitSpan)
|
|
|
|
|
|
for !(carrierUp(names[0]) && carrierUp(names[1])) {
|
|
|
|
|
|
if time.Now().After(deadline) || done.Load() {
|
|
|
|
|
|
return
|
2026-08-13 10:19:32 -07:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
}
|