A little something I've been cooking up for a few weeks now for ingress auto-rate shaping...
#!/usr/bin/env lua
-- adaptive_sqm.lua
--
-- Adaptive SQM Controller (Lua)
-- Monitors the ifb4eth1 interface for download traffic, applies a PID controller,
-- and updates sqm.eth1.download via UCI. This version imposes:
-- 1) a strict max step of 5% from the current rate each update,
-- 2) a min rate of 70% of the setpoint,
-- 3) outlier detection to reset the PID state, and
-- 4) dynamic setpoint updates from UCI.
local posix = require("posix")
-- Register SIGINT handler for graceful termination
local function handle_sigint(signum)
print("\nSIGINT received, exiting gracefully.")
os.exit(0)
end
posix.signal(posix.SIGINT, handle_sigint)
-- Helper: run cmd, capture output
local function capture(cmd)
local f = assert(io.popen(cmd, "r"))
local s = f:read("*a")
f:close()
return s
end
-- Retrieve current SQM download rate (setpoint) from UCI
local function get_setpoint()
local output = capture("uci get sqm.eth1.download")
if output then
local rate = tonumber(output:match("(%d+)"))
if rate then
return rate
end
end
error("Failed to retrieve sqm.eth1.download from UCI")
end
-- Script constants
local interface = "ifb4eth1"
local update_interval = 5
-- We'll define variables that can be updated in main loop
local setpoint = get_setpoint() -- in bps
local min_frac = 0.70 -- 70% as minimum fraction
local step_frac = 0.05 -- 5% step limit
local outlier_mult = 5.0 -- outlier threshold multiple
local Kp = 0.05
local Ki = 0.005
local Kd = 0.01
local alpha = 0.2
local previous_smoothed = 0
local previous_error = 0
local integral = 0
local current_sqm_rate = setpoint
-- We recalc these dynamically each loop
local min_rate = math.floor(setpoint * min_frac)
local outlier_threshold = math.floor(setpoint * outlier_mult)
local update_threshold = math.floor(0.05 * setpoint) -- 5% update threshold
-- Sleep using posix.sleep
local function sleep(n) posix.sleep(n) end
-- Wait for interface
local function wait_for_interface(iface, timeout)
local start_time = os.time()
while os.difftime(os.time(), start_time) < timeout do
local file = io.open("/proc/net/dev", "r")
if file then
for line in file:lines() do
if line:match(iface .. ":") then
file:close()
return true
end
end
file:close()
end
sleep(1)
end
return false
end
-- Read rx bytes
local function read_rx_bytes(iface)
local max_attempts = 5
for attempt=1,max_attempts do
local file = io.open("/proc/net/dev", "r")
if file then
for line in file:lines() do
if line:match(iface .. ":") then
local parts = {}
for token in line:gmatch("%S+") do
table.insert(parts, token)
end
file:close()
return tonumber(parts[2])
end
end
file:close()
end
print("Interface " .. iface .. " not found, attempt " .. attempt .. " of " .. max_attempts)
sleep(2)
end
error("Interface "..iface.." not found after "..max_attempts.." attempts")
end
-- Hard-limit a new rate to only 5% change from old rate
-- We'll do that after computing new_rate from the PID
local function limit_rate_change(old_rate, raw_new_rate)
local max_step = old_rate * step_frac -- 5% of the current rate
local desired_change = raw_new_rate - old_rate
if desired_change > max_step then
return old_rate + max_step
elseif desired_change < -max_step then
return old_rate - max_step
else
return raw_new_rate
end
end
-- Update SQM
local function update_sqm_rate(new_rate)
local diff = math.abs(new_rate - current_sqm_rate)
if diff < update_threshold then
return
end
local cmd = string.format("uci set sqm.eth1.download=%d && uci commit sqm && /etc/init.d/sqm restart", math.floor(new_rate))
print("Running command: "..cmd)
local res = os.execute(cmd)
if res ~= 0 then
print("Error updating SQM rate with command: "..cmd)
else
print(string.format("SQM rate updated to %d bps", math.floor(new_rate)))
sleep(3)
if not wait_for_interface(interface, 10) then
io.write("Warning: Interface "..interface.." did not reappear.\n")
end
end
current_sqm_rate = new_rate
end
-- Clear screen
io.write("\27[2J\27[H")
io.write("Adaptive SQM Controller - Press Ctrl-C to exit\n")
io.flush()
local last_bytes = read_rx_bytes(interface)
while true do
sleep(update_interval)
-- Re-read setpoint from UCI
local new_setpoint = get_setpoint()
if new_setpoint ~= setpoint then
print("\nSetpoint changed from "..setpoint.." bps to "..new_setpoint.." bps.")
setpoint = new_setpoint
current_sqm_rate = setpoint
min_rate = math.floor(setpoint * min_frac)
outlier_threshold = math.floor(setpoint * outlier_mult)
update_threshold = math.floor(0.05 * setpoint)
previous_smoothed = setpoint
previous_error = 0
integral = 0
end
local new_bytes = read_rx_bytes(interface)
local diff_bytes = new_bytes - last_bytes
last_bytes = new_bytes
local throughput = (diff_bytes * 8)/update_interval
-- Outlier check
if throughput > outlier_threshold then
print("\nOutlier detected ("..throughput.." bps). Resetting PID state.")
previous_smoothed = throughput
previous_error = 0
integral = 0
else
-- EWMA
local smoothed = alpha*throughput + (1-alpha)*previous_smoothed
previous_smoothed = smoothed
-- PID error
local err = setpoint - smoothed
integral = integral + err*update_interval
local derivative = (err - previous_error)/update_interval
local correction = Kp*err + Ki*integral + Kd*derivative
local raw_new_rate = current_sqm_rate + correction
-- clamp to [min_rate, setpoint]
if raw_new_rate < min_rate then raw_new_rate = min_rate end
if raw_new_rate > setpoint then raw_new_rate = setpoint end
-- limit to 5% step from current_sqm_rate
local limited_rate = limit_rate_change(current_sqm_rate, raw_new_rate)
update_sqm_rate(limited_rate)
previous_error = err
io.write(string.format("\rMeasured: %.2f bps | Smoothed: %.2f bps | Err: %.2f | Corr: %.2f | New Rate: %d bps ",
throughput, smoothed, err, correction, math.floor(limited_rate)))
io.flush()
end
end
Adaptive SQM Controller (Lua)
Overview
This script is designed to help manage your network's download (ingress) rate automatically. It does this by:
- Monitoring your network: It checks the number of bytes received on the ifb4eth1 interface (which is used for download shaping).
- Calculating the actual speed: It calculates how fast data is coming in (throughput) in bits per second.
- Smoothing out fluctuations: It uses a simple method (an Exponential Weighted Moving Average or EWMA) so that temporary spikes or drops don't cause sudden changes.
- Adjusting the rate with a control loop: It uses a basic PID (Proportional, Integral, Derivative) controllerâthink of it as a smart dial that nudges your rate up or down to match a target value.
- Updating SQM settings: When it determines that the download rate should change significantly, it uses OpenWrtâs UCI system to update the SQM (Smart Queue Management) settings and restarts SQM to apply the new rate.
- Handling interface availability: Since restarting SQM may temporarily remove the ifb4eth1 interface, the script waits until the interface comes back before continuing.
- Gracefully handling exit: With LuaPosix installed, the script can catch CtrlâC (SIGINT) and exit cleanly.
- Professional output: The script continuously updates a single line on your screen with current status, so you see a live status update without scrolling.
Key Terms (In Plain Language)
-
SQM (Smart Queue Management):
A system used to manage network traffic so that you get lower latency (less delay) and avoid âbufferbloatâ (too much queued data causing slow response times).
-
ifb4eth1:
A virtual interface used by SQM for shaping incoming (download) traffic. Itâs like a temporary holding area for packets before they are passed on.
-
Throughput:
How much data is passing through your network per second, measured in bits per second (bps).
-
EWMA (Exponential Weighted Moving Average):
A method of smoothing out quick ups and downs in the measurements to get a more stable number.
-
PID Controller:
A control system that looks at the difference (error) between what you want (setpoint) and what youâre getting (measured throughput). It then calculates how much to adjust the rate.
-
Proportional (P): Adjusts based on the current error.
-
Integral (I): Looks at past errors to remove any long-term bias.
-
Derivative (D): Predicts future errors based on how quickly the error is changing.
-
(K_p) (Proportional Gain):
Think of (K_p) like the reaction you have when something is off target right now. Imagine you're trying to keep your video game character at exactly 50 health. If the health is lower than 50, (K_p) tells you how much to immediately add to get back to 50. A higher (K_p) means you react more strongly to the current difference. If it's too high, you might overreact and go past the target.
-
(K_i) (Integral Gain):
(K_i) is like noticing a slow, steady problem over time. If youâre always a little below 50 health, (K_i) adds up all those small misses and gives you extra correction to fix that long-term issue. Itâs like accumulating a debt of âerrorâ that needs to be paid off, so if youâre consistently off, (K_i) helps correct it gradually.
-
(K_d) (Derivative Gain):
(K_d) looks at how fast things are changing. Imagine you notice that your health is dropping super fast; (K_d) tells you to take action to slow down that drop before you hit a crisis. It predicts what might happen next by looking at the rate of change. If things are changing too quickly, (K_d) helps by âbrakingâ the systemâreducing the chance of overshooting the target.
In short:
- (K_p) is your immediate reaction to the current error.
- (K_i) is your adjustment for the accumulated error over time.
- (K_d) is your anticipation of future error based on how quickly things are changing.
Each one helps balance the control system so it can smoothly hit the target without overreacting or underreacting.
How It Works (Step by Step)
-
Initialization:
- The script starts by clearing the screen and displaying a header.
- It sets a target download rate (setpoint) of 600,000 bits per second (600 Kbps).
- It also defines a minimum rate (50% of the setpoint) and a threshold (10% of the setpoint) so that small changes wonât trigger a full update.
-
Monitoring Traffic:
- Every 5 seconds, the script reads the number of bytes received on the ifb4eth1 interface from the file
/proc/net/dev.
- It calculates how many extra bytes have arrived during those 5 seconds, converts that to bits per second (bps), and treats that as your current throughput.
-
Smoothing the Data:
- Because network traffic can have temporary spikes and dips, the script uses EWMA to smooth out the measurements. This gives a more stable âsmoothed throughputâ value.
-
Calculating the Error:
- The script then calculates the error: the difference between your target rate (600 Kbps) and the smoothed throughput.
-
PID Correction:
- It uses the PID formula (with three tunable parameters) to calculate how much to adjust your shaping rate.
- This correction is added to the current SQM rate, but the result is limited so that it never goes above the target rate or below the minimum.
-
Updating SQM Settings:
- If the change is significant (more than 10% of the target), the script issues a UCI command to update the SQM download rate (sqm.eth1.download) and restarts SQM.
- Because SQM restarts take 2â3 seconds and can temporarily remove the ifb4eth1 interface, the script waits until ifb4eth1 reappears before continuing.
-
Continuous Status Display:
- Instead of printing a new line every time, the script updates a single status line on your screen. This line shows:
- The measured throughput (current speed in bps).
- The smoothed throughput.
- The error (difference from the target).
- The correction computed by the PID controller.
- The new shaping rate.
- This output updates in place, giving you a live view without the screen scrolling.
-
Graceful Exit:
- If you press CtrlâC, the script (using LuaPosix) catches the signal and exits cleanly.
How to Use the Script
- Install Dependencies:
- Copy the Script:
- Save the script (the file above) as
adaptive_sqm.lua on your router.
- Make It Executable:
- Run the Script:
- Start the script with:
lua adaptive_sqm.lua
- You will see a status line updating in place.
- Exit the Script:
- Press CtrlâC to exit gracefully.
Troubleshooting
-
Interface Not Found:
If you see messages that ifb4eth1 is not found, it may be due to the SQM restart process. The script is designed to wait until the interface reappears, but if it doesnât, check your SQM configuration.
-
No SQM Rate Changes:
If the script keeps reporting âChange (0 bps) below threshold,â it means that the computed adjustments are not large enough to trigger an update. You may need to adjust the PID parameters or the update threshold.
-
Frequent SQM Restarts:
If SQM is restarting too frequently, consider increasing the update threshold so that only significant changes trigger an update.
Conclusion
This README explains in simple terms what the adaptive SQM Lua script does, how it works, and how to use it. The script monitors your download shaping interface, calculates the current throughput, uses a PID controller to decide if and how to adjust the shaping rate, and then updates SQM accordinglyâall while giving you a clear, professional status display and handling CtrlâC gracefully.
If you have any further questions or need additional modifications, feel free to ask.