Dumb AP - Associated Stations Resolver

6 steps:

  1. Install luci-lua-runtime.

  2. Create the script /pr_home/associated_stations.sh that will produce the htm file /usr/lib/lua/luci/view/associated_stations.htm showing associated stations & PIv4 neighbours:

associated_stations.sh script:
#!/bin/sh

# Output directory and file (root path of the web server)
OUTPUT_FILE="/usr/lib/lua/luci/view/associated_stations.htm"

TIMESTAMP=$(date '+%d/%m/%Y - %H:%M:%S')

# Establish who I am
ROUTER=$(uci get system.@system[0].hostname)


# ******************** Generate the HTML header with proper CSS inside the <head> section ********************
cat > $OUTPUT_FILE << EOF
<% 
local s = require "associated_stations.associated_stations"
s.on_load()
%>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>$ROUTER's Associated Stations and IPv4 Neighbours</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f9f9f9;
        }
        h1 {
            font-size: 1.8em;
            text-align: center;
            margin-bottom: 20px;
        }
        .timestamp {
            text-align: center;
            font-size: 0.9em;
            color: #666;
            margin-bottom: 20px;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
            font-size: 0.9em;
        }
        th {
            background-color: #f2f2f2;
        }
        .perfect-signal {
            color: green;
        }
        .good-signal {
            color: blue;
        }
        .medium-signal {
            color: orange;
        }
        .unstable-connection {
            color: purple;
        }
        .unlikely-connection {
            color: red;
        }
        .unknown {
            color: #999;
            font-style: italic;
        }

        /* Responsive table */
        @media (max-width: 768px) {
            table {
                width: 100%;
                border: none;
                font-size: 0.8em;
            }
            th, td {
                padding: 8px;
                text-align: left;
                display: block;
                width: 100%;
            }
            td {
                border-top: 1px solid #ddd;
            }
            th {
                display: none;
            }
            tr {
                margin-bottom: 15px;
                display: block;
                background-color: #fff;
                padding: 10px;
                box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
            }
            td:before {
                content: attr(data-label);
                font-weight: bold;
                display: block;
                margin-bottom: 5px;
            }
        }
    </style>
</head>
<body>

<h1>$ROUTER's Associated Stations and IPv4 Neighbours</h1>
<p class="timestamp">Last Updated: $TIMESTAMP</p>

<center><p> Signal Strength:                                
<span class='perfect-signal'>Perfect signal: green </span> -
<span class='good-signal'>Good signal: blue </span> -
<span class='medium-signal'>Medium signal: orange </span> -
<span class='unstable-connection'>Unstable connection: purple </span> -
<span class='unlikely-connection'>Unlikely connection: red </span>
</p></center>

<center><p> Status:                                
<span class='perfect-signal'>REACHABLE: the neighbour entry is valid until the reachability timeout expires</span> -
<span class='good-signal'>STALE: the neighbour entry is valid but considered suspicious; this state doesn't change the neighbour</span> -
<span class='unlikely-connection'>FAILED: the maximum number of probes has been exceeded without success; neighbour validation has ultimately failed </span>
</p></center>

<table>
    <thead>
        <tr>
            <th>Device</th>
            <th>Freq</th>
            <th>MAC Address</th>
            <th>Host</th>
            <th>IP Address</th>
            <th>Signal Strength or Status</th>
            <th>RX MBit/s</th>
            <th>TX MBit/s</th>
        </tr>
    </thead>
    <tbody>
EOF


# ****************** Get list of wireless interfaces *******************************
for DEVICE in $(iwinfo | grep ESSID | cut -d' ' -f1); do

    # Get clients for this interface
    iwinfo $DEVICE assoclist | while read -r line; do
        if echo "$line" | grep -q "dBm"; then

            # To manage my two APs 
            if [ $DEVICE == "phy0-ap0" ]; then
                FREQ="2.4 GHz"
            else 
                FREQ="5 GHz"
            fi

            # Extract MAC and signal strength
            MAC=$(echo "$line" | awk '{print $1}')
            SIGNAL=$(echo $line | awk '{print $2}')

            # Get hostname and IP from ethers
            HOSTNAME=$(grep -i "$MAC" /etc/ethers | awk '{print $2}')

            # Skip to the next two lines to extract RX and TX
            read -r line_rx
            RX=$(echo "$line_rx" | grep -o 'RX: [0-9.]\+' | awk '{print $2}')
            
            read -r line_tx
            TX=$(echo "$line_tx" | grep -o 'TX: [0-9.]\+' | awk '{print $2}')
          
            if [ ! -z "$HOSTNAME" ]; then
                IP=$(grep -i "$HOSTNAME" /etc/hosts | awk '{print $1}')
            else
                IP="Unknown"
                HOSTNAME=$(echo "$MAC" | sed 's/://g')
            fi
            
            # Format signal strength with color coding
            if [ ! -z "$SIGNAL" ]; then
                
                if [ $SIGNAL -ge -30 ]; then
                    SIGNAL_DISPLAY="<span class='perfect-signal'>${SIGNAL} dBm</span>"          
                elif [ $SIGNAL -ge -50 ]; then
                    SIGNAL_DISPLAY="<span class='good-signal'>${SIGNAL} dBm</span>"
                elif [ $SIGNAL -ge -70 ]; then
                    SIGNAL_DISPLAY="<span class='medium-signal'>${SIGNAL} dBm</span>"
                elif [ $SIGNAL -ge -80 ]; then
                    SIGNAL_DISPLAY="<span class='unstable-connection'>${SIGNAL} dBm</span>" 
                else
                    SIGNAL_DISPLAY="<span class='unlikely-connection'>${SIGNAL} dBm</span>"
                fi
            else
                SIGNAL_DISPLAY="<span class='unknown'>No Signal</span>"
            fi
            
            # Add row to the HTML table with data-labels for mobile view
            echo "<tr><td data-label='Device'>$DEVICE</td><td data-label='Freq'>$FREQ</td><td data-label='MAC Address'>$MAC</td><td data-label='Host'>$HOSTNAME</td><td data-label='IP Address'>$IP</td><td data-label='Signal Strength or Status'>$SIGNAL_DISPLAY</td><td data-label='RX'>$RX</td><td data-label='TX'>$TX</td></tr>" >> $OUTPUT_FILE
        fi
    done
