sync: auto-sync from HOWARD-HOME at 2026-06-15 23:43:51
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-15 23:43:51
This commit is contained in:
@@ -81,7 +81,11 @@ controller knows and making prioritized, validated changes. Built for any site;
|
||||
These read each AP directly (controller hides this data). All take `<site> [ap-ssh-vault-path]`,
|
||||
use sshpass-or-SSH_ASKPASS auth, and must run in the **foreground**:
|
||||
```bash
|
||||
# AP-to-AP SNR neighbor matrix (/proc/ui_neighbor) -> redundancy = data-backed disable candidates
|
||||
# AP-to-AP SNR neighbor matrix (/proc/ui_neighbor) -> redundancy = data-backed disable candidates.
|
||||
# Set NBR_JSON to also emit a machine-readable adjacency, then feed it to optimize-radios.sh:
|
||||
NBR_JSON=.claude/tmp/<site>-nbr.json bash .claude/skills/unifi-wifi/scripts/neighbor-collect.sh cascades
|
||||
NEIGHBOR_JSON=.claude/tmp/<site>-nbr.json bash .claude/skills/unifi-wifi/scripts/optimize-radios.sh cascades 14 ng
|
||||
# ^ optimize-radios uses the measured SNR overlap (not the sparse roam graph) -> real DISABLE list.
|
||||
bash .claude/skills/unifi-wifi/scripts/neighbor-collect.sh cascades [vault-path] [snr_min=20]
|
||||
# measured per-channel busy%/noise per AP -> cleanest-channel plan (iw survey dump)
|
||||
bash .claude/skills/unifi-wifi/scripts/survey-collect.sh cascades [vault-path]
|
||||
|
||||
@@ -26,6 +26,8 @@ VAULT="$REPO/.claude/scripts/vault.sh"; UOS="$REPO/.claude/scripts/uos-mongo.sh"
|
||||
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
|
||||
SITEARG="${1:?usage: neighbor-collect.sh <site-name|id> [ap-ssh-vault-path] [snr_min]}"
|
||||
VP="${2:-clients/cascades-tucson/unifi-ap-ssh}"; SNR_MIN="${3:-20}"
|
||||
NBR_JSON="${NBR_JSON:-}" # if set, also write a machine-readable adjacency {ap:{band:{nbr:snr}}} here
|
||||
# (consumed by optimize-radios.sh via NEIGHBOR_JSON for data-backed disables)
|
||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# --- controller creds + login (RW admin reads fine) ---
|
||||
@@ -97,10 +99,11 @@ done < "$TMP/aps.tsv"
|
||||
echo "" >&2
|
||||
|
||||
# --- parse + map + emit adjacency matrix + redundancy summary ---
|
||||
python - "$RAW" "$TMP/macmap.json" "$TMP/bssmap.json" "$SNR_MIN" <<'PY'
|
||||
python - "$RAW" "$TMP/macmap.json" "$TMP/bssmap.json" "$SNR_MIN" "${NBR_JSON:-NONE}" <<'PY'
|
||||
import json,re,sys
|
||||
raw=open(sys.argv[1],encoding='utf-8',errors='replace').read().splitlines()
|
||||
macmap=json.load(open(sys.argv[2])); bssmap=json.load(open(sys.argv[3])); SNR_MIN=int(sys.argv[4])
|
||||
OUTJSON=sys.argv[5] if len(sys.argv)>5 else 'NONE'
|
||||
BN={'0':'2.4','1':'5','2':'6'}; EB={'2.4ghz':'2.4','5ghz':'5','6ghz':'6'}
|
||||
# edges[(src, band)] = {neighbor_name: best_snr}; presence[(src,band)] = set(names) from ess_ap_list
|
||||
edges={}; presence={}; cur=None; mode=None; band=None
|
||||
@@ -153,6 +156,14 @@ for b in ('2.4','5','6'):
|
||||
print(f"\n-- {b}GHz: {len(redund)}/{len(rows)} APs have >=2 strong neighbors (disable/power-down candidates) --")
|
||||
for cnt,ap,strong in sorted(redund,reverse=True)[:12]:
|
||||
print(f" {ap}: {cnt} strong ({', '.join(strong[:4])}{'...' if len(strong)>4 else ''})")
|
||||
|
||||
# machine-readable adjacency for optimize-radios.sh: {ap_name: {band: {neighbor_name: snr}}}
|
||||
if OUTJSON!='NONE':
|
||||
adj={}
|
||||
for (src,b),nbrs in edges.items():
|
||||
adj.setdefault(src,{})[b]=nbrs
|
||||
json.dump(adj, open(OUTJSON,'w'))
|
||||
print(f"\n[INFO] wrote adjacency JSON -> {OUTJSON} ({len(adj)} APs) — feed to optimize-radios.sh via NEIGHBOR_JSON")
|
||||
PY
|
||||
echo ""
|
||||
echo "[next] feed the redundancy list to optimize-radios.sh; validate per-zone with watch-ap.sh before any --apply."
|
||||
|
||||
@@ -29,10 +29,35 @@ if [[ "$arg" =~ ^[0-9a-f]{24}$ ]]; then SITE="$arg"; else
|
||||
[ -n "$SITE" ] || { echo "[ERROR] no site matching '$arg'"; exit 1; }
|
||||
fi
|
||||
case "$BAND" in ng) RSSI_OK=-68; REDUN=$REDUN_NG;; na) RSSI_OK=-72; REDUN=$REDUN_OTHER;; 6e) RSSI_OK=-75; REDUN=$REDUN_OTHER;; *) echo "band must be ng|na|6e"; exit 1;; esac
|
||||
# Coverage-adjacency source: the measured AP-to-AP SNR matrix from neighbor-collect.sh (NEIGHBOR_JSON)
|
||||
# is preferred — it's dense + accurate. The roam graph is the fallback but is sparse in sites with
|
||||
# stationary clients (e.g. senior living), which is why disables came back 0 there. NBR_SNR_MIN = the
|
||||
# SNR (dB) at which a managed-AP neighbor counts as real overlap (neighbor-collect default 20).
|
||||
NBR_SNR_MIN="${NBR_SNR_MIN:-20}"; STRONG_RAW=""; ADJ_SRC="roam-graph (sparse w/ stationary clients)"
|
||||
if [ -n "${NEIGHBOR_JSON:-}" ] && [ -f "${NEIGHBOR_JSON:-}" ]; then
|
||||
# Precompute bidirectional strong-overlap NAME pairs HERE (not inside mongo JS — a large embedded
|
||||
# object literal crashes the old mongo shell). Emit a compact ';'-joined, tab-internal flat string.
|
||||
STRONG_RAW="$(python - "$NEIGHBOR_JSON" "$NBR_SNR_MIN" <<'PY'
|
||||
import json,sys
|
||||
nbr=json.load(open(sys.argv[1])); MIN=int(sys.argv[2])
|
||||
def ms(a,b): # max SNR a->b over any band
|
||||
o=nbr.get(a,{}); return max([o.get(bd,{}).get(b,-1) for bd in ('2.4','5','6')]+[-1])
|
||||
aps=list(nbr); out=[]
|
||||
for i,a in enumerate(aps):
|
||||
for b in aps[i+1:]:
|
||||
if ms(a,b)>=MIN and ms(b,a)>=MIN: # bidirectional overlap
|
||||
out.append(a.replace('"','').replace('\\','')+'\t'+b.replace('"','').replace('\\',''))
|
||||
print(';'.join(out))
|
||||
PY
|
||||
)"
|
||||
ADJ_SRC="neighbor SNR matrix /proc/ui_neighbor (SNR>=$NBR_SNR_MIN)"
|
||||
fi
|
||||
echo "[INFO] site=$SITE band=$BAND window=${DAYS}d rssi>=$RSSI_OK roam>=$ROAM_MIN cap=${CAP}% need_neighbors=$REDUN zone_cap=${ZPCT}%"
|
||||
echo "[INFO] coverage adjacency source: $ADJ_SRC"
|
||||
|
||||
cat <<JS | bash "$UOS" 2>&1 | grep -viE 'pq.html|post-quantum|store now|server may need'
|
||||
var SITE='$SITE',DAYS=$DAYS,BAND='$BAND',RSSI_OK=$RSSI_OK,ROAM_MIN=$ROAM_MIN,CAP=$CAP,ZPCT=$ZPCT,REDUN=$REDUN;
|
||||
var STRONG_RAW="$STRONG_RAW"; // ';'-joined "nameA<tab>nameB" bidirectional strong-overlap pairs, or ""
|
||||
var ace=db.getSiblingDB('ace'),st=db.getSiblingDB('ace_stat');
|
||||
var since=new Date().getTime()-DAYS*86400000;
|
||||
// identity + zone
|
||||
@@ -73,6 +98,18 @@ Object.keys(prof).forEach(function(A){ strong[A]={};
|
||||
if(Math.min(ab,ba)>=ROAM_MIN && p25((rs[A+'>'+B]||[]).concat(rs[B+'>'+A]||[]))>=RSSI_OK) strong[A][B]=1;
|
||||
});
|
||||
});
|
||||
// PREFERRED: rebuild adjacency from the measured AP-to-AP SNR matrix (neighbor-collect / /proc/ui_neighbor)
|
||||
// when provided — denser + accurate. Overlap is bidirectional: A hears B AND B hears A, both >= NBR_SNR_MIN
|
||||
// on any band (physical overlap is band-independent). This is what makes DISABLE decisions data-backed in
|
||||
// sites where the roam graph is too sparse (stationary clients).
|
||||
if(STRONG_RAW){
|
||||
var nm2mac={}; for(var m in name){ nm2mac[name[m]]=m; }
|
||||
Object.keys(prof).forEach(function(A){ strong[A]={}; });
|
||||
STRONG_RAW.split(';').forEach(function(pr){ if(!pr)return; var t=pr.split('\t');
|
||||
var A=nm2mac[t[0]], B=nm2mac[t[1]];
|
||||
if(A&&B&&prof[A]&&prof[B]){ strong[A][B]=1; strong[B][A]=1; } // bidirectional overlap proven by SNR
|
||||
});
|
||||
}
|
||||
// greedy capacity-aware disable
|
||||
var aps=Object.keys(prof), active={}; aps.forEach(function(a){active[a]=true;});
|
||||
var projCu={}; aps.forEach(function(a){projCu[a]=prof[a].cu;}); // projected utilization (grows as we shift load)
|
||||
|
||||
@@ -301,3 +301,33 @@ and fails ("No such file or directory") on Windows. Use `python - "$TMP/x" <<'PY
|
||||
|
||||
Coord: collectors+status announce c3ccaa07. Next: wire neighbor-collect redundancy into
|
||||
optimize-radios.sh; Floor-4 2.4 power-down pilot (still nothing applied to live radios).
|
||||
|
||||
---
|
||||
|
||||
## Update: 00:05 PT (06-16) — wired neighbor SNR matrix into optimize-radios.sh (disables now data-backed)
|
||||
|
||||
Closed the last follow-up: the AP-to-AP SNR matrix now DRIVES optimize-radios.sh disable decisions.
|
||||
- neighbor-collect.sh: `NBR_JSON=<path>` env now also emits machine-readable adjacency
|
||||
`{ap:{band:{nbr:snr}}}` (74 APs, ~21KB).
|
||||
- optimize-radios.sh: `NEIGHBOR_JSON=<path>` env now uses the measured bidirectional SNR overlap
|
||||
(A hears B AND B hears A, SNR>=NBR_SNR_MIN default 20, any band) as the coverage-adjacency source,
|
||||
replacing the sparse roam graph. Roam graph remains the fallback when NEIGHBOR_JSON unset.
|
||||
|
||||
Workflow:
|
||||
NBR_JSON=.claude/tmp/cascades-nbr.json bash .../neighbor-collect.sh cascades
|
||||
NEIGHBOR_JSON=.claude/tmp/cascades-nbr.json bash .../optimize-radios.sh cascades 14 ng
|
||||
|
||||
VALIDATED on Cascades 2.4GHz: roam-graph -> POWER-DOWN 74 / DISABLE 0; neighbor-SNR -> POWER-DOWN 65
|
||||
/ **DISABLE 9** / KEEP 1, est. 621% interference-airtime removed, each disable listing its covering
|
||||
neighbors (127->128; 229->128; 248->348; 330->128; 445->347/348/247; 428->128; 622->505/615/517;
|
||||
Kitchen->Memcare TV room; salon->128). The disable capability we concluded was impossible is now
|
||||
data-backed for any site with a neighbor-collect run.
|
||||
|
||||
IMPL NOTE (bug fixed): injecting the full matrix as a JS object literal CRASHED the old mongo shell
|
||||
(SpiderMonkey GC-during-compile on a ~21KB literal). Fix: precompute bidirectional strong NAME-pairs
|
||||
in python, inject a compact ';'-joined flat STRING, parse in JS. (Pattern for any large data ->
|
||||
mongo-shell injection: flat string, not object literal.)
|
||||
|
||||
Still gated: apply-radio.sh has NO disable action (power/channel/width only); disables remain a
|
||||
reviewed, per-zone, live-validated MANUAL step. Coord: wire announce 68cae757.
|
||||
Remaining open: Floor-4 2.4 power-down pilot (still nothing applied to live radios).
|
||||
|
||||
Reference in New Issue
Block a user