Is there a concise, terminal-based script to monitor Wi-Fi clients?

I frequently want to monitor the Wi-Fi signals and other stats (MCS/NSS/GI/bandwidth/etc) of clients connected to my OpenWrt Wi-Fi router. The LuCI page at /cgi-bin/luci/admin/network/wireless is very user-friendly, but I'm looking for something a bit more concise and terminal-based.

I have found that ubus call hostapd.<interface> get_clients provides most of the information I need, but the json output is a bit unwieldly. I'm thinking about writing my own script using shell and jsonfilter or jshn to parse the json and output it in a tab-delimited format, but before I do that, I wanted to see if anyone else knew of a terminal-based script that would provide the same information as /cgi-bin/luci/admin/network/wireless, but in a more concise format?

Thanks!

Did you try iwinfo wlan0 assoclist ?

2 Likes

I have used that as well, it just takes up a bit more vertical space than I'd like (similar to LuCI's wireless status page). Having something in a tabular format would allow more information to fit in less vertical space, as you could consolidate some frequently repeated information into column headers (Tx/Rx, units like pkts, dbm, MHz, etc), as well as tighten up some of the extra space in the display.

Having the ability to choose which columns were visible as well would be useful, as I don't always need to see the same pieces of information.

Then I guess there's no such thing, then. The iwinfo library has a Lua binding though, so it might be possible to build a little script with whatever format you desire, see e.g. Query WiFi status via CLI?

On recentish OpenWrt you could also use ucode in conjunction with ucode-mod-nl80211 to write an associated station dumper with the maximum amount of info the kernel can provide:

#!/usr/bin/env ucode

'use strict';

import { 'request' as wlreq, 'const' as wlconst } from 'nl80211';

const reply = wlreq(wlconst.NL80211_CMD_GET_STATION, wlconst.NLM_F_DUMP, { "dev": ARGV[0] ?? "wlan0" });

for (let entry in reply) {
	printf("%20s %20s %.J\n",
		entry.dev, entry.mac, entry.sta_info);
}

Example:

root@rb750gr3:~# ucode dump.uc phy0-sta0
           phy0-sta0    40:ed:00:39:86:c2 {
	"inactive_time": 16980,
	"rx_bytes": 31405475,
	"tx_bytes": 10117,
	"rx_bytes64": 31405475,
	"tx_bytes64": 10117,
	"rx_packets": 210811,
	"tx_packets": 338,
	"beacon_rx": 98667,
	"signal": -31,
	"tx_bitrate": {
		"bitrate": 10,
		"bitrate32": 10
	},
	"rx_bitrate": {
		"bitrate": 1080,
		"bitrate32": 1080,
		"mcs": 5,
		"40_mhz_width": true,
		"width_40": true
	},
	"tx_retries": 0,
	"tx_failed": 20,
	"beacon_loss": 357,
	"rx_drop_misc": 4,
	"sta_flags": [
		254,
		170
	],
	"tid_stats": [
		{
			"rx_msdu": 3,
			"tx_msdu": 5,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 1,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 5,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 1086,
				"tx_packets": 5
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 5,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 2,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 2,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 288,
				"tx_packets": 2
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 0,
			"tx_msdu": 0,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0,
			"txq_stats": {
				"backlog_bytes": 0,
				"backlog_packets": 0,
				"flows": 0,
				"drops": 0,
				"ecn_marks": 0,
				"overlimit": 0,
				"collisions": 0,
				"tx_bytes": 0,
				"tx_packets": 0
			}
		},
		{
			"rx_msdu": 13211,
			"tx_msdu": 5,
			"tx_msdu_retries": 0,
			"tx_msdu_failed": 0
		}
	],
	"bss_param": {
		"short_preamble": true,
		"short_slot_time": true,
		"dtim_period": 1,
		"beacon_interval": 100
	},
	"rx_duration": 0,
	"tx_duration": 0,
	"connected_time": 10182,
	"assoc_at_boottime": 777332443920341,
	"beacon_signal_avg": -30,
	"expected_throughput": 11250,
	"signal_avg": -30
}
root@rb750gr3:~# 

Then it is just a matter of printing and formatting desired values in entry.sta_info.* accordingly.