done


# ****************** Get IPv4 Neighbours for br-lan interface *******************************

ip neigh show dev br-lan nud reachable | while read -r line; do
        
    # Extract STATUS
    STATUS=$(echo "$line" | awk '{print $10}')
   
    # Extract IP
    IP=$(echo "$line" | awk '{print $1}')
   
    # Extract MAC and signal strength
    MAC=$(echo "$line" | awk '{print $3}')
   
    # Get hostname and IP from ethers
    HOSTNAME=$(grep -i "$MAC" /etc/ethers | awk '{print $2}')

    STATUS_DISPLAY="<span class='perfect-signal'>${STATUS}</span>"  
   
    # Add row to the HTML table with data-labels for mobile view
    echo "<tr><td data-label='Device'>br-lan</td><td data-label='Freq'>-</td><td data-label='MAC Address'>$MAC</td><td data-label='Host'>$HOSTNAME</td><td data-label='IP Address'>$IP</td><td data-label='Signal Strength or Status'>$STATUS_DISPLAY</td><td data-label='RX'>-</td><td data-label='TX'>-</td></tr>" >> $OUTPUT_FILE

done

ip neigh show dev br-lan nud stale | while read -r line; do
    
    # Extract STATUS
    STATUS=$(echo "$line" | awk '{print $8}')
    
    # Extract IP
    IP=$(echo "$line" | awk '{print $1}')
    
    # Extract MAC and signal strength
    MAC=$(echo "$line" | awk '{print $3}')
    
    # Get hostname and IP from ethers
    HOSTNAME=$(grep -i "$MAC" /etc/ethers | awk '{print $2}')

    STATUS_DISPLAY="<span class='good-signal'>${STATUS}</span>"  
    
    echo "<tr><td data-label='Device'>br-lan</td><td data-label='Freq'>-</td><td data-label='MAC Address'>$MAC</td><td data-label='Host'>$HOSTNAME</td><td data-label='IP Address'>$IP</td><td data-label='Signal Strength or Status'>$STATUS_DISPLAY</td><td data-label='RX'>-</td><td data-label='TX'>-</td></tr>" >> $OUTPUT_FILE

done

ip neigh show dev br-lan nud failed | while read -r line; do
    
    # Extract STATUS
    STATUS=$(echo "$line" | awk '{print $6}')
    
    # Extract IP
    IP=$(echo "$line" | awk '{print $1}')
    
    # Extract MAC and signal strength
    MAC=$(echo "$line" | awk '{print $3}')
    
    # Get hostname and IP from ethers
    HOSTNAME=$(grep -i "$MAC" /etc/ethers | awk '{print $2}')
    
    STATUS_DISPLAY="<span class='unlikely-connection'>${STATUS}</span>"  

    echo "<tr><td data-label='Device'>br-lan</td><td data-label='Freq'>-</td><td data-label='MAC Address'>$MAC</td><td data-label='Host'>$HOSTNAME</td><td data-label='IP Address'>$IP</td><td data-label='Signal Strength or Status'>$STATUS_DISPLAY</td><td data-label='RX'>-</td><td data-label='TX'>-</td></tr>" >> $OUTPUT_FILE
done

# ********************* Generate the HTML footer to close the table and HTML tags ***********************
cat >> $OUTPUT_FILE << EOF
    </tbody>
</table>

</body>
</html>
EOF

# Set proper permissions
chmod 644 $OUTPUT_FILE
  1. Create the Associated Stations LuCI menu entry:
    mkdir /usr/lib/lua/luci/controller
    vi /usr/lib/lua/luci/controller/associated_stations.lua:
associated_stations.lua script:
module("luci.controller.associated_stations", package.seeall)
function index()
    entry({"admin", "associated_stations"}, template("associated_stations"), "Associated Stations", 70)
end
  1. Logout from LuCI and login: you (should) see the new menu entry.

  2. Link the new menu entry to the step 2) htm page by means of the /usr/lib/lua/associated_stations/associated_stations.lua script, which contains what follows:

final script:
module("associated_stations.associated_stations", package.seeall)
	function on_load()
       	 os.execute("/pr_home/associated_stations.sh")
	end
  1. By selecting Associated Stations in LuCI, you should now see the custom page.

The minor "problem": to update the page you should reload it with the browser, otherwise when you leave and return, the page you get is the older one (that's why there is "Last Updated :...."). I tried to understand why it happens, but without success.

Points of improvements:

  1. associated_stations.sh could probably be written better;

  2. maybe the IPv4 neighbour section of the script should be (re)written to scan the interfaces (devices) instead of statically writing br-lan and considering other nud states;

  3. when the custom page appears, LuCI menu disappears and to get it again you need to go back with your browser.

Last but not least, the work was carried out by consulting the following sources:

Hope you'll enjoy it.

3 Likes