This script does not bypass or alter any regulatory‑compliant DFS (Dynamic Frequency Selection) or CAC (Channel Availability Check) behavior.
It only calls the existing wl command and leverages the same built‑in DFS/CAC mechanisms that Broadcom and ASUS provide with the router firmware. Using it means you accept that:
- If you believe this script is doing something illegal, that is effectively a claim about Broadcom/ASUS’s own implementation and regulatory compliance, not about this script specifically, because it only calls their existing
wlcommand and uses the built‑in DFS/CAC behavior. - Any concerns about regulatory compliance should be directed to ASUS or Broadcom; this script is simply a wrapper around functionality that is already present in the firmware.
fix_160A.sh is a shell script designed for dual‑band Asuswrt‑Merlin routers that support WiFi‑6 160‑MHz channels.
It automatically attempts to keep the 5 GHz radio on a 160 MHz channel after a DFS hit causes downgrade to 80 MHz or a non‑DFS channel.
- Target devices: Dual‑band AC/AX routers (e.g., RT‑AX82U, RT‑AX86U, RT‑AX88U) running Asuswrt‑Merlin.
- Function: Runs via cron every 30 minutes (
1,31 * * * *) and performs a controlleddfs_ap_moveto recover 160 MHz operation. - Tri‑band notes: Tri‑band owners can run two copies of the script (e.g.,
sticky_160.shandsticky_160_2.sh), offset the cron jobs by 1–2 minutes, and setIFACEmanually for each radio.
Set these at the top of the script to tune behavior:
SCRIPT_NAME="fix_160A" # Script name must match filename without .sh
IFACE="" # 5GHz interface; leave empty for auto‑detect or set e.g. "eth6"
COOLDOWN=60 # Minimum seconds between recovery attempts
CAC_WAIT=61 # Wait time for CAC to finish (slightly above 61s)
PREFERRED="100/160" # Preferred 160MHz chanspec (may be overridden by NVRAM)
DISABLE_FALLBACK=0 # 1: never switch to opposite 160MHz block once PREFERRED is chosen
STRICT_STICKY=0 # 0: jump to PREFERRED only if current channel is outside block
# 1: jump to PREFERRED if not on exact chanspec, even within block
MANAGE_CRON=1 # Set to 1/0 to add/remove cron and init-start entries
VERBOSE=2 # 0=silent, 1=basic logs, 2=verbose (includes DFS status)
LOG_ROTATE_RAM=1 # 1=RAM‑only log rotation, 0=use temp file on disk
LOG_LINES=200Key behaviors:
DISABLE_FALLBACK=1disables cross‑block fallback; the script will only ever attempt thePREFERRED160‑MHz block.STRICT_STICKY=1forces the radio back to the exactPREFERREDchanspeceven if it’s already on 160 MHz in the same block.LOG_ROTATE_RAM=1keeps logs in memory‑backed storage (no disk writes), while0uses a temporary file rotation on/jffs.
The script reads wl1_chanspec from NVRAM to respect the GUI settings:
- If
wl1_chanspec=0(Auto), the script usesPREFERRED,DISABLE_FALLBACK, andSTRICT_STICKYas configured. - If
wl1_chanspecis a valid*/160spec (e.g.,100/160):PREFERREDis overridden to match the GUI‑selected channel.DISABLE_FALLBACK=1andSTRICT_STICKY=1are enforced so the script always targets that exact channel.
This means:
- If you lock the GUI to a specific 160‑MHz channel (e.g.,
100/160), the script will never try the opposite block and will aggressively try to stay on that channel. - If you set the GUI to Auto, the script behaves in its default “fallback‑enabled” mode.
The script can manage itself:
- When
MANAGE_CRON=1:- Adds a cron job:
1,31 * * * * /jffs/scripts/fix_160A.sh. - Creates/updates
/jffs/scripts/init-startso the cron job is re‑installed on each boot.
- Adds a cron job:
- Uses
$LOCK_FILE(/tmp/fix_160A.last_action) to enforce a cooldown window ($COOLDOWNseconds) so consecutive runs don’t immediately re‑trigger a DFS move.
Timeouts are tuned around the router’s background DFS‑CAC mechanism; CAC_WAIT=61 ensures the script waits long enough for the CAC to complete before logging the result.
-
Interface detection
- If
IFACEis empty, the script auto‑detects the first 5 GHz interface reported bysta_phy_ifnamesthat is in the 5 GHz range (channels 36–165). - Logs
Auto‑detected 5GHz interface [$IFACE].
- If
-
Cooldown & radio check
- If the last move was within
$COOLDOWNseconds, exit early. - If the radio is down (
wl ... isup≠ 1), exit with a log entry.
- If the last move was within
-
Current state inspection
- Reads
wl -i "$IFACE" chanspecto get current channel and width. - If already on 160 MHz, behavior depends on settings:
STRICT_STICKY=1: must be on exactPREFERREDchanspec; if not, attemptdfs_ap_movetoPREFERRED.DISABLE_FALLBACK=1: must be in the preferred block range (e.g., 100–128); if not, attempt move back toPREFERRED.- Otherwise, exit with
[__END] Already on 160MHz.
- Reads
-
Recovery logic (when not on 160 MHz)
- If
DISABLE_FALLBACK=1:- Only attempt
PRIMARY=PREFERRED.
- Only attempt
- If
DISABLE_FALLBACK=0:- Current in 36–64 block →
PRIMARY=36/160,FALLBACK=100/160. - Current in 100–128 block →
PRIMARY=100/160,FALLBACK=36/160. - Outside both blocks →
PRIMARY=PREFERRED,FALLBACKopposite block.
- Current in 36–64 block →
- Tries
dfs_ap_move PRIMARY; if rejected andDISABLE_FALLBACK=0, triesFALLBACK.
- If
-
Post‑CAC verification
- After
dfs_ap_moveis accepted, the script:- Waits
$CAC_WAITseconds for background CAC. - Reads
chanspecagain and logs:[OK] Recovery successfulif width is160.[NOTICE] Recovery failedif still <160 MHz.
- Waits
- If disabled, no fallback is attempted; if enabled, it may retry the fallback on the next run.
- After
-
Logging and rotation
- Logs are written to
/jffs/scripts/fix_160A.log. - When
LOG_ROTATE_RAM=1,tail -n $LOG_LINESkeeps the log capped in memory; otherwise a temp‑file rotation is used.
- Logs are written to
Typical AUTO‑mode scenario (GUI set to Auto):
- Radio is on
100/80after a DFS hit. - Script:
- Detects current width =
80. - Sets
PRIMARY=100/160andFALLBACK=36/160. - Tries
dfs_ap_move 100/160. - If that fails (NOP/CAC), tries
36/160.
- Detects current width =
- Radio moves to
100/160or36/160and remains there until another DFS hit. - On the next run, if already on 160 MHz and within the same block, no action is taken (unless
STRICT_STICKY=1and channel is not exactlyPREFERRED).
If DISABLE_FALLBACK=0 and the preferred block is still blocked, the script may succeed on the opposite block, achieving:
100/160→36/160(or vice‑versa) as needed to keep maximum possible bandwidth.
Run this on the router shell (via SSH) to install and register the script:
curl -L "https://raw.githubusercontent.com/soul4kills/hindsight160/refs/heads/main/fix_160A.sh" -o /jffs/scripts/fix_160A.sh && \
chmod 755 /jffs/scripts/fix_160A.sh && \
cru a fix_160A "1,31 * * * * /jffs/scripts/fix_160A.sh"This:
- Downloads the script to
/jffs/scripts/fix_160A.sh. - Makes it executable.
- Sets a cron job to run every 30 minutes at 1 and 31 minutes past the hour.
-
Download:
curl -L "https://raw.githubusercontent.com/soul4kills/hindsight160/refs/heads/main/fix_160A.sh" -o /jffs/scripts/fix_160A.sh -
Make executable:
chmod 755 /jffs/scripts/fix_160A.sh
-
Add cron job:
cru a fix_160A "1,31 * * * * /jffs/scripts/fix_160A.sh" -
Ensure it survives reboots (if
MANAGE_CRON=1):The script will auto‑create/update
/jffs/scripts/init-startso the cron job is re‑installed on boot.
cru d fix_160ARe‑enable later:
cru a fix_160A "1,31 * * * * /jffs/scripts/fix_160A.sh"Set:
MANAGE_CRON=0Then run the script once and it will:
- Remove the cron job
- Remove cron registration from
/jffs/scripts/init-start.
You can then manage cron and init‑start entries yourself.
-
Logs are written to
/jffs/scripts/fix_160A.log. -
Useful
VERBOSElevels:0: Silent.1: Basic info (actions, DFS status summary).2: Verbose (fullwl ... dfs_ap_movestatus blocks).
-
To inspect current DFS status blocks:
wl -i eth6 dfs_ap_move
(replace
eth6with your actual 5 GHz interface).