BCM module diagnostics replace timestamp probes: bringup forces EEE off and runs ECD (re-run on every reset, re-baselining past its blip), 1 Hz SNR margin + corrected counters on panel and console; X520 bench divergences bypassed and tracked in open-questions
This commit is contained in:
+48
-17
@@ -1,7 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -28,22 +30,43 @@ var nicTxFields = []string{
|
|||||||
// Receiving is the hardware's, and is taken from the driver's own array, which
|
// Receiving is the hardware's, and is taken from the driver's own array, which
|
||||||
// sysfs both flattens and overlaps: ice folds crc errors and jabbers into
|
// sysfs both flattens and overlaps: ice folds crc errors and jabbers into
|
||||||
// rx_errors, so the old sum of rx_errors alongside rx_crc_errors charged every
|
// rx_errors, so the old sum of rx_errors alongside rx_crc_errors charged every
|
||||||
// bad frame check twice. Named individually here, so nothing contains anything
|
// bad frame check twice. Named individually per driver, so nothing contains
|
||||||
// else in the list.
|
// anything else in its list.
|
||||||
var nicRxStats = []string{
|
var nicRxStatsByDriver = map[string][]string{
|
||||||
"rx_crc_errors.nic",
|
// The reference set (E810). Below the frame: illegal_bytes is a 64b/66b
|
||||||
"rx_jabber.nic",
|
// block that decoded to no legal symbol, and the faults are the ordered
|
||||||
"rx_undersize.nic",
|
// sets the pcs sends when it loses sync. A cable going marginal moves
|
||||||
"rx_oversize.nic",
|
// these while every frame still arrives intact, which is as close to a
|
||||||
"rx_fragments.nic",
|
// bit error rate as this link will report.
|
||||||
"rx_dropped.nic",
|
"ice": {
|
||||||
// Below the frame: a 64b/66b block that decoded to no legal symbol, and the
|
"rx_crc_errors.nic",
|
||||||
// fault ordered sets the pcs sends when it loses sync. A cable going
|
"rx_jabber.nic",
|
||||||
// marginal moves these while every frame still arrives intact, which is as
|
"rx_undersize.nic",
|
||||||
// close to a bit error rate as this link will report.
|
"rx_oversize.nic",
|
||||||
"illegal_bytes.nic",
|
"rx_fragments.nic",
|
||||||
"mac_local_faults.nic",
|
"rx_dropped.nic",
|
||||||
"mac_remote_faults.nic",
|
"illegal_bytes.nic",
|
||||||
|
"mac_local_faults.nic",
|
||||||
|
"mac_remote_faults.nic",
|
||||||
|
},
|
||||||
|
// The 82599 exposes no jabber, fragment, illegal-byte or fault counters
|
||||||
|
// through ethtool — this is the closest bench set, and one of the X520
|
||||||
|
// divergences listed in docs/open-questions.md.
|
||||||
|
"ixgbe": {
|
||||||
|
"rx_crc_errors",
|
||||||
|
"rx_missed_errors",
|
||||||
|
"rx_length_errors",
|
||||||
|
"rx_long_length_errors",
|
||||||
|
"rx_short_length_errors",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ifDriver(name string) (string, error) {
|
||||||
|
link, err := os.Readlink("/sys/class/net/" + name + "/device/driver")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Base(link), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readUint(path string) (uint64, bool) {
|
func readUint(path string) (uint64, bool) {
|
||||||
@@ -93,7 +116,15 @@ type nicPoller struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newNICPoller(fd int, txName, rxName string, total *atomic.Uint64) (*nicPoller, error) {
|
func newNICPoller(fd int, txName, rxName string, total *atomic.Uint64) (*nicPoller, error) {
|
||||||
rx, err := newStatReader(fd, rxName, nicRxStats)
|
drv, err := ifDriver(rxName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
want, ok := nicRxStatsByDriver[drv]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: no rx error statistic set for driver %s", rxName, drv)
|
||||||
|
}
|
||||||
|
rx, err := newStatReader(fd, rxName, want)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -8,8 +8,8 @@ The target of measurement is the cable, not throughput. Line rate is a means to
|
|||||||
|
|
||||||
1. **Loss and error attribution** — reception gaps, link errors, NIC/driver counters as first-class output alongside application loss. Baseline loss must be exactly zero before a run counts; any host-side loss masks real cable faults.
|
1. **Loss and error attribution** — reception gaps, link errors, NIC/driver counters as first-class output alongside application loss. Baseline loss must be exactly zero before a run counts; any host-side loss masks real cable faults.
|
||||||
2. **Noise tolerance** — a deliberately-bad "noise" cable intertwined with the test cable, driven by link up/down cycling, stresses the cable under test with alien crosstalk.
|
2. **Noise tolerance** — a deliberately-bad "noise" cable intertwined with the test cable, driven by link up/down cycling, stresses the cable under test with alien crosstalk.
|
||||||
3. **Per-pair SNR** from the module PHYs — the leading indicator of a marginal cable before it drops frames. IEEE 802.3an standard registers on the Marvells; the BCM leaves those unpopulated and reports through its vendor command handler instead (modules/fs/).
|
3. **Per-pair SNR** from the module PHYs — the leading indicator of a marginal cable before it drops frames. IEEE 802.3an standard registers on the Marvells; the BCM leaves those unpopulated and reports through its vendor command handler instead (modules/fs/). Shown as margin over the operating point, classified good/marginal/bad, so nobody needs the magic numbers to read it (`phy.go`).
|
||||||
4. **Cable length** — sanity check and fault localization. The BCM ECD is the product path: per-pair lengths in meters, healthy pairs included, proven meter-accurate on the bench (modules/fs/).
|
4. **Cable length** — sanity check and fault localization. The BCM ECD is the product path: per-pair lengths in meters, healthy pairs included, proven meter-accurate on the bench (modules/fs/); run at bringup and on every reset, with pair-swap and fault verdicts on the panel.
|
||||||
5. **Pre-FEC error visibility** — corrected-error counters that move before post-FEC loss appears. Vendor-specific; located on the Aquantia (modules/fibergaga/), unlocated elsewhere. Standard latched PCS counters (errored blocks, BER, block-lock loss) are the working proxy under noise stress.
|
5. **Pre-FEC error visibility** — corrected-error counters that move before post-FEC loss appears. Vendor-specific; located on the Aquantia (modules/fibergaga/), unlocated elsewhere. Standard latched PCS counters (errored blocks, BER, block-lock loss) are the working proxy under noise stress.
|
||||||
|
|
||||||
## Design preferences that shaped the tool
|
## Design preferences that shaped the tool
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Protocol and full verified catalog: [bcm84891l-mdio-commands.md](bcm84891l-mdio-
|
|||||||
|
|
||||||
| Capability | How | Observed |
|
| Capability | How | Observed |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Per-pair SNR | `CMD_GET_SNR` 0x8030, **invoked bare** (writing the documented DATA1 display flag returns zeros; IEEE 1.133–1.140 never populate — constant 0x8080) | DATA2–5 = SNR A–D ×0.1 dB; ≈ 27–32 dB absolute on bench, 0.1–0.4 dB jitter. 10GBASE-T operating point ≈ 26.5 dB, so margin ≈ value − 26.5 |
|
| Per-pair SNR | `CMD_GET_SNR` 0x8030, **invoked bare** (writing the documented DATA1 display flag returns zeros; IEEE 1.133–1.140 never populate — constant 0x8080) | DATA2–5 = SNR A–D ×0.1 dB; ≈ 27–32 dB absolute on bench, 0.1–0.4 dB jitter. 10GBASE-T operating point ≈ 26.5 dB, so margin ≈ value − 26.5. cabletest shows the margin classified green ≥ 3 dB / amber ≥ 1 dB / red below — provisional thresholds until the graded-noise correlation run |
|
||||||
| Die temperature | `CMD_GET_CURRENT_TEMP` 0x8031 | ~68–70 °C on bench |
|
| Die temperature | `CMD_GET_CURRENT_TEMP` 0x8031 | ~68–70 °C on bench |
|
||||||
| Supply rails | `GET_CURRENT_VOLTAGE` 0x802F | 0.8 V and 1.88 V rails, tenths of mV |
|
| Supply rails | `GET_CURRENT_VOLTAGE` 0x802F | 0.8 V and 1.88 V rails, tenths of mV |
|
||||||
| Error counters | IEEE PCS 3.32/3.33 — block lock, latched errored-block/BER, clear-on-read | The noise-stress error proxy |
|
| Error counters | IEEE PCS 3.32/3.33 — block lock, latched errored-block/BER, clear-on-read | The noise-stress error proxy |
|
||||||
@@ -37,7 +37,7 @@ Protocol and full verified catalog: [bcm84891l-mdio-commands.md](bcm84891l-mdio-
|
|||||||
|
|
||||||
| Item | Command | State |
|
| Item | Command | State |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| EEE / AutogrEEEn | 0x8008/0x8009 | Once read 0x0047 (10G AutogrEEEn variable latency + 5G/1G native — local-only, invisible in IEEE 7.60/7.61, which read 0); later reads 0. **Forced all-off** via SET with explicit params `(0, 0, 0x7A12, 0x480, 0)` + AN restart, verified. cabletest should apply this defensively at bringup |
|
| EEE / AutogrEEEn | 0x8008/0x8009 | Once read 0x0047 (10G AutogrEEEn variable latency + 5G/1G native — local-only, invisible in IEEE 7.60/7.61, which read 0); later reads 0. **Forced all-off** via SET with explicit params `(0, 0, 0x7A12, 0x480, 0)` + AN restart, verified. cabletest applies this defensively at every bringup and verifies 7.60 reads 0 after relink (`phy.go`) |
|
||||||
| EEE wire-truth | arm 0x801A after link-up, read 0x801B | Zero LPI events/duration on idle link; repeat under traffic |
|
| EEE wire-truth | arm 0x801A after link-up, read 0x801B | Zero LPI events/duration on idle link; repeat under traffic |
|
||||||
| Fast retrain | 0x800A (datasheet titles it EMI_MODE; description is fast retrain) | Enabled 10G/5G/2.5G; IEEE 1.147 = 0x0019, count bits zero. Keep enabled; read the 1.147 count per run — a marginal cable that fast-retrains still gets counted |
|
| Fast retrain | 0x800A (datasheet titles it EMI_MODE; description is fast retrain) | Enabled 10G/5G/2.5G; IEEE 1.147 = 0x0019, count bits zero. Keep enabled; read the 1.147 count per run — a marginal cable that fast-retrains still gets counted |
|
||||||
| Pair map | 0x8000 | DATA2 = 0x00E4 = identity (A/B/C/D straight through) — MDI wiring verification works |
|
| Pair map | 0x8000 | DATA2 = 0x00E4 = identity (A/B/C/D straight through) — MDI wiring verification works |
|
||||||
|
|||||||
@@ -21,3 +21,13 @@ The register question is answered (post-FEC vs corrected-by-iteration histogram
|
|||||||
## 4. Wiitek VCT — pursue or leave dead?
|
## 4. Wiitek VCT — pursue or leave dead?
|
||||||
|
|
||||||
No confirmed-safe path exists (every candidate lands in the µC danger window). The open decision is whether the capability is worth the NDA route or a sacrificial unit — the product doesn't need it for length ([modules/wiitek/](modules/wiitek/README.md), [modules/README.md](modules/README.md)).
|
No confirmed-safe path exists (every candidate lands in the µC danger window). The open decision is whether the capability is worth the NDA route or a sacrificial unit — the product doesn't need it for length ([modules/wiitek/](modules/wiitek/README.md), [modules/README.md](modules/README.md)).
|
||||||
|
|
||||||
|
## 5. X520 bench divergences — features to restore on the product NIC
|
||||||
|
|
||||||
|
Running on the X520 (BCM development) required parking product-NIC capabilities the 82599 lacks. Each stays parked only until the ConnectX-5 is in; none is a settled design change:
|
||||||
|
|
||||||
|
- **Hardware timestamp hard check bypassed** (`ts.go`): `rx_filter=ALL` failure now reports yellow and continues instead of stopping the run. On the X520 that means the per-frame-stamp rate buckets never fill and the panel/console rates read zero — the measurement doctrine (exact per-frame RX stamps as a hard host requirement) is intact in the docs and must return to fatal on the product NIC.
|
||||||
|
- **RX error counter set is per-driver** (`counters.go`, `nicRxStatsByDriver`): the ice set is the reference — jabber, fragments, `illegal_bytes` (64b/66b decode errors) and MAC local/remote faults have **no ixgbe ethtool equivalent**, so those signals are simply absent on the bench. The mlx5 name set needs deriving on CX-5 arrival; the "as close to BER as the link reports" counters (illegal bytes, faults) are the ones to insist on finding there.
|
||||||
|
- **TX interrupt moderation** (`system.go`): ixgbe's mixed rx/tx vectors reject a tx-specific value, so `checkCoalesce` falls back to rx-shared-with-tx on EINVAL. Generic and self-reporting, but verify the product NIC takes the full rx+tx pair (the fallback must never fire there).
|
||||||
|
- **`testDriver` still names "ice"** (`main.go`): the default pair discovery has no working target — bench runs pass `-a`/`-b` explicitly. Point it at the product driver (mlx5_core) when the CX-5 lands.
|
||||||
|
- **Module I2C transport is ixgbe-only** (`phy.go`, `openBCM`): the sff_i2c debugfs path. The CX-5 needs the MCIA answer (question 1) and a second transport arm.
|
||||||
|
|||||||
+6
-6
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Committed tree
|
## Committed tree
|
||||||
|
|
||||||
AF_PACKET raw sockets everywhere (`sock.go`); flow-director steering; per-packet-MAC-rx-stamped rate buckets (`SO_TIMESTAMPING` cmsg, `rx_filter=ALL` as a hard host check — nics/README.md for what that demands of the NIC); read-time-stamped NIC-counter rates; hardware-timestamped length probes (`probe.go`); framebuffer UI; harness.
|
AF_PACKET raw sockets everywhere (`sock.go`); flow-director steering; per-packet-MAC-rx-stamped rate buckets (`SO_TIMESTAMPING` cmsg, `rx_filter=ALL` as a hard host check — nics/README.md for what that demands of the NIC; **temporarily bypassed** in `ts.go` so BCM work can run on the X520, which cannot stamp — the check reports yellow and the panel rates read zero there; restore to fatal for the product NIC); read-time-stamped NIC-counter rates; BCM module diagnostics (`phy.go`, over the patched-ixgbe `sff_i2c` debugfs, compound-op framing): bringup identifies both modules, forces EEE off with an AN restart, then runs the ECD — per-pair verdicts, lengths and pair maps are the length/wiring path — and every reset re-runs it, re-baselining the counters only after the diag's own link blip so it is never charged to the run; a 1 Hz poller feeds per-pair SNR margin (vs the ≈26.5 dB operating point; green ≥ 3 dB, amber ≥ 1 dB — provisional until the graded-noise run) and the corrected-error set (PCS 3.33 errored blocks/BER, PMA 1.147 fast-retrain count) to the panel and console; framebuffer UI; harness.
|
||||||
|
|
||||||
## Stashes
|
## Stashes
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ AF_PACKET raw sockets everywhere (`sock.go`); flow-director steering; per-packet
|
|||||||
|
|
||||||
| Interface | Device | Role (rules: hardware.md) |
|
| Interface | Device | Role (rules: hardware.md) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `enp1s0f0` | X520 port 0 (ixgbe) | Test pair — new Wiitek module |
|
| `enp1s0f0` | X520 port 0 (ixgbe) | Test pair — FS module |
|
||||||
| `enp1s0f1` | X520 port 1 (ixgbe) | Test pair — FS module |
|
| `enp1s0f1` | X520 port 1 (ixgbe) | Test pair — FS module |
|
||||||
| `enp3s0f0np0` / `enp3s0f1np1` | X710 (i40e) | Noise pair (has been `enp4s0f*` across reboots) |
|
| `enp3s0f0np0` / `enp3s0f1np1` | X710 (i40e) | Noise pair (has been `enp4s0f*` across reboots) |
|
||||||
| `enp88s0` | igc | LAN uplink, default route; sibling `enp89s0` is dark |
|
| `enp88s0` | igc | LAN uplink, default route; sibling `enp89s0` is dark |
|
||||||
@@ -26,10 +26,10 @@ AF_PACKET raw sockets everywhere (`sock.go`); flow-director steering; per-packet
|
|||||||
|
|
||||||
| Item | Status | Notes |
|
| Item | Status | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| X520-DA2 | **Installed** in the single PCIe slot (E810 out); PCIe 5 GT/s ×8 | Port 0 `enp1s0f0` = new Wiitek (SN WAMZ012606X039U); port 1 `enp1s0f1` = FS (SN S2433774168); cable linked at 10G. Stock ixgbe needs `allow_unsupported_sfp=1` — the *FS* trips qualification (hardware.md) |
|
| X520-DA2 | **Installed** in the single PCIe slot (E810 out); PCIe 5 GT/s ×8 | Both ports FS (port 1 SN S2433774168); cable linked at 10G. Stock ixgbe needs `allow_unsupported_sfp=1` — the *FS* trips qualification (hardware.md) |
|
||||||
| ConnectX-5 | **Ordered** (dual SFP28, PCIe x8) — the product NIC candidate | mlx5 is the one driver meeting the full requirement set: stamps every packet, shared PHC across ports, native ETHER_FLOW steering. Open: MCIA diagnostics questions (nics/connectx-5/). Arrival notes: ports may ship in InfiniBand mode (`mlxconfig set LINK_TYPE_P1=2 LINK_TYPE_P2=2`); SFP+ drops into SFP28 cages at 10G; check `mlx5_ib` vs channel changes (the irdma lesson) |
|
| ConnectX-5 | **Ordered** (dual SFP28, PCIe x8) — the product NIC candidate | mlx5 is the one driver meeting the full requirement set: stamps every packet, shared PHC across ports, native ETHER_FLOW steering. Open: MCIA diagnostics questions (nics/connectx-5/). Arrival notes: ports may ship in InfiniBand mode (`mlxconfig set LINK_TYPE_P1=2 LINK_TYPE_P2=2`); SFP+ drops into SFP28 cages at 10G; check `mlx5_ib` vs channel changes (the irdma lesson) |
|
||||||
| Replacement Wiiteks | Arrived; one in X520 port 0 | Originals bricked by register exploration — modules/wiitek/ trap first |
|
| Replacement Wiiteks | Arrived; on the shelf | Originals bricked by register exploration — modules/wiitek/ trap first |
|
||||||
| FS SFP-10G-T-100 ×2 | In hand; one in X520 port 1 | BCM84891L, documented, robust |
|
| FS SFP-10G-T-100 ×2 | Both in the X520 test pair | BCM84891L, documented, robust. **2× FS at both ends is the expected product module config** unless mixed ends prove wanted |
|
||||||
| Fibergaga SFP-10G-T-30M | In hand | Aquantia, RollBall, the documented oracle |
|
| Fibergaga SFP-10G-T-30M | In hand | Aquantia, RollBall, the documented oracle |
|
||||||
| 10Gtek | In hand | Claims SFP-10G-SR, still copper RJ45; filler, not in the test set |
|
| 10Gtek | In hand | Claims SFP-10G-SR, still copper RJ45; filler, not in the test set |
|
||||||
| E810 | Out of the box | Patched ice + `sff_i2c` remains useful only if it returns for read-side work |
|
| E810 | Out of the box | Patched ice + `sff_i2c` remains useful only if it returns for read-side work |
|
||||||
@@ -64,5 +64,5 @@ In `~/work/` alongside the phydiag artifacts, ready to fold into the repo's `ker
|
|||||||
|
|
||||||
## Open items
|
## Open items
|
||||||
|
|
||||||
- **BCM ECD works** (recipe recovered from the OpenBCM SDK, validated on the FS — modules/fs/): per-pair lengths meter-accurate against a known ~45 m cable. Remaining work is characterizing the link blip the run causes (length is a between-runs operation until then). The FS ECD-chapter ask is now confirmation, not unblocking.
|
- **BCM ECD works** (recipe recovered from the OpenBCM SDK, validated on the FS — modules/fs/): per-pair lengths meter-accurate against a known ~45 m cable. cabletest runs it at bringup and on every reset, re-baselining counters after the relink so the blip is never charged (`phy.go`). Remaining work is characterizing the link blip the run causes (length stays a between-measurements operation until then). The FS ECD-chapter ask is now confirmation, not unblocking.
|
||||||
- **Pre-FEC verification** on the Aquantia — counters documented; needs the graded-noise correlation run (design: modules/fibergaga/).
|
- **Pre-FEC verification** on the Aquantia — counters documented; needs the graded-noise correlation run (design: modules/fibergaga/).
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ type mountSpec struct {
|
|||||||
var wantMounts = []mountSpec{
|
var wantMounts = []mountSpec{
|
||||||
{"/proc", "proc", unix.PROC_SUPER_MAGIC, unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC},
|
{"/proc", "proc", unix.PROC_SUPER_MAGIC, unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC},
|
||||||
{"/sys", "sysfs", unix.SYSFS_MAGIC, unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC},
|
{"/sys", "sysfs", unix.SYSFS_MAGIC, unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC},
|
||||||
|
// The module I2C transport lives behind debugfs.
|
||||||
|
{"/sys/kernel/debug", "debugfs", unix.DEBUGFS_MAGIC, unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC},
|
||||||
// devtmpfs reports itself as tmpfs, which is why the magic alone is not
|
// devtmpfs reports itself as tmpfs, which is why the magic alone is not
|
||||||
// enough to tell it apart from the directory it would be mounted over.
|
// enough to tell it apart from the directory it would be mounted over.
|
||||||
{"/dev", "devtmpfs", unix.TMPFS_MAGIC, unix.MS_NOSUID},
|
{"/dev", "devtmpfs", unix.TMPFS_MAGIC, unix.MS_NOSUID},
|
||||||
|
|||||||
@@ -37,12 +37,7 @@ type direction struct {
|
|||||||
streams []lossWindow
|
streams []lossWindow
|
||||||
txFDs []int
|
txFDs []int
|
||||||
rxFDs []int
|
rxFDs []int
|
||||||
|
statFD int
|
||||||
probeSpec *frameSpec
|
|
||||||
probeTxFD int
|
|
||||||
probeRxFD int
|
|
||||||
statFD int
|
|
||||||
cable *cableStats
|
|
||||||
|
|
||||||
// Guards everything the sampler touches. The counters are read on their own
|
// Guards everything the sampler touches. The counters are read on their own
|
||||||
// clock and drawn on another, and the two must not read them at once:
|
// clock and drawn on another, and the two must not read them at once:
|
||||||
@@ -220,16 +215,17 @@ func (d *direction) reset() {
|
|||||||
d.base = d.capture()
|
d.base = d.capture()
|
||||||
d.win.push(d.base)
|
d.win.push(d.base)
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
d.cable.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the new start time, so the uptime shown alongside the totals counts
|
// Returns the new start time, so the uptime shown alongside the totals counts
|
||||||
// from the reset rather than from launch.
|
// from the reset rather than from launch.
|
||||||
func resetAll(dirs []*direction, stats *streamTable) time.Time {
|
func resetAll(dirs []*direction, mods []*phyModule, stats *streamTable) time.Time {
|
||||||
for _, d := range dirs {
|
for _, d := range dirs {
|
||||||
d.reset()
|
d.reset()
|
||||||
}
|
}
|
||||||
|
for _, m := range mods {
|
||||||
|
m.reset()
|
||||||
|
}
|
||||||
stats.sinceHeader = 0
|
stats.sinceHeader = 0
|
||||||
fmt.Println(stats.rule("counters reset"))
|
fmt.Println(stats.rule("counters reset"))
|
||||||
return time.Now()
|
return time.Now()
|
||||||
@@ -248,15 +244,18 @@ func gbps(bytes, frames uint64, secs float64) float64 {
|
|||||||
var intervalCols = []colSpec{
|
var intervalCols = []colSpec{
|
||||||
{group: "NOW", title: "bits/s", width: 9, right: true},
|
{group: "NOW", title: "bits/s", width: 9, right: true},
|
||||||
{group: "NOW", title: "packets/s", width: 9, right: true},
|
{group: "NOW", title: "packets/s", width: 9, right: true},
|
||||||
|
{group: "NOW", title: "snr", width: 6, right: true},
|
||||||
{group: "NOW", title: "lost", width: 7, right: true},
|
{group: "NOW", title: "lost", width: 7, right: true},
|
||||||
{group: "NOW", title: "corrupt", width: 7, right: true},
|
{group: "NOW", title: "corrupt", width: 7, right: true},
|
||||||
{group: "NOW", title: "link", width: 7, right: true},
|
{group: "NOW", title: "link", width: 7, right: true},
|
||||||
{group: "NOW", title: "internal", width: 8, right: true},
|
{group: "NOW", title: "internal", width: 8, right: true},
|
||||||
|
{group: "NOW", title: "corrected", width: 9, right: true},
|
||||||
{group: "NOW", title: "noise", width: 7, right: true},
|
{group: "NOW", title: "noise", width: 7, right: true},
|
||||||
{group: "OVERALL", title: "elapsed", width: 9, right: true},
|
{group: "OVERALL", title: "elapsed", width: 9, right: true},
|
||||||
{group: "OVERALL", title: "packets", width: 9, right: true},
|
{group: "OVERALL", title: "packets", width: 9, right: true},
|
||||||
{group: "OVERALL", title: "bytes", width: 9, right: true},
|
{group: "OVERALL", title: "bytes", width: 9, right: true},
|
||||||
{group: "OVERALL", title: "metres", width: 6, right: true},
|
{group: "OVERALL", title: "metres", width: 6, right: true},
|
||||||
|
{group: "OVERALL", title: "corrected", width: 9, right: true},
|
||||||
{group: "OVERALL", title: "lost", width: 9, right: true},
|
{group: "OVERALL", title: "lost", width: 9, right: true},
|
||||||
{group: "OVERALL", title: "corrupt", width: 9, right: true},
|
{group: "OVERALL", title: "corrupt", width: 9, right: true},
|
||||||
{group: "OVERALL", title: "link", width: 9, right: true},
|
{group: "OVERALL", title: "link", width: 9, right: true},
|
||||||
@@ -271,7 +270,6 @@ type view struct {
|
|||||||
rxFrames, rxBytes uint64
|
rxFrames, rxBytes uint64
|
||||||
since errs
|
since errs
|
||||||
window errs
|
window errs
|
||||||
cable cableView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func errsBetween(b, n counterSet) errs {
|
func errsBetween(b, n counterSet) errs {
|
||||||
@@ -297,7 +295,6 @@ func (d *direction) counters(now counterSet) view {
|
|||||||
return view{
|
return view{
|
||||||
rxFrames: now.s.rxFrames - d.base.s.rxFrames,
|
rxFrames: now.s.rxFrames - d.base.s.rxFrames,
|
||||||
rxBytes: now.s.rxBytes - d.base.s.rxBytes,
|
rxBytes: now.s.rxBytes - d.base.s.rxBytes,
|
||||||
cable: d.cable.view(),
|
|
||||||
since: errsBetween(d.base, now),
|
since: errsBetween(d.base, now),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,7 +326,7 @@ func (d *direction) displayView() view {
|
|||||||
n := d.win.count()
|
n := d.win.count()
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
return view{cable: d.cable.view()}
|
return view{}
|
||||||
}
|
}
|
||||||
v := d.counters(d.win.at(n - 1))
|
v := d.counters(d.win.at(n - 1))
|
||||||
if n >= 2 {
|
if n >= 2 {
|
||||||
@@ -344,19 +341,22 @@ func (d *direction) displayView() view {
|
|||||||
// The same figures the panel draws, in the same order: the last second as
|
// The same figures the panel draws, in the same order: the last second as
|
||||||
// rates and error flags with the noise cable riding at the end of them, then
|
// rates and error flags with the noise cable riding at the end of them, then
|
||||||
// everything since the reset.
|
// everything since the reset.
|
||||||
func totalRow(elapsed time.Duration, v view, target float64, length string, noiseMissing uint64) []string {
|
func totalRow(elapsed time.Duration, v view, target float64, phy phyDisplay, noiseMissing uint64) []string {
|
||||||
return []string{
|
return []string{
|
||||||
rateCell(v.rxGbps*1e9, target*1e9),
|
rateCell(v.rxGbps*1e9, target*1e9),
|
||||||
scaleSI(v.rxPPS),
|
scaleSI(v.rxPPS),
|
||||||
|
snrCell(phy),
|
||||||
flagCell(v.window.lost),
|
flagCell(v.window.lost),
|
||||||
flagCell(v.window.corrupt),
|
flagCell(v.window.corrupt),
|
||||||
flagCell(v.window.link),
|
flagCell(v.window.link),
|
||||||
flagCell(v.window.internal),
|
flagCell(v.window.internal),
|
||||||
|
correctedFlag(phy.recent),
|
||||||
flagCell(noiseMissing),
|
flagCell(noiseMissing),
|
||||||
scaleTime(elapsed),
|
scaleTime(elapsed),
|
||||||
scaleCount(v.rxFrames),
|
scaleCount(v.rxFrames),
|
||||||
scaleCount(v.rxBytes),
|
scaleCount(v.rxBytes),
|
||||||
length,
|
phy.metres,
|
||||||
|
correctedCell(phy.corrected),
|
||||||
statusCell(v.since.lost),
|
statusCell(v.since.lost),
|
||||||
statusCell(v.since.corrupt),
|
statusCell(v.since.corrupt),
|
||||||
statusCell(v.since.link),
|
statusCell(v.since.link),
|
||||||
@@ -382,7 +382,6 @@ func buildDirection(label string, tx, rx endpoint) (*direction, error) {
|
|||||||
d := &direction{
|
d := &direction{
|
||||||
txStats: txs,
|
txStats: txs,
|
||||||
streams: newLossWindows(txs),
|
streams: newLossWindows(txs),
|
||||||
cable: newCableStats(),
|
|
||||||
}
|
}
|
||||||
// Held open for the life of the run: the stats ioctl is issued five times a
|
// Held open for the life of the run: the stats ioctl is issued five times a
|
||||||
// second and reopening a socket for each one is pure overhead.
|
// second and reopening a socket for each one is pure overhead.
|
||||||
@@ -412,8 +411,8 @@ func buildDirection(label string, tx, rx endpoint) (*direction, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s rx socket for 0x%04x: %w", label, et, err)
|
return nil, fmt.Errorf("%s rx socket for 0x%04x: %w", label, et, err)
|
||||||
}
|
}
|
||||||
// The mac already stamps every frame for the probe's sake, so this only
|
// The mac already stamps every frame; this only asks for the stamp to be
|
||||||
// asks for the stamp to be delivered.
|
// delivered.
|
||||||
if err := enableRxTimestamps(fd); err != nil {
|
if err := enableRxTimestamps(fd); err != nil {
|
||||||
return nil, fmt.Errorf("%s rx timestamps for 0x%04x: %w", label, et, err)
|
return nil, fmt.Errorf("%s rx timestamps for 0x%04x: %w", label, et, err)
|
||||||
}
|
}
|
||||||
@@ -421,26 +420,6 @@ func buildDirection(label string, tx, rx endpoint) (*direction, error) {
|
|||||||
d.rxStats = append(d.rxStats, &rxStats{})
|
d.rxStats = append(d.rxStats, &rxStats{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deliberately given no flow rule: a few frames a second does not need a
|
|
||||||
// queue of its own, and the stamps are taken at the wire either way.
|
|
||||||
d.probeSpec = newFrameSpec(rx.mac, tx.mac, probeEther, []int{probeSize})
|
|
||||||
fd, err := openTxSocket(tx.idx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s probe tx socket: %w", label, err)
|
|
||||||
}
|
|
||||||
if err := enableTxTimestamps(fd); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s probe tx timestamps: %w", label, err)
|
|
||||||
}
|
|
||||||
d.probeTxFD = fd
|
|
||||||
fd, err = openRxSocket(rx.idx, probeEther)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s probe rx socket: %w", label, err)
|
|
||||||
}
|
|
||||||
if err := enableRxTimestamps(fd); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s probe rx timestamps: %w", label, err)
|
|
||||||
}
|
|
||||||
d.probeRxFD = fd
|
|
||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,22 +458,6 @@ func (d *direction) start(wg *sync.WaitGroup, done *atomic.Bool, rxReady *sync.W
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
sender := &probeSender{fd: d.probeTxFD, spec: d.probeSpec, stats: d.cable}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer holdPanic()
|
|
||||||
sender.run(done, startTx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
receiver := &probeReceiver{fd: d.probeRxFD, stats: d.cable, ready: rxReady}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer holdPanic()
|
|
||||||
receiver.run(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
@@ -510,8 +473,6 @@ func (d *direction) close() {
|
|||||||
for _, fd := range d.rxFDs {
|
for _, fd := range d.rxFDs {
|
||||||
unix.Close(fd)
|
unix.Close(fd)
|
||||||
}
|
}
|
||||||
unix.Close(d.probeTxFD)
|
|
||||||
unix.Close(d.probeRxFD)
|
|
||||||
unix.Close(d.statFD)
|
unix.Close(d.statFD)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,8 +480,6 @@ const (
|
|||||||
numStreams = 7
|
numStreams = 7
|
||||||
batchSize = 64
|
batchSize = 64
|
||||||
|
|
||||||
probeEther uint16 = etherBase + numStreams
|
|
||||||
|
|
||||||
testDriver = "ice"
|
testDriver = "ice"
|
||||||
|
|
||||||
// A constant rather than the negotiated speed, since this has to come up
|
// A constant rather than the negotiated speed, since this has to come up
|
||||||
@@ -538,10 +497,9 @@ func main() {
|
|||||||
// which ethN shifts with every driver built into the kernel.
|
// which ethN shifts with every driver built into the kernel.
|
||||||
aName := flag.String("a", "", "first interface (default: the ice pair)")
|
aName := flag.String("a", "", "first interface (default: the ice pair)")
|
||||||
bName := flag.String("b", "", "second interface")
|
bName := flag.String("b", "", "second interface")
|
||||||
nsPerM := flag.Float64("ns-per-m", 4.85, "mean of both directions, per metre of cable")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if err := run(*aName, *bName, *nsPerM); err != nil {
|
if err := run(*aName, *bName); err != nil {
|
||||||
fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
// A clean return is ctrl-alt-delete, which the kernel hands PID 1 as a
|
// A clean return is ctrl-alt-delete, which the kernel hands PID 1 as a
|
||||||
@@ -584,7 +542,7 @@ func (s *sampler) run(done *atomic.Bool, startTx <-chan struct{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(aName, bName string, nsPerM float64) (err error) {
|
func run(aName, bName string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if p := recover(); p != nil {
|
if p := recover(); p != nil {
|
||||||
if os.Getpid() != 1 {
|
if os.Getpid() != 1 {
|
||||||
@@ -640,6 +598,12 @@ func run(aName, bName string, nsPerM float64) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modules, cable, moduleChecks := moduleBringup([2]string{a.name, b.name})
|
||||||
|
if err := reportChecks("MODULES", moduleChecks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
diag := newCableDiag(modules, cable)
|
||||||
|
|
||||||
var dirs []*direction
|
var dirs []*direction
|
||||||
for _, p := range [][2]endpoint{{a, b}, {b, a}} {
|
for _, p := range [][2]endpoint{{a, b}, {b, a}} {
|
||||||
d, err := buildDirection(p[0].name+"->"+p[1].name, p[0], p[1])
|
d, err := buildDirection(p[0].name+"->"+p[1].name, p[0], p[1])
|
||||||
@@ -673,7 +637,7 @@ func run(aName, bName string, nsPerM float64) (err error) {
|
|||||||
var rxReady sync.WaitGroup
|
var rxReady sync.WaitGroup
|
||||||
startTx := make(chan struct{})
|
startTx := make(chan struct{})
|
||||||
for _, d := range dirs {
|
for _, d := range dirs {
|
||||||
rxReady.Add(len(d.rxFDs) + 1)
|
rxReady.Add(len(d.rxFDs))
|
||||||
}
|
}
|
||||||
for _, d := range dirs {
|
for _, d := range dirs {
|
||||||
d.start(&wg, &done, &rxReady, startTx)
|
d.start(&wg, &done, &rxReady, startTx)
|
||||||
@@ -693,6 +657,16 @@ func run(aName, bName string, nsPerM float64) (err error) {
|
|||||||
defer holdPanic()
|
defer holdPanic()
|
||||||
noise.run(&done)
|
noise.run(&done)
|
||||||
}()
|
}()
|
||||||
|
// Also ungated: SNR and temperature ride the module's own management bus,
|
||||||
|
// not the wire being measured.
|
||||||
|
for _, m := range modules {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer holdPanic()
|
||||||
|
m.run(&done)
|
||||||
|
}()
|
||||||
|
}
|
||||||
// Every return from here on stops the workers before the deferred closes
|
// Every return from here on stops the workers before the deferred closes
|
||||||
// pull their sockets out from under them: otherwise the sampler panics on a
|
// pull their sockets out from under them: otherwise the sampler panics on a
|
||||||
// closed fd and can mask the error that actually ended the run. An error
|
// closed fd and can mask the error that actually ended the run. An error
|
||||||
@@ -754,41 +728,40 @@ func run(aName, bName string, nsPerM float64) (err error) {
|
|||||||
return fmt.Errorf("%v", p)
|
return fmt.Errorf("%v", p)
|
||||||
case <-sig:
|
case <-sig:
|
||||||
return nil
|
return nil
|
||||||
|
// A reset re-measures the cable first — the cable under a reset is
|
||||||
|
// usually a new one — and the counters re-baseline when the diag's own
|
||||||
|
// link blip is over, so it is never charged to the fresh run.
|
||||||
case <-space:
|
case <-space:
|
||||||
start = resetAll(dirs, stats)
|
if diag.kick(&done) {
|
||||||
|
fmt.Println(stats.rule("measuring cable"))
|
||||||
|
}
|
||||||
|
case <-diag.completed:
|
||||||
|
start = resetAll(dirs, modules, stats)
|
||||||
case <-disp.fb.flips:
|
case <-disp.fb.flips:
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
px, py, down := touch.get()
|
px, py, down := touch.get()
|
||||||
x, y := disp.fb.fromPanel(px, py)
|
x, y := disp.fb.fromPanel(px, py)
|
||||||
if disp.holdReset(x, y, down, now) {
|
if disp.holdReset(x, y, down, now) {
|
||||||
start = resetAll(dirs, stats)
|
diag.kick(&done)
|
||||||
}
|
}
|
||||||
disp.showVersion = down && disp.versionSpot.contains(x, y)
|
disp.showVersion = down && disp.versionSpot.contains(x, y)
|
||||||
for i, d := range dirs {
|
for i, d := range dirs {
|
||||||
views[i] = d.displayView()
|
views[i] = d.displayView()
|
||||||
}
|
}
|
||||||
// Empty until the probe has a stamp from each direction, so the
|
info, measuring := diag.snapshot()
|
||||||
// panel shows nothing there rather than a placeholder.
|
phy := phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view())
|
||||||
cable := ""
|
if err := disp.render(totalView(views), now.Sub(start), phy,
|
||||||
if m, ok := cableMetres(views, nsPerM); ok {
|
|
||||||
cable = fmt.Sprintf("%.1f", m)
|
|
||||||
}
|
|
||||||
if err := disp.render(totalView(views), now.Sub(start), cable,
|
|
||||||
noise.missing()); err != nil {
|
noise.missing()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case now := <-tick.C:
|
case now := <-tick.C:
|
||||||
elapsed := now.Sub(start)
|
elapsed := now.Sub(start)
|
||||||
// Length needs both directions, so every row is sampled before any of
|
|
||||||
// them is printed.
|
|
||||||
for i, d := range dirs {
|
for i, d := range dirs {
|
||||||
rows[i] = d.displayView()
|
rows[i] = d.displayView()
|
||||||
}
|
}
|
||||||
length := "-"
|
info, measuring := diag.snapshot()
|
||||||
if m, ok := cableMetres(rows, nsPerM); ok {
|
phy := phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view())
|
||||||
length = fmt.Sprintf("%.1f", m)
|
for _, line := range stats.emit(totalRow(elapsed, totalView(rows), target, phy,
|
||||||
}
|
|
||||||
for _, line := range stats.emit(totalRow(elapsed, totalView(rows), target, length,
|
|
||||||
noise.missing())) {
|
noise.missing())) {
|
||||||
fmt.Println(line)
|
fmt.Println(line)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ func TestErrsBetweenBuckets(t *testing.T) {
|
|||||||
func TestResetDoesNotUnderflowTotals(t *testing.T) {
|
func TestResetDoesNotUnderflowTotals(t *testing.T) {
|
||||||
d := &direction{
|
d := &direction{
|
||||||
win: newRateWindow(8),
|
win: newRateWindow(8),
|
||||||
cable: newCableStats(),
|
|
||||||
rxStats: []*rxStats{{}},
|
rxStats: []*rxStats{{}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const (
|
|||||||
noiseDownSpan = 5 * time.Second
|
noiseDownSpan = 5 * time.Second
|
||||||
noiseFrameGap = 10 * time.Millisecond
|
noiseFrameGap = 10 * time.Millisecond
|
||||||
|
|
||||||
noiseEther uint16 = probeEther + 1
|
noiseEther uint16 = etherBase + numStreams
|
||||||
)
|
)
|
||||||
|
|
||||||
// Kernel names shift with which drivers are built in, since ethN is handed out
|
// Kernel names shift with which drivers are built in, since ethN is handed out
|
||||||
|
|||||||
@@ -0,0 +1,808 @@
|
|||||||
|
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
|
||||||
|
bcmCmdGetSNR uint16 = 0x8030
|
||||||
|
|
||||||
|
bcmRegECDCtrl uint16 = 0x4006
|
||||||
|
bcmRegECDResult uint16 = 0xA896
|
||||||
|
bcmRegECDLen uint16 = 0xA897
|
||||||
|
|
||||||
|
bcmPHYIDHi = 0x3590
|
||||||
|
bcmPHYIDLo = 0x5081
|
||||||
|
|
||||||
|
bcmReadDelayUs = 3000
|
||||||
|
bcmRetryDelayUs = 10000
|
||||||
|
|
||||||
|
bcmStatusPoll = 100 * time.Millisecond
|
||||||
|
bcmStatusTries = 30
|
||||||
|
|
||||||
|
ecdPoll = 200 * time.Millisecond
|
||||||
|
ecdDeadline = 50 * time.Second
|
||||||
|
|
||||||
|
pairIdentityMap = 0xE4
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pairOK = 1
|
||||||
|
pairOpen = 2
|
||||||
|
pairShort = 3
|
||||||
|
pairXtalk = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var pairVerdicts = map[int]string{
|
||||||
|
pairOK: "ok", pairOpen: "OPEN", pairShort: "SHORT", pairXtalk: "XTALK",
|
||||||
|
}
|
||||||
|
|
||||||
|
type bcm struct {
|
||||||
|
ifname string
|
||||||
|
path string
|
||||||
|
|
||||||
|
// Serializes the multi-op sequences — a handler command, an ECD run — that
|
||||||
|
// would corrupt each other interleaved. Single register reads ride bare:
|
||||||
|
// the compound op makes each one atomic on the wire.
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func openBCM(ifname string) (*bcm, 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)
|
||||||
|
}
|
||||||
|
b := &bcm{
|
||||||
|
ifname: ifname,
|
||||||
|
path: "/sys/kernel/debug/ixgbe/" + filepath.Base(devLink) + "/sff_i2c",
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(b.path); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w (patched ixgbe?)", ifname, err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) op(cmd string) (string, error) {
|
||||||
|
fd, err := unix.Open(b.path, unix.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s: %w", b.path, err)
|
||||||
|
}
|
||||||
|
defer unix.Close(fd)
|
||||||
|
if _, err := unix.Write(fd, []byte(cmd)); err != nil {
|
||||||
|
return "", fmt.Errorf("%s %q: %w", b.ifname, cmd, err)
|
||||||
|
}
|
||||||
|
buf := make([]byte, 256)
|
||||||
|
n, err := unix.Read(fd, buf)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s %q: %w", b.ifname, cmd, err)
|
||||||
|
}
|
||||||
|
resp := strings.TrimSpace(string(buf[:n]))
|
||||||
|
if !strings.HasPrefix(resp, "ok") {
|
||||||
|
return "", fmt.Errorf("%s %q: %s", b.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// One write-STOP-delay-read transaction under a single bus hold, so the
|
||||||
|
// driver's own SFP traffic can never consume the bridge's pending data.
|
||||||
|
func (b *bcm) 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 := b.op(sb.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseHexBytes(resp, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) eeprom(off byte, n int) ([]byte, error) {
|
||||||
|
return b.compound(0xA0, 0xA1, 500, n, []byte{off})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) waitStatus(want func(uint16) bool) (uint16, error) {
|
||||||
|
var st uint16
|
||||||
|
for i := 0; i < bcmStatusTries; i++ {
|
||||||
|
var err error
|
||||||
|
st, err = b.mdioRead(bcmMMDVendor, bcmRegStatus)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if want(st) {
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
time.Sleep(bcmStatusPoll)
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s: command handler stuck, status %#04x", b.ifname, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handler never clears DATA registers it does not use, so every SET must
|
||||||
|
// pass its full parameter set and every GET must pass none.
|
||||||
|
func (b *bcm) command(code uint16, params ...uint16) ([5]uint16, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
var data [5]uint16
|
||||||
|
if _, err := b.waitStatus(func(st uint16) bool {
|
||||||
|
return st != bcmStInProgress && st != bcmStBusy
|
||||||
|
}); err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
for i, p := range params {
|
||||||
|
if err := b.mdioWrite(bcmMMDVendor, bcmRegData1+uint16(i), p); err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := b.mdioWrite(bcmMMDVendor, bcmRegCmd, code); err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
st, err := b.waitStatus(func(st uint16) bool {
|
||||||
|
return st == bcmStPass || st == bcmStError
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
if st == bcmStError {
|
||||||
|
return data, fmt.Errorf("%s: command %#04x returned ERROR", b.ifname, code)
|
||||||
|
}
|
||||||
|
for i := range data {
|
||||||
|
data[i], err = b.mdioRead(bcmMMDVendor, bcmRegData1+uint16(i))
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) identify() (string, error) {
|
||||||
|
hi, err := b.mdioRead(1, 2)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
lo, err := b.mdioRead(1, 3)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if hi != bcmPHYIDHi || lo != bcmPHYIDLo {
|
||||||
|
return "", fmt.Errorf("%s: PHY ID %#04x:%#04x, want %#04x:%#04x",
|
||||||
|
b.ifname, hi, lo, bcmPHYIDHi, bcmPHYIDLo)
|
||||||
|
}
|
||||||
|
sn, err := b.eeprom(68, 16)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "BCM84891L sn " + strings.TrimSpace(string(sn)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (b *bcm) linkUp() (bool, error) {
|
||||||
|
if _, err := b.mdioRead(1, 1); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
v, err := b.mdioRead(1, 1)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return v&0x0004 != 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) forceEEEOff() error {
|
||||||
|
_, err := b.command(bcmCmdSetEEEMode, 0x0000, 0x0000, 0x7A12, 0x0480, 0x0000)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) restartAN() error {
|
||||||
|
v, err := b.mdioRead(7, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.mdioWrite(7, 0, v|0x0200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) eeeAdvert() (uint16, error) {
|
||||||
|
return b.mdioRead(7, 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) pairMap() (byte, error) {
|
||||||
|
d, err := b.command(bcmCmdGetPairSwap)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return byte(d[1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) snr() ([4]float64, error) {
|
||||||
|
var out [4]float64
|
||||||
|
d, err := b.command(bcmCmdGetSNR)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
for i := range out {
|
||||||
|
out[i] = float64(d[i+1]) / 10
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) pcsLatch() (blocks, ber uint64, err error) {
|
||||||
|
v, err := b.mdioRead(3, 33)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return uint64(v & 0xFF), uint64((v >> 8) & 0x3F), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) fastRetrainCount() (uint16, error) {
|
||||||
|
v, err := b.mdioRead(1, 147)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return v >> 11, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecdResult struct {
|
||||||
|
verdicts [4]int
|
||||||
|
metres [4]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bcm) cableDiag() (ecdResult, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
var res ecdResult
|
||||||
|
ctrl, err := b.mdioRead(bcmMMDVendor, bcmRegECDCtrl)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
if err := b.mdioWrite(bcmMMDVendor, bcmRegECDCtrl, ctrl&^0xF400|0x8400); err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(ecdDeadline)
|
||||||
|
for {
|
||||||
|
ctrl, err = b.mdioRead(bcmMMDVendor, bcmRegECDCtrl)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
if ctrl&0x0800 == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return res, fmt.Errorf("%s: cable diag still busy after %s", b.ifname, ecdDeadline)
|
||||||
|
}
|
||||||
|
time.Sleep(ecdPoll)
|
||||||
|
}
|
||||||
|
v, err := b.mdioRead(1, bcmRegECDResult)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
for i := range res.verdicts {
|
||||||
|
res.verdicts[i] = int(v>>(4*i)) & 0xF
|
||||||
|
m, err := b.mdioRead(1, bcmRegECDLen+uint16(i))
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
res.metres[i] = int(m)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
phyInterval = time.Second
|
||||||
|
phyStale = 5 * time.Second
|
||||||
|
phyMaxDark = 30
|
||||||
|
linkWaitSpan = 25 * time.Second
|
||||||
|
linkWaitPoll = time.Second
|
||||||
|
|
||||||
|
snrOperatingPoint = 26.5
|
||||||
|
snrGoodMargin = 3.0
|
||||||
|
snrWarnMargin = 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
type phyModule struct {
|
||||||
|
bcm *bcm
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
sampled bool
|
||||||
|
lastOK time.Time
|
||||||
|
link bool
|
||||||
|
haveSNR bool
|
||||||
|
snr [4]float64
|
||||||
|
blocks uint64
|
||||||
|
ber uint64
|
||||||
|
retrains uint64
|
||||||
|
recentDelta uint64
|
||||||
|
primed bool
|
||||||
|
retrainCount uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *phyModule) poll() error {
|
||||||
|
link, err := m.bcm.linkUp()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var snr [4]float64
|
||||||
|
if link {
|
||||||
|
if snr, err = m.bcm.snr(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blocks, ber, err := m.bcm.pcsLatch()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
count, err := m.bcm.fastRetrainCount()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.sampled = true
|
||||||
|
m.lastOK = time.Now()
|
||||||
|
m.link = link
|
||||||
|
m.haveSNR = link
|
||||||
|
m.snr = snr
|
||||||
|
// The first poll after a baseline drains what the latches gathered during
|
||||||
|
// the bringup or diag retrain, which predates the run: it only establishes
|
||||||
|
// the origin. The retrain counter is 5 bits and rolls over, so only its
|
||||||
|
// forward motion is kept.
|
||||||
|
if m.primed {
|
||||||
|
delta := blocks + ber + uint64((count-m.retrainCount)&0x1F)
|
||||||
|
m.blocks += blocks
|
||||||
|
m.ber += ber
|
||||||
|
m.retrains += uint64((count - m.retrainCount) & 0x1F)
|
||||||
|
m.recentDelta = delta
|
||||||
|
} else {
|
||||||
|
m.recentDelta = 0
|
||||||
|
m.primed = true
|
||||||
|
}
|
||||||
|
m.retrainCount = count
|
||||||
|
m.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A tester that quietly loses its SNR eye goes on reporting a clean link, so a
|
||||||
|
// transport that stays dark past every transient explanation stops the run.
|
||||||
|
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
|
||||||
|
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 && m.haveSNR,
|
||||||
|
blocks: m.blocks,
|
||||||
|
ber: m.ber,
|
||||||
|
retrain: m.retrains,
|
||||||
|
}
|
||||||
|
if v.fresh {
|
||||||
|
v.recent = m.recentDelta
|
||||||
|
}
|
||||||
|
for i, s := range m.snr {
|
||||||
|
v.margins[i] = s - snrOperatingPoint
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
type cableInfo struct {
|
||||||
|
ecd ecdResult
|
||||||
|
maps [2]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// The four pair lengths of one healthy cable disagree by a few metres of twist
|
||||||
|
// rate, so the cable's length is shown as their mean.
|
||||||
|
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 end counts.
|
||||||
|
func pairSwapped(i int, maps [2]byte) bool {
|
||||||
|
return int(maps[0]>>(2*i))&3 != i || int(maps[1]>>(2*i))&3 != i
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cable as one figure and one judgment: the mean length of its healthy
|
||||||
|
// pairs, red when the diag found a fault, amber when a pair arrived swapped.
|
||||||
|
// Per-pair detail stays on the console — pair letters don't correlate back to
|
||||||
|
// wires by eye.
|
||||||
|
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.maps) {
|
||||||
|
anySwap = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s := cable.metresString()
|
||||||
|
switch {
|
||||||
|
case !anyData:
|
||||||
|
return "-", clsNone
|
||||||
|
case anyFault:
|
||||||
|
return s, clsBad
|
||||||
|
case anySwap:
|
||||||
|
return s, clsWarn
|
||||||
|
}
|
||||||
|
return s, clsGood
|
||||||
|
}
|
||||||
|
|
||||||
|
// The worse of the two receivers' margins, worst pair across the cable.
|
||||||
|
func phyDisplayFrom(cable cableInfo, measuring bool, a, b phyModView) phyDisplay {
|
||||||
|
d := phyDisplay{
|
||||||
|
haveSNR: a.fresh && b.fresh && a.link && b.link,
|
||||||
|
corrected: a.blocks + a.ber + a.retrain + b.blocks + b.ber + b.retrain,
|
||||||
|
recent: a.recent + b.recent,
|
||||||
|
}
|
||||||
|
if d.haveSNR {
|
||||||
|
d.worstMargin = min(a.margins[0], b.margins[0])
|
||||||
|
for i := range a.margins {
|
||||||
|
if m := min(a.margins[i], b.margins[i]); m < d.worstMargin {
|
||||||
|
d.worstMargin = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.metres, d.metresClass = cableSummary(cable, measuring)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitLink(mods []*phyModule, done *atomic.Bool) (time.Duration, bool, error) {
|
||||||
|
start := time.Now()
|
||||||
|
deadline := start.Add(linkWaitSpan)
|
||||||
|
for {
|
||||||
|
up := true
|
||||||
|
for _, m := range mods {
|
||||||
|
v, err := m.bcm.linkUp()
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, err
|
||||||
|
}
|
||||||
|
up = up && v
|
||||||
|
}
|
||||||
|
if up {
|
||||||
|
return time.Since(start), true, nil
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) || (done != nil && done.Load()) {
|
||||||
|
return time.Since(start), false, nil
|
||||||
|
}
|
||||||
|
time.Sleep(linkWaitPoll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole cable picture in one pass: the ECD's per-pair verdicts and
|
||||||
|
// lengths, then — after the blip it causes has settled — both ends' pair
|
||||||
|
// maps, read post-link so the MDI resolution is the fresh one.
|
||||||
|
func measureCable(mods []*phyModule, waitRelink bool, done *atomic.Bool) (cableInfo, bool, error) {
|
||||||
|
var c cableInfo
|
||||||
|
var err error
|
||||||
|
c.ecd, err = mods[0].bcm.cableDiag()
|
||||||
|
if err != nil {
|
||||||
|
return c, false, err
|
||||||
|
}
|
||||||
|
relinked := false
|
||||||
|
if waitRelink {
|
||||||
|
if _, relinked, err = waitLink(mods, done); err != nil {
|
||||||
|
return c, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, m := range mods {
|
||||||
|
if c.maps[i], err = m.bcm.pairMap(); err != nil {
|
||||||
|
return c, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, relinked, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owns the cable picture after bringup: a reset re-measures — the cable under
|
||||||
|
// a reset is usually a different cable — and the counters re-baseline only
|
||||||
|
// once the diag's own link blip is over, so it is never charged to the run.
|
||||||
|
type cableDiag struct {
|
||||||
|
mods []*phyModule
|
||||||
|
completed chan struct{}
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
info cableInfo
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCableDiag(mods []*phyModule, info cableInfo) *cableDiag {
|
||||||
|
return &cableDiag{mods: mods, completed: make(chan struct{}, 1), info: info}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cableDiag) snapshot() (cableInfo, bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.info, c.running
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the re-measure off the display loop, so the panel keeps drawing while
|
||||||
|
// the diag and the relink take their seconds. Reports whether one started; a
|
||||||
|
// press while one is in flight is absorbed.
|
||||||
|
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 {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
c.info = info
|
||||||
|
c.running = false
|
||||||
|
c.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case c.completed <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapString(m byte) string {
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs before any socket opens: forcing EEE off retrains the link and the ECD
|
||||||
|
// blips it, and both belong before the baselines rather than under them.
|
||||||
|
func moduleBringup(names [2]string) ([]*phyModule, cableInfo, []checkResult) {
|
||||||
|
var out []checkResult
|
||||||
|
var cable cableInfo
|
||||||
|
mods := make([]*phyModule, 0, 2)
|
||||||
|
fail := func(item string, err error) ([]*phyModule, cableInfo, []checkResult) {
|
||||||
|
return nil, cable, append(out, checkResult{item: item, err: err})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
res := checkResult{item: name + " module"}
|
||||||
|
b, err := openBCM(name)
|
||||||
|
if err != nil {
|
||||||
|
return fail(res.item, err)
|
||||||
|
}
|
||||||
|
res.state, err = b.identify()
|
||||||
|
if err != nil {
|
||||||
|
return fail(res.item, err)
|
||||||
|
}
|
||||||
|
out = append(out, res)
|
||||||
|
mods = append(mods, &phyModule{bcm: b})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, m := range mods {
|
||||||
|
res := checkResult{item: names[i] + " eee", fixed: true}
|
||||||
|
if err := m.bcm.forceEEEOff(); err != nil {
|
||||||
|
return fail(res.item, err)
|
||||||
|
}
|
||||||
|
if err := m.bcm.restartAN(); err != nil {
|
||||||
|
return fail(res.item, err)
|
||||||
|
}
|
||||||
|
res.state = "forced off, retraining"
|
||||||
|
out = append(out, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := checkResult{item: "link retrain"}
|
||||||
|
took, up, err := waitLink(mods, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fail(res.item, err)
|
||||||
|
}
|
||||||
|
if up {
|
||||||
|
var adv [2]string
|
||||||
|
for i, m := range mods {
|
||||||
|
v, err := m.bcm.eeeAdvert()
|
||||||
|
if err != nil {
|
||||||
|
return fail(res.item, err)
|
||||||
|
}
|
||||||
|
adv[i] = fmt.Sprintf("%#04x", v)
|
||||||
|
if v != 0 {
|
||||||
|
res.err = fmt.Errorf("%s still advertises EEE %#04x", names[i], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.state = fmt.Sprintf("up in %.1fs, eee advert %s/%s", took.Seconds(), adv[0], adv[1])
|
||||||
|
} else {
|
||||||
|
res.state = "no link (cable unplugged?)"
|
||||||
|
}
|
||||||
|
out = append(out, res)
|
||||||
|
if res.err != nil {
|
||||||
|
return nil, cable, out
|
||||||
|
}
|
||||||
|
|
||||||
|
res = checkResult{item: "cable diag"}
|
||||||
|
cable, relinked, err := measureCable(mods, up, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fail(res.item, err)
|
||||||
|
}
|
||||||
|
res.state = verdictString(cable.ecd)
|
||||||
|
if up && !relinked {
|
||||||
|
res.err = fmt.Errorf("link did not return after cable diag")
|
||||||
|
}
|
||||||
|
out = append(out, res)
|
||||||
|
|
||||||
|
for i := range mods {
|
||||||
|
out = append(out, checkResult{
|
||||||
|
item: names[i] + " pair map",
|
||||||
|
state: mapString(cable.maps[i]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return mods, cable, out
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func freshMod(margins [4]float64) phyModView {
|
||||||
|
return phyModView{fresh: true, link: true, margins: margins}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The worst pair at the worse end is what the margin cell shows, and a fault
|
||||||
|
// paints the length red: the live margins can only make the cable look worse,
|
||||||
|
// never repair a fault.
|
||||||
|
func TestPhyDisplayWorstMarginAndFault(t *testing.T) {
|
||||||
|
cable := cableInfo{
|
||||||
|
ecd: ecdResult{verdicts: [4]int{pairOK, pairOpen, pairOK, pairOK}, metres: [4]int{45, 12, 41, 46}},
|
||||||
|
maps: [2]byte{pairIdentityMap, pairIdentityMap},
|
||||||
|
}
|
||||||
|
a := freshMod([4]float64{5, 5, 2, 0.5})
|
||||||
|
b := freshMod([4]float64{4, 5, 5, 5})
|
||||||
|
d := phyDisplayFrom(cable, false, a, b)
|
||||||
|
|
||||||
|
if !d.haveSNR || d.worstMargin != 0.5 {
|
||||||
|
t.Errorf("worstMargin = %v (have %v), want 0.5", d.worstMargin, d.haveSNR)
|
||||||
|
}
|
||||||
|
if d.metres != "44" || d.metresClass != clsBad {
|
||||||
|
t.Errorf("metres = %q class %d, want the healthy mean 44 painted bad", d.metres, d.metresClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A swap at either end marks the cable: the far end resolving MDI-X on its
|
||||||
|
// own is the usual way a crossover shows up.
|
||||||
|
func TestCableSummary(t *testing.T) {
|
||||||
|
healthy := ecdResult{verdicts: [4]int{pairOK, pairOK, pairOK, pairOK}, metres: [4]int{50, 48, 47, 51}}
|
||||||
|
for _, c := range []struct {
|
||||||
|
name string
|
||||||
|
cable cableInfo
|
||||||
|
measuring bool
|
||||||
|
want string
|
||||||
|
class int
|
||||||
|
}{
|
||||||
|
{"clean", cableInfo{ecd: healthy, maps: [2]byte{pairIdentityMap, pairIdentityMap}}, false, "49", clsGood},
|
||||||
|
{"far-end swap", cableInfo{ecd: healthy, maps: [2]byte{pairIdentityMap, 0xE1}}, false, "49", clsWarn},
|
||||||
|
{"no diag yet", cableInfo{}, false, "-", clsNone},
|
||||||
|
{"measuring", cableInfo{ecd: healthy, maps: [2]byte{pairIdentityMap, pairIdentityMap}}, true, "...", clsNone},
|
||||||
|
} {
|
||||||
|
s, cl := cableSummary(c.cable, c.measuring)
|
||||||
|
if s != c.want || cl != c.class {
|
||||||
|
t.Errorf("%s = %q class %d, want %q class %d", c.name, s, cl, c.want, c.class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPhyDisplayStaleGoesDim(t *testing.T) {
|
||||||
|
cable := cableInfo{
|
||||||
|
ecd: ecdResult{verdicts: [4]int{pairOK, pairOK, pairOK, pairOK}, metres: [4]int{45, 45, 41, 46}},
|
||||||
|
maps: [2]byte{pairIdentityMap, pairIdentityMap},
|
||||||
|
}
|
||||||
|
d := phyDisplayFrom(cable, false, freshMod([4]float64{5, 5, 5, 5}), phyModView{})
|
||||||
|
if d.haveSNR {
|
||||||
|
t.Error("one silent module should withhold snr")
|
||||||
|
}
|
||||||
|
if d.metres != "44" || d.metresClass != clsGood {
|
||||||
|
t.Errorf("metres = %q class %d, want the diag verdict kept", d.metres, d.metresClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mean skips faulted pairs, whose length is a distance to the fault
|
||||||
|
// rather than a length of the cable.
|
||||||
|
func TestCableMetresString(t *testing.T) {
|
||||||
|
c := cableInfo{ecd: ecdResult{
|
||||||
|
verdicts: [4]int{pairOK, pairOpen, pairOK, pairOK},
|
||||||
|
metres: [4]int{45, 3, 41, 46},
|
||||||
|
}}
|
||||||
|
if got := c.metresString(); got != "44" {
|
||||||
|
t.Errorf("metres = %q, want 44", got)
|
||||||
|
}
|
||||||
|
if got := (cableInfo{}).metresString(); got != "-" {
|
||||||
|
t.Errorf("no diag = %q, want -", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSNRClass(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
margin float64
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{5, clsGood}, {3, clsGood}, {2, clsWarn}, {1, clsWarn}, {0.5, clsBad}, {-2, clsBad},
|
||||||
|
} {
|
||||||
|
if got := snrClass(c.margin); got != c.want {
|
||||||
|
t.Errorf("snrClass(%v) = %d, want %d", c.margin, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerdictString(t *testing.T) {
|
||||||
|
ok := ecdResult{verdicts: [4]int{1, 1, 1, 1}, metres: [4]int{45, 45, 41, 46}}
|
||||||
|
if got := verdictString(ok); got != "all pairs ok, 45/45/41/46 m" {
|
||||||
|
t.Errorf("healthy = %q", got)
|
||||||
|
}
|
||||||
|
bad := ecdResult{verdicts: [4]int{1, 2, 4, 1}, metres: [4]int{45, 12, 30, 46}}
|
||||||
|
if got := verdictString(bad); got != "B OPEN at 12m, C XTALK at 30m" {
|
||||||
|
t.Errorf("faulted = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Above any real stream number, so a probe is never taken for payload.
|
|
||||||
probeStream = 0xffff
|
|
||||||
probeSize = 64
|
|
||||||
probePattern = 0
|
|
||||||
|
|
||||||
// The mac has only a handful of transmit stamp slots. Asking faster than it
|
|
||||||
// can drain them gets slots recycled while a stamp is still outstanding, and
|
|
||||||
// the one that comes back then belongs to a different frame.
|
|
||||||
probeInterval = 200 * time.Millisecond
|
|
||||||
probeTimeout = 20 * time.Millisecond
|
|
||||||
// A phy costs microseconds and a hundred metres of copper costs five hundred
|
|
||||||
// nanoseconds, so anything past this is a broken stamp, not a slow frame.
|
|
||||||
probeMaxDelay = 50 * time.Microsecond
|
|
||||||
probePendCap = 256
|
|
||||||
)
|
|
||||||
|
|
||||||
// Both ports hang off one PTP clock, so a transmit stamp from one and a receive
|
|
||||||
// stamp from the other subtract directly. Both are taken at the mac, so all host
|
|
||||||
// time and all queueing falls outside the stamped interval, which is why load
|
|
||||||
// does not move it.
|
|
||||||
type cableStats struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
min int64
|
|
||||||
samples uint64
|
|
||||||
floor int64
|
|
||||||
|
|
||||||
txPend map[uint64]int64
|
|
||||||
rxPend map[uint64]int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type cableView struct {
|
|
||||||
min int64
|
|
||||||
floor int64
|
|
||||||
ok bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Averaging the two directions cancels the phy asymmetry between them, which is
|
|
||||||
// about 790ns and swamps any cable, so one direction alone cannot give a length.
|
|
||||||
func cableMetres(views []view, nsPerM float64) (float64, bool) {
|
|
||||||
if len(views) == 0 {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
var excess float64
|
|
||||||
for _, v := range views {
|
|
||||||
if !v.cable.ok {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
excess += float64(v.cable.min - v.cable.floor)
|
|
||||||
}
|
|
||||||
return excess / float64(len(views)) / nsPerM, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCableStats() *cableStats {
|
|
||||||
return &cableStats{
|
|
||||||
txPend: make(map[uint64]int64, probePendCap),
|
|
||||||
rxPend: make(map[uint64]int64, probePendCap),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cableStats) put(seq uint64, ts int64, tx bool) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
mine, theirs := c.txPend, c.rxPend
|
|
||||||
if !tx {
|
|
||||||
mine, theirs = c.rxPend, c.txPend
|
|
||||||
}
|
|
||||||
other, ok := theirs[seq]
|
|
||||||
if !ok {
|
|
||||||
if len(mine) >= probePendCap {
|
|
||||||
clear(mine)
|
|
||||||
}
|
|
||||||
mine[seq] = ts
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delete(theirs, seq)
|
|
||||||
|
|
||||||
delta := ts - other
|
|
||||||
if tx {
|
|
||||||
delta = -delta
|
|
||||||
}
|
|
||||||
// The driver rebuilds a full timestamp from a truncated hardware value plus a
|
|
||||||
// cached clock read, and a stale cache lands hundreds of milliseconds out. A
|
|
||||||
// minimum would latch onto the first of those and never recover.
|
|
||||||
if delta <= 0 || delta > int64(probeMaxDelay) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.samples == 0 || delta < c.min {
|
|
||||||
c.min = delta
|
|
||||||
}
|
|
||||||
if c.floor == 0 || delta < c.floor {
|
|
||||||
c.floor = delta
|
|
||||||
}
|
|
||||||
c.samples++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cableStats) view() cableView {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return cableView{c.min, c.floor, c.samples > 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cableStats) reset() {
|
|
||||||
c.mu.Lock()
|
|
||||||
c.min, c.samples = 0, 0
|
|
||||||
clear(c.txPend)
|
|
||||||
clear(c.rxPend)
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
type probeSender struct {
|
|
||||||
fd int
|
|
||||||
spec *frameSpec
|
|
||||||
stats *cableStats
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *probeSender) run(done *atomic.Bool, startTx <-chan struct{}) {
|
|
||||||
buf := make([]byte, probeSize)
|
|
||||||
p.spec.prefill(buf, probePattern)
|
|
||||||
oob := make([]byte, 512)
|
|
||||||
scratch := make([]byte, 1)
|
|
||||||
|
|
||||||
<-startTx
|
|
||||||
|
|
||||||
tick := time.NewTicker(probeInterval)
|
|
||||||
defer tick.Stop()
|
|
||||||
|
|
||||||
var seq uint64
|
|
||||||
for !done.Load() {
|
|
||||||
<-tick.C
|
|
||||||
|
|
||||||
// Stamps are matched to sends by position in the queue, so one that
|
|
||||||
// arrived after its probe gave up would be handed to this probe.
|
|
||||||
for {
|
|
||||||
if _, _, _, _, err := unix.Recvmsg(p.fd, scratch, oob,
|
|
||||||
unix.MSG_ERRQUEUE|unix.MSG_DONTWAIT); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
putHeader(buf, probePattern, probeStream, seq, probeSize-minFrame)
|
|
||||||
err := unix.Send(p.fd, buf, 0)
|
|
||||||
// The sequence advances even when a probe fails, so a stale receive half
|
|
||||||
// can never be paired with a later probe that reused its number.
|
|
||||||
cur := seq
|
|
||||||
seq++
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ts, ok := p.awaitTx(scratch, oob)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p.stats.put(cur, ts, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *probeSender) awaitTx(scratch, oob []byte) (int64, bool) {
|
|
||||||
fds := []unix.PollFd{{Fd: int32(p.fd), Events: unix.POLLERR}}
|
|
||||||
deadline := time.Now().Add(probeTimeout)
|
|
||||||
for {
|
|
||||||
left := time.Until(deadline)
|
|
||||||
if left <= 0 {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
// Rounded up, since truncating would give up with time still on the
|
|
||||||
// clock and shave the last fraction of a millisecond off every wait.
|
|
||||||
ms := int((left + time.Millisecond - 1) / time.Millisecond)
|
|
||||||
n, err := unix.Poll(fds, ms)
|
|
||||||
if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil || n == 0 {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
_, oobn, _, _, err := unix.Recvmsg(p.fd, scratch, oob,
|
|
||||||
unix.MSG_ERRQUEUE|unix.MSG_DONTWAIT)
|
|
||||||
if err == unix.EAGAIN || err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return hwTimestamp(oob[:oobn])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type probeReceiver struct {
|
|
||||||
fd int
|
|
||||||
stats *cableStats
|
|
||||||
ready *sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *probeReceiver) run(done *atomic.Bool) {
|
|
||||||
buf := make([]byte, maxFrame)
|
|
||||||
oob := make([]byte, 512)
|
|
||||||
|
|
||||||
r.ready.Done()
|
|
||||||
|
|
||||||
for !done.Load() {
|
|
||||||
n, oobn, _, _, err := unix.Recvmsg(r.fd, buf, oob, 0)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
h, st := parseHeader(buf[:n])
|
|
||||||
if st != hdrOK || h.stream != probeStream {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ts, ok := hwTimestamp(oob[:oobn])
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.stats.put(h.seq, ts, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func stamped(min, floor int64) view {
|
|
||||||
return view{cable: cableView{min: min, floor: floor, ok: true}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Averaged across directions, since one direction alone carries a phy asymmetry
|
|
||||||
// that swamps the cable.
|
|
||||||
func TestCableMetresAveragesDirections(t *testing.T) {
|
|
||||||
m, ok := cableMetres([]view{stamped(1000, 900), stamped(1100, 900)}, 5)
|
|
||||||
if !ok || m != 30 {
|
|
||||||
t.Errorf("cableMetres = %v, %v; want 30, true", m, ok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCableMetresNeedsEveryDirection(t *testing.T) {
|
|
||||||
if _, ok := cableMetres(nil, 5); ok {
|
|
||||||
t.Error("no views should not yield a length")
|
|
||||||
}
|
|
||||||
if _, ok := cableMetres([]view{stamped(1000, 900), {}}, 5); ok {
|
|
||||||
t.Error("a direction with no stamp yet should not yield a length")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -275,6 +275,39 @@ func statusCell(v uint64) string {
|
|||||||
return paint(s, cRed)
|
return paint(s, cRed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The worst pair margin across both receivers, in dB above the 10GBASE-T
|
||||||
|
// operating point, so nobody has to know the operating point to read it.
|
||||||
|
func snrCell(phy phyDisplay) string {
|
||||||
|
if !phy.haveSNR {
|
||||||
|
return paint("-", cGrey)
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("%+.1f", phy.worstMargin)
|
||||||
|
switch snrClass(phy.worstMargin) {
|
||||||
|
case clsGood:
|
||||||
|
return paint(s, cGreen)
|
||||||
|
case clsWarn:
|
||||||
|
return paint(s, cYellow)
|
||||||
|
default:
|
||||||
|
return paint(s, cRed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors the phy absorbed before they could cost a frame: yellow rather than
|
||||||
|
// red, the cable being stressed rather than failing.
|
||||||
|
func correctedCell(v uint64) string {
|
||||||
|
if v == 0 {
|
||||||
|
return paint("0", cGreen)
|
||||||
|
}
|
||||||
|
return paint(commas(v), cYellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func correctedFlag(v uint64) string {
|
||||||
|
if v == 0 {
|
||||||
|
return paint("ok", cGreen)
|
||||||
|
}
|
||||||
|
return paint("warn", cYellow)
|
||||||
|
}
|
||||||
|
|
||||||
// Per-interval rates jitter by a couple of percent at line rate, so green has
|
// Per-interval rates jitter by a couple of percent at line rate, so green has
|
||||||
// to cover that. Yellow means a real shortfall, red means badly off.
|
// to cover that. Yellow means a real shortfall, red means badly off.
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -567,18 +567,40 @@ func checkCoalesce(fd int, ifname string, rxUsecs, txUsecs uint32) checkResult {
|
|||||||
return fmt.Sprintf("adaptive rx=%d tx=%d rx-usecs=%d tx-usecs=%d",
|
return fmt.Sprintf("adaptive rx=%d tx=%d rx-usecs=%d tx-usecs=%d",
|
||||||
e.useAdaptiveRxCoalesce, e.useAdaptiveTxCoalesce, e.rxCoalesceUsecs, e.txCoalesceUsecs)
|
e.useAdaptiveRxCoalesce, e.useAdaptiveTxCoalesce, e.rxCoalesceUsecs, e.txCoalesceUsecs)
|
||||||
}
|
}
|
||||||
if ec.useAdaptiveRxCoalesce == 0 && ec.useAdaptiveTxCoalesce == 0 &&
|
settled := func(e ethtoolCoalesce, tx uint32) bool {
|
||||||
ec.rxCoalesceUsecs == rxUsecs && ec.txCoalesceUsecs == txUsecs {
|
return e.useAdaptiveRxCoalesce == 0 && e.useAdaptiveTxCoalesce == 0 &&
|
||||||
|
e.rxCoalesceUsecs == rxUsecs && e.txCoalesceUsecs == tx
|
||||||
|
}
|
||||||
|
if settled(ec, txUsecs) {
|
||||||
res.state = desc(ec)
|
res.state = desc(ec)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
was := desc(ec)
|
was := desc(ec)
|
||||||
ec.cmd = unix.ETHTOOL_SCOALESCE
|
set := func(tx uint32) error {
|
||||||
ec.useAdaptiveRxCoalesce = 0
|
s := ec
|
||||||
ec.useAdaptiveTxCoalesce = 0
|
s.cmd = unix.ETHTOOL_SCOALESCE
|
||||||
ec.rxCoalesceUsecs = rxUsecs
|
s.useAdaptiveRxCoalesce = 0
|
||||||
ec.txCoalesceUsecs = txUsecs
|
s.useAdaptiveTxCoalesce = 0
|
||||||
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&ec)); err != nil {
|
s.rxCoalesceUsecs = rxUsecs
|
||||||
|
s.txCoalesceUsecs = tx
|
||||||
|
return ethtoolCall(fd, ifname, unsafe.Pointer(&s))
|
||||||
|
}
|
||||||
|
err = set(txUsecs)
|
||||||
|
// A driver running mixed rx/tx vectors has one moderation value per
|
||||||
|
// vector and rejects a tx-specific one: rx-usecs covers both, and get
|
||||||
|
// reports the tx side as zero.
|
||||||
|
if err == unix.EINVAL {
|
||||||
|
if settled(ec, 0) {
|
||||||
|
res.state = desc(ec) + " (tx shares rx)"
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if err = set(0); err == nil {
|
||||||
|
res.fixed = true
|
||||||
|
res.state = fmt.Sprintf("was %s, now adaptive off rx-usecs=%d shared with tx", was, rxUsecs)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
res.err = err
|
res.err = err
|
||||||
res.state = "could not set"
|
res.state = "could not set"
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hwtstampTxOn = 1
|
hwtstampTxOff = 0
|
||||||
hwtstampFilterAll = 1
|
hwtstampFilterAll = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,9 +18,23 @@ type hwtstampConfig struct {
|
|||||||
rxFilter int32
|
rxFilter int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Temporarily bypassed rather than fatal: the X520 bench card cannot stamp
|
||||||
|
// all packets, and the BCM diagnostics path needs runs now. Without stamps
|
||||||
|
// the rate buckets never fill, so the panel's rates read zero; everything
|
||||||
|
// else still measures.
|
||||||
|
func checkTimestamping(fd int, ifname string) checkResult {
|
||||||
|
res := configureTimestamping(fd, ifname)
|
||||||
|
if res.err != nil {
|
||||||
|
res.state = "bypassed: " + res.detail()
|
||||||
|
res.err = nil
|
||||||
|
res.fixed = true
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// Receive stamping is filtered by protocol and ours is not PTP, so nothing
|
// Receive stamping is filtered by protocol and ours is not PTP, so nothing
|
||||||
// narrower than "all" will see our frames.
|
// narrower than "all" will see our frames.
|
||||||
func checkTimestamping(fd int, ifname string) checkResult {
|
func configureTimestamping(fd int, ifname string) checkResult {
|
||||||
res := checkResult{item: ifname + " hw timestamps"}
|
res := checkResult{item: ifname + " hw timestamps"}
|
||||||
desc := func(c hwtstampConfig) string {
|
desc := func(c hwtstampConfig) string {
|
||||||
return fmt.Sprintf("tx_type=%d rx_filter=%d", c.txType, c.rxFilter)
|
return fmt.Sprintf("tx_type=%d rx_filter=%d", c.txType, c.rxFilter)
|
||||||
@@ -42,20 +56,20 @@ func checkTimestamping(fd int, ifname string) checkResult {
|
|||||||
res.err = err
|
res.err = err
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
if have.txType == hwtstampTxOn && have.rxFilter == hwtstampFilterAll {
|
if have.txType == hwtstampTxOff && have.rxFilter == hwtstampFilterAll {
|
||||||
res.state = desc(have)
|
res.state = desc(have)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ioctl reports back what the driver actually applied, which can be
|
// The ioctl reports back what the driver actually applied, which can be
|
||||||
// narrower than what was asked for.
|
// narrower than what was asked for.
|
||||||
want := hwtstampConfig{txType: hwtstampTxOn, rxFilter: hwtstampFilterAll}
|
want := hwtstampConfig{txType: hwtstampTxOff, rxFilter: hwtstampFilterAll}
|
||||||
if err := hwtstampCall(unix.SIOCSHWTSTAMP, &want); err != nil {
|
if err := hwtstampCall(unix.SIOCSHWTSTAMP, &want); err != nil {
|
||||||
res.err = err
|
res.err = err
|
||||||
res.state = "could not set"
|
res.state = "could not set"
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
if want.txType != hwtstampTxOn || want.rxFilter != hwtstampFilterAll {
|
if want.txType != hwtstampTxOff || want.rxFilter != hwtstampFilterAll {
|
||||||
res.err = fmt.Errorf("driver applied %s instead", desc(want))
|
res.err = fmt.Errorf("driver applied %s instead", desc(want))
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -64,13 +78,6 @@ func checkTimestamping(fd int, ifname string) checkResult {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableTxTimestamps(fd int) error {
|
|
||||||
return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_TIMESTAMPING,
|
|
||||||
unix.SOF_TIMESTAMPING_TX_HARDWARE|
|
|
||||||
unix.SOF_TIMESTAMPING_RAW_HARDWARE|
|
|
||||||
unix.SOF_TIMESTAMPING_OPT_TSONLY)
|
|
||||||
}
|
|
||||||
|
|
||||||
func enableRxTimestamps(fd int) error {
|
func enableRxTimestamps(fd int) error {
|
||||||
return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_TIMESTAMPING,
|
return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_TIMESTAMPING,
|
||||||
unix.SOF_TIMESTAMPING_RX_HARDWARE|unix.SOF_TIMESTAMPING_RAW_HARDWARE)
|
unix.SOF_TIMESTAMPING_RX_HARDWARE|unix.SOF_TIMESTAMPING_RAW_HARDWARE)
|
||||||
|
|||||||
@@ -17,9 +17,22 @@ var (
|
|||||||
uiDim = rgb{0x9a, 0xa2, 0xac}
|
uiDim = rgb{0x9a, 0xa2, 0xac}
|
||||||
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
||||||
uiGreen = rgb{0x6c, 0xdc, 0x86}
|
uiGreen = rgb{0x6c, 0xdc, 0x86}
|
||||||
|
uiAmber = rgb{0xe8, 0xc4, 0x52}
|
||||||
uiRed = rgb{0xf0, 0x6b, 0x6b}
|
uiRed = rgb{0xf0, 0x6b, 0x6b}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func classColor(c int) rgb {
|
||||||
|
switch c {
|
||||||
|
case clsGood:
|
||||||
|
return uiGreen
|
||||||
|
case clsWarn:
|
||||||
|
return uiAmber
|
||||||
|
case clsBad:
|
||||||
|
return uiRed
|
||||||
|
}
|
||||||
|
return uiDim
|
||||||
|
}
|
||||||
|
|
||||||
// Distances between ink, since layout measures a line from the top of a digit
|
// Distances between ink, since layout measures a line from the top of a digit
|
||||||
// to the baseline rather than across a cell with accent and descender slack in
|
// to the baseline rather than across a cell with accent and descender slack in
|
||||||
// it. Values carried over from spacing cells will look too small here.
|
// it. Values carried over from spacing cells will look too small here.
|
||||||
@@ -48,7 +61,7 @@ const (
|
|||||||
chipBorder = 2
|
chipBorder = 2
|
||||||
|
|
||||||
gridCols = 2
|
gridCols = 2
|
||||||
chipPadY = spaceGroup
|
chipPadY = step * 3
|
||||||
chipGap = spaceTight
|
chipGap = spaceTight
|
||||||
statRowGap = spaceRow
|
statRowGap = spaceRow
|
||||||
)
|
)
|
||||||
@@ -92,9 +105,9 @@ func newDisplay() (*display, error) {
|
|||||||
bold bool
|
bold bool
|
||||||
size float64
|
size float64
|
||||||
}{
|
}{
|
||||||
{&d.big, true, 40},
|
{&d.big, true, 36},
|
||||||
{&d.grid, false, 34},
|
{&d.grid, false, 30},
|
||||||
{&d.gridB, true, 34},
|
{&d.gridB, true, 30},
|
||||||
} {
|
} {
|
||||||
face, err := loadFace(spec.bold, spec.size)
|
face, err := loadFace(spec.bold, spec.size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,22 +121,29 @@ func newDisplay() (*display, error) {
|
|||||||
return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d",
|
return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d",
|
||||||
d.grid.cellW, d.gridB.cellW)
|
d.grid.cellW, d.gridB.cellW)
|
||||||
}
|
}
|
||||||
now := []int{d.statsH(d.big, 2), d.chipsH()}
|
if err := d.layout(fb.w, fb.h); err != nil {
|
||||||
since := []int{d.statsH(d.gridB, 4), d.countsH(), btnH}
|
fb.close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) layout(w, h int) error {
|
||||||
|
now := []int{d.statsH(d.big, 3), d.chipsH()}
|
||||||
|
since := []int{d.statsH(d.gridB, 4), d.statsH(d.gridB, 1), d.countsH(), btnH}
|
||||||
|
|
||||||
// One gap for both panels, and vertically the frame is the border alone.
|
// One gap for both panels, and vertically the frame is the border alone.
|
||||||
// Insetting by uiPad as well would add it to the gaps at a panel's ends but
|
// Insetting by uiPad as well would add it to the gaps at a panel's ends but
|
||||||
// not to the ones between blocks, which is not equal spacing however evenly
|
// not to the ones between blocks, which is not equal spacing however evenly
|
||||||
// the remainder is divided.
|
// the remainder is divided.
|
||||||
gaps := len(now) + len(since) + 2
|
gaps := len(now) + len(since) + 2
|
||||||
spare := fb.h - 2*uiMargin - blockGap - 4*uiBorder - sum(now) - sum(since)
|
spare := h - 2*uiMargin - blockGap - 4*uiBorder - sum(now) - sum(since)
|
||||||
if spare < 0 {
|
if spare < 0 {
|
||||||
fb.close()
|
return fmt.Errorf("panel content is %dpx taller than the screen", -spare)
|
||||||
return nil, fmt.Errorf("panel content is %dpx taller than the screen", -spare)
|
|
||||||
}
|
}
|
||||||
gap := spare / gaps
|
gap := spare / gaps
|
||||||
|
|
||||||
inner := fb.w - 2*uiMargin
|
inner := w - 2*uiMargin
|
||||||
nowH := 2*uiBorder + sum(now) + (len(now)+1)*gap
|
nowH := 2*uiBorder + sum(now) + (len(now)+1)*gap
|
||||||
sinceH := 2*uiBorder + sum(since) + (len(since)+1)*gap
|
sinceH := 2*uiBorder + sum(since) + (len(since)+1)*gap
|
||||||
d.nowPanel = rect{uiMargin, uiMargin, inner, nowH}
|
d.nowPanel = rect{uiMargin, uiMargin, inner, nowH}
|
||||||
@@ -137,8 +157,8 @@ func newDisplay() (*display, error) {
|
|||||||
w: btnW,
|
w: btnW,
|
||||||
h: btnH,
|
h: btnH,
|
||||||
}
|
}
|
||||||
d.versionSpot = rect{fb.w - versionSpotSide, 0, versionSpotSide, versionSpotSide}
|
d.versionSpot = rect{w - versionSpotSide, 0, versionSpotSide, versionSpotSide}
|
||||||
return d, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sum(hs []int) int {
|
func sum(hs []int) int {
|
||||||
@@ -270,10 +290,10 @@ func (d *display) statsH(vf *textFace, n int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A last row that does not fill the grid is centred.
|
// A last row that does not fill the grid is centred.
|
||||||
func gridCell(i, n, x, w int) (cx, cw int) {
|
func gridCell(i, n, cols, x, w int) (cx, cw int) {
|
||||||
cw = (w - (gridCols-1)*chipGap) / gridCols
|
cw = (w - (cols-1)*chipGap) / cols
|
||||||
inRow := min(n-(i/gridCols)*gridCols, gridCols)
|
inRow := min(n-(i/cols)*cols, cols)
|
||||||
cx = x + (w-(inRow*cw+(inRow-1)*chipGap))/2 + (i%gridCols)*(cw+chipGap)
|
cx = x + (w-(inRow*cw+(inRow-1)*chipGap))/2 + (i%cols)*(cw+chipGap)
|
||||||
return cx, cw
|
return cx, cw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +304,7 @@ func (d *display) stats(vf *textFace, x, w, y int, cells []statCell) int {
|
|||||||
if c.value == "" {
|
if c.value == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cx, cw := gridCell(i, len(cells), x, w)
|
cx, cw := gridCell(i, len(cells), gridCols, x, w)
|
||||||
cy := y + (i/gridCols)*(d.statPairH(vf)+statRowGap)
|
cy := y + (i/gridCols)*(d.statPairH(vf)+statRowGap)
|
||||||
ly := d.centerIn(vf, cx, cw, cy, c.value, c.col)
|
ly := d.centerIn(vf, cx, cw, cy, c.value, c.col)
|
||||||
d.centerIn(d.grid, cx, cw, ly, c.label, uiDim)
|
d.centerIn(d.grid, cx, cw, ly, c.label, uiDim)
|
||||||
@@ -306,9 +326,10 @@ func (d *display) chipH() int { return d.grid.lineH + 2*chipPadY }
|
|||||||
|
|
||||||
func (d *display) countChipH() int { return d.chipH() + d.gridB.lineH + pairGap }
|
func (d *display) countChipH() int { return d.chipH() + d.gridB.lineH + pairGap }
|
||||||
|
|
||||||
// The error chips plus the noise cable's, which shares their row grid.
|
// The error chips plus corrected and the noise cable's, which share their
|
||||||
|
// row grid.
|
||||||
func (d *display) chipsH() int {
|
func (d *display) chipsH() int {
|
||||||
return gridRows(len(errRows)+1)*(d.chipH()+chipGap) - chipGap
|
return gridRows(len(errRows)+2)*(d.chipH()+chipGap) - chipGap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *display) countsH() int {
|
func (d *display) countsH() int {
|
||||||
@@ -317,9 +338,9 @@ func (d *display) countsH() int {
|
|||||||
|
|
||||||
// Outlined by drawing the border colour and sinking a smaller well of
|
// Outlined by drawing the border colour and sinking a smaller well of
|
||||||
// background into it, so both curves get the same antialiasing.
|
// background into it, so both curves get the same antialiasing.
|
||||||
func (d *display) chipAt(i, n, x, w, y, h int, c rgb) (int, int, int) {
|
func (d *display) chipAt(i, n, cols, x, w, y, h int, c rgb) (int, int, int) {
|
||||||
cx, cw := gridCell(i, n, x, w)
|
cx, cw := gridCell(i, n, cols, x, w)
|
||||||
cy := y + (i/gridCols)*(h+chipGap)
|
cy := y + (i/cols)*(h+chipGap)
|
||||||
|
|
||||||
d.fb.roundRect(cx, cy, cw, h, chipRadius, c)
|
d.fb.roundRect(cx, cy, cw, h, chipRadius, c)
|
||||||
d.fb.roundRect(cx+chipBorder, cy+chipBorder,
|
d.fb.roundRect(cx+chipBorder, cy+chipBorder,
|
||||||
@@ -330,15 +351,22 @@ func (d *display) chipAt(i, n, x, w, y, h int, c rgb) (int, int, int) {
|
|||||||
// Whether rather than how many: over a window this short a count changes faster
|
// Whether rather than how many: over a window this short a count changes faster
|
||||||
// than it can be read. The noise chip rides along at the end, presence rather
|
// than it can be read. The noise chip rides along at the end, presence rather
|
||||||
// than health: red is the cable missing, not the cable failing.
|
// than health: red is the cable missing, not the cable failing.
|
||||||
func (d *display) errChips(x, w, y int, e errs, noiseMissing uint64) int {
|
func (d *display) errChips(x, w, y int, e errs, recentCorrected, noiseMissing uint64) int {
|
||||||
n := len(errRows) + 1
|
n := len(errRows) + 2
|
||||||
for i, r := range errRows {
|
for i, r := range errRows {
|
||||||
c := errColor(r.get(e))
|
c := errColor(r.get(e))
|
||||||
cx, cw, cy := d.chipAt(i, n, x, w, y, d.chipH(), c)
|
cx, cw, cy := d.chipAt(i, n, gridCols, x, w, y, d.chipH(), c)
|
||||||
d.centerIn(d.grid, cx, cw, cy+chipPadY, r.label, c)
|
d.centerIn(d.grid, cx, cw, cy+chipPadY, r.label, c)
|
||||||
}
|
}
|
||||||
c := errColor(noiseMissing)
|
// Amber rather than red: the phy absorbed these before they cost a frame.
|
||||||
cx, cw, cy := d.chipAt(len(errRows), n, x, w, y, d.chipH(), c)
|
c := uiGreen
|
||||||
|
if recentCorrected > 0 {
|
||||||
|
c = uiAmber
|
||||||
|
}
|
||||||
|
cx, cw, cy := d.chipAt(len(errRows), n, gridCols, x, w, y, d.chipH(), c)
|
||||||
|
d.centerIn(d.grid, cx, cw, cy+chipPadY, "corrected", c)
|
||||||
|
c = errColor(noiseMissing)
|
||||||
|
cx, cw, cy = d.chipAt(len(errRows)+1, n, gridCols, x, w, y, d.chipH(), c)
|
||||||
d.centerIn(d.grid, cx, cw, cy+chipPadY, "noise", c)
|
d.centerIn(d.grid, cx, cw, cy+chipPadY, "noise", c)
|
||||||
return y + d.chipsH()
|
return y + d.chipsH()
|
||||||
}
|
}
|
||||||
@@ -347,16 +375,26 @@ func (d *display) errCounts(x, w, y int, e errs) int {
|
|||||||
for i, r := range errRows {
|
for i, r := range errRows {
|
||||||
n := r.get(e)
|
n := r.get(e)
|
||||||
c := errColor(n)
|
c := errColor(n)
|
||||||
cx, cw, cy := d.chipAt(i, len(errRows), x, w, y, d.countChipH(), c)
|
cx, cw, cy := d.chipAt(i, len(errRows), gridCols, x, w, y, d.countChipH(), c)
|
||||||
ty := d.centerIn(d.gridB, cx, cw, cy+chipPadY, scaleCount(n), c)
|
ty := d.centerIn(d.gridB, cx, cw, cy+chipPadY, scaleCount(n), c)
|
||||||
d.centerIn(d.grid, cx, cw, ty, r.label, c)
|
d.centerIn(d.grid, cx, cw, ty, r.label, c)
|
||||||
}
|
}
|
||||||
return y + d.countsH()
|
return y + d.countsH()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *display) panel(p rect, e errs) (int, int) {
|
// White when the wiring is clean: a measurement rather than a judgment. The
|
||||||
|
// colours are reserved for the diag finding a fault or a swapped pair.
|
||||||
|
func metresStat(phy phyDisplay) statCell {
|
||||||
|
col := uiFg
|
||||||
|
if phy.metresClass != clsGood {
|
||||||
|
col = classColor(phy.metresClass)
|
||||||
|
}
|
||||||
|
return statCell{phy.metres, "m", col}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) panel(p rect, bad bool) (int, int) {
|
||||||
fill, edge := uiOKFill, uiOKEdge
|
fill, edge := uiOKFill, uiOKEdge
|
||||||
if e.total() > 0 {
|
if bad {
|
||||||
fill, edge = uiErrFil, uiErrEdg
|
fill, edge = uiErrFil, uiErrEdg
|
||||||
}
|
}
|
||||||
d.fb.rect(p.x, p.y, p.w, p.h, edge)
|
d.fb.rect(p.x, p.y, p.w, p.h, edge)
|
||||||
@@ -374,25 +412,45 @@ func errColor(n uint64) rgb {
|
|||||||
return uiRed
|
return uiRed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *display) render(v view, elapsed time.Duration, cable string, noiseMissing uint64) error {
|
func snrStat(phy phyDisplay) statCell {
|
||||||
|
if !phy.haveSNR {
|
||||||
|
return statCell{"-", "db margin", uiDim}
|
||||||
|
}
|
||||||
|
return statCell{fmt.Sprintf("%+.1f", phy.worstMargin), "db margin",
|
||||||
|
classColor(snrClass(phy.worstMargin))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors the phy absorbed before they could cost a frame: amber rather than
|
||||||
|
// red, the cable being stressed rather than failing.
|
||||||
|
func correctedStat(v uint64) statCell {
|
||||||
|
col := uiGreen
|
||||||
|
if v > 0 {
|
||||||
|
col = uiAmber
|
||||||
|
}
|
||||||
|
return statCell{scaleCount(v), "corrected", col}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) render(v view, elapsed time.Duration, phy phyDisplay, noiseMissing uint64) error {
|
||||||
fb := d.fb
|
fb := d.fb
|
||||||
fb.fill(uiBg)
|
fb.fill(uiBg)
|
||||||
|
|
||||||
x, w := d.panel(d.nowPanel, v.window)
|
x, w := d.panel(d.nowPanel, v.window.total() > 0)
|
||||||
d.stats(d.big, x, w, d.nowYs[0], []statCell{
|
d.stats(d.big, x, w, d.nowYs[0], []statCell{
|
||||||
{scaleSI(v.rxGbps * 1e9), "bits/s", uiFg},
|
{scaleSI(v.rxGbps * 1e9), "bits/s", uiFg},
|
||||||
{scaleSI(v.rxPPS), "packets/s", uiFg},
|
{scaleSI(v.rxPPS), "packets/s", uiFg},
|
||||||
|
snrStat(phy),
|
||||||
})
|
})
|
||||||
d.errChips(x, w, d.nowYs[1], v.window, noiseMissing)
|
d.errChips(x, w, d.nowYs[1], v.window, phy.recent, noiseMissing)
|
||||||
|
|
||||||
x, w = d.panel(d.sincePanel, v.since)
|
x, w = d.panel(d.sincePanel, v.since.total() > 0 || phy.metresClass == clsBad)
|
||||||
d.stats(d.gridB, x, w, d.sinceYs[0], []statCell{
|
d.stats(d.gridB, x, w, d.sinceYs[0], []statCell{
|
||||||
{scaleTime(elapsed), "elapsed", uiFg},
|
{scaleTime(elapsed), "elapsed", uiFg},
|
||||||
{scaleCount(v.rxFrames), "packets", uiFg},
|
{scaleCount(v.rxFrames), "packets", uiFg},
|
||||||
{scaleCount(v.rxBytes), "bytes", uiFg},
|
{scaleCount(v.rxBytes), "bytes", uiFg},
|
||||||
{cable, "m", uiFg},
|
correctedStat(phy.corrected),
|
||||||
})
|
})
|
||||||
d.errCounts(x, w, d.sinceYs[1], v.since)
|
d.stats(d.gridB, x, w, d.sinceYs[1], []statCell{metresStat(phy)})
|
||||||
|
d.errCounts(x, w, d.sinceYs[2], v.since)
|
||||||
|
|
||||||
d.drawResetButton()
|
d.drawResetButton()
|
||||||
return fb.flush()
|
return fb.flush()
|
||||||
|
|||||||
+32
-3
@@ -3,8 +3,8 @@ package main
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestGridCellFullRow(t *testing.T) {
|
func TestGridCellFullRow(t *testing.T) {
|
||||||
x0, w0 := gridCell(0, 2, 0, 100)
|
x0, w0 := gridCell(0, 2, gridCols, 0, 100)
|
||||||
x1, w1 := gridCell(1, 2, 0, 100)
|
x1, w1 := gridCell(1, 2, gridCols, 0, 100)
|
||||||
if w0 != w1 {
|
if w0 != w1 {
|
||||||
t.Errorf("cells differ in width: %d vs %d", w0, w1)
|
t.Errorf("cells differ in width: %d vs %d", w0, w1)
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,37 @@ func TestGridCellFullRow(t *testing.T) {
|
|||||||
|
|
||||||
// A last row that does not fill the grid is centred.
|
// A last row that does not fill the grid is centred.
|
||||||
func TestGridCellShortLastRow(t *testing.T) {
|
func TestGridCellShortLastRow(t *testing.T) {
|
||||||
cx, cw := gridCell(2, 3, 0, 100)
|
cx, cw := gridCell(2, 3, gridCols, 0, 100)
|
||||||
if left, right := cx, 100-(cx+cw); left != right {
|
if left, right := cx, 100-(cx+cw); left != right {
|
||||||
t.Errorf("lone cell has %d left and %d right, want centred", left, right)
|
t.Errorf("lone cell has %d left and %d right, want centred", left, right)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The panel is a fixed 600x1024, so whether everything fits is decidable here
|
||||||
|
// rather than on the hardware, with enough spare that the gaps stay readable.
|
||||||
|
func TestPanelFitsScreen(t *testing.T) {
|
||||||
|
d := &display{}
|
||||||
|
for _, spec := range []struct {
|
||||||
|
dst **textFace
|
||||||
|
bold bool
|
||||||
|
size float64
|
||||||
|
}{
|
||||||
|
{&d.big, true, 40},
|
||||||
|
{&d.grid, false, 34},
|
||||||
|
{&d.gridB, true, 34},
|
||||||
|
} {
|
||||||
|
face, err := loadFace(spec.bold, spec.size)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
*spec.dst = face
|
||||||
|
}
|
||||||
|
if err := d.layout(600, 1024); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
gap := d.nowYs[0] - (d.nowPanel.y + uiBorder)
|
||||||
|
t.Logf("panel gap %dpx, since panel ends at %dpx", gap, d.sincePanel.y+d.sincePanel.h)
|
||||||
|
if gap < spaceTight {
|
||||||
|
t.Errorf("panel gap is %dpx, want at least %d", gap, spaceTight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user