Merge pull request #1828 from RaspAP/feat/net-activity-led

Feature: Network activity LED indicator
This commit is contained in:
Bill Zimmerman
2025-04-29 09:36:49 +02:00
committed by GitHub
13 changed files with 255 additions and 50 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ rootCA.pem
vendor
.env
locale/**/*.mo
app/net_activity

View File

@@ -661,3 +661,13 @@ a.inactive:focus {
max-width: 250px;
}
.led-pulse {
opacity: 0.3 !important;
}
.hostapd-led {
color: #28a745;
opacity: 1;
transition: opacity 0.05s;
}

View File

@@ -1022,6 +1022,31 @@ function disableValidation(form) {
});
}
function updateActivityLED() {
const threshold_bytes = 300;
fetch('/app/net_activity')
.then(res => res.text())
.then(data => {
const activity = parseInt(data.trim());
const leds = document.querySelectorAll('.hostapd-led');
if (!isNaN(activity)) {
leds.forEach(led => {
if (activity > threshold_bytes) {
led.classList.add('led-pulse');
setTimeout(() => {
led.classList.remove('led-pulse');
}, 50);
} else {
led.classList.remove('led-pulse');
}
});
}
})
.catch(() => { /* ignore fetch errors */ });
}
setInterval(updateActivityLED, 100);
$(document).ready(function() {
const $htmlElement = $('html');
const $modeswitch = $('#night-mode');

View File

@@ -971,7 +971,7 @@ function renderStatus($hostapd_led, $hostapd_status, $memused_led, $memused, $cp
<div class="col ml-2">
<div class="ml-1 sb-status">Status</div>
<div class="info-item-xs"><span class="icon">
<i class="fas fa-circle <?php echo ($hostapd_led); ?>"></i></span> <?php echo _("Hotspot").' '. _($hostapd_status); ?>
<i class="fas fa-circle hostapd-led <?php echo ($hostapd_led); ?>"></i></span> <?php echo _("Hotspot").' '. _($hostapd_status); ?>
</div>
<div class="info-item-xs"><span class="icon">
<i class="fas fa-circle <?php echo ($memused_led); ?>"></i></span> <?php echo _("Mem Use").': '. htmlspecialchars(strval($memused), ENT_QUOTES); ?>%

View File

@@ -57,11 +57,17 @@ function DisplayHostAPDConfig()
if (isset($_POST['StartHotspot']) || isset($_POST['RestartHotspot'])) {
$status->addMessage('Attempting to start hotspot', 'info');
if ($arrHostapdConf['BridgedEnable'] == 1) {
exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface br0 --seconds 2', $return);
exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface br0 --seconds 1', $return);
} elseif ($arrHostapdConf['WifiAPEnable'] == 1) {
exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface uap0 --seconds 2', $return);
exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface uap0 --seconds 1', $return);
} else {
exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --seconds 2', $return);
// systemctl expects a unit name like raspap-network-activity@wlan0.service, no extra quotes
$iface_nonescaped = $_POST['interface'];
if (preg_match('/^[a-zA-Z0-9_-]+$/', $iface_nonescaped)) { // validate interface name
exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface ' .$iface_nonescaped. ' --seconds 1', $return);
} else {
throw new \Exception('Invalid network interface');
}
}
foreach ($return as $line) {
$status->addMessage($line, 'info');
@@ -69,6 +75,7 @@ function DisplayHostAPDConfig()
} elseif (isset($_POST['StopHotspot'])) {
$status->addMessage('Attempting to stop hotspot', 'info');
exec('sudo /bin/systemctl stop hostapd.service', $return);
exec('sudo systemctl stop "raspap-network-activity@*.service"');
foreach ($return as $line) {
$status->addMessage($line, 'info');
}

View File

@@ -75,6 +75,7 @@ function _update_raspap() {
_download_latest_files
_change_file_ownership
_patch_system_files
_enable_network_activity_monitor
_create_plugin_scripts
_install_complete
}
@@ -818,9 +819,38 @@ function _configure_networking() {
echo -e
_enable_raspap_daemon
fi
# Enable RaspAP network activity monitor
_enable_network_activity_monitor
_install_status 0
}
# Install and enable RaspAP network activity monitor
function _enable_network_activity_monitor() {
_install_log "Enabling RaspAP network activity monitor"
echo "Compiling raspap-network-monitor.c to /usr/local/bin/"
if ! command -v gcc >/dev/null 2>&1; then
echo "gcc not found, installing..."
sudo apt-get update
sudo apt-get install -y gcc || _install_status 1 "Failed to install gcc"
fi
sudo gcc -O2 -o /usr/local/bin/raspap-network-monitor $webroot_dir/installers/raspap-network-monitor.c || _install_status 1 "Failed to compile raspap-network-monitor.c"
echo "Copying raspap-network-activity@.service to /lib/systemd/system/"
sudo cp $webroot_dir/installers/raspap-network-activity@.service /lib/systemd/system/ || _install_status 1 "Unable to move raspap-network-activity.service file"
sudo systemctl daemon-reload
echo "Enabling raspap-network-activity@wlan0.service"
sudo systemctl enable raspap-network-activity@wlan0.service || _install_status 1 "Failed to enable raspap-network-activity.service"
echo "Starting raspap-network-activity@wlan0.service"
sudo systemctl start raspap-network-activity@wlan0.service || _install_status 1 "Failed to start raspap-network-activity.service"
sleep 0.5
echo "Symlinking /dev/shm/net_activity to $webroot_dir/app/net_activity"
sudo ln -sf /dev/shm/net_activity $webroot_dir/app/net_activity || _install_status 1 "Failed to link net_activity to ${webroot_dir}/app"
echo "Setting ownership for ${raspap_user} on ${webroot_dir}/app/net_activity"
sudo chown -R $raspap_user:$raspap_user $webroot_dir/app/net_activity || _install_status 1 "Unable to set ownership of ${webroot_dir}/app/net_activity"
echo "Network activity monitor enabled"
}
# Prompt to configure TCP BBR option
function _prompt_configure_tcp_bbr() {
_install_log "Configure TCP BBR congestion control"

View File

@@ -0,0 +1,15 @@
# Author: BillZ <billzimmerman@gmail.com>
[Unit]
Description=RaspAP Network Activity Monitor for %I
After=network.target
[Service]
ExecStart=/usr/local/bin/raspap-network-monitor %i
Restart=always
RestartSec=2
User=root
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,106 @@
// raspap-network-monitor.c
/*
RaspAP Network Activity Monitor
Author: @billz <billzimmerman@gmail.com>
Author URI: https://github.com/billz/
License: GNU General Public License v3.0
License URI: https://github.com/raspap/raspap-webgui/blob/master/LICENSE
Usage: raspap-network-monitor [interface]
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/timerfd.h>
#include <errno.h>
#include <stdint.h>
#define TMPFILE "/dev/shm/net_activity"
#define POLL_INTERVAL_MS 100 // 100 milliseconds
unsigned long read_interface_bytes(const char *iface) {
FILE *fp = fopen("/proc/net/dev", "r");
if (!fp) return 0;
char line[512];
unsigned long rx = 0, tx = 0;
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, iface)) {
char *ptr = strchr(line, ':');
if (ptr) {
sscanf(ptr + 1, "%lu %*u %*u %*u %*u %*u %*u %*u %lu", &rx, &tx);
}
break;
}
}
fclose(fp);
return rx + tx;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
return EXIT_FAILURE;
}
const char *iface = argv[1];
unsigned long prev_total = read_interface_bytes(iface);
// create a timerfd
int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
if (tfd == -1) {
perror("timerfd_create");
return EXIT_FAILURE;
}
struct itimerspec timer;
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_nsec = POLL_INTERVAL_MS * 1000000; // interval
timer.it_value.tv_sec = 0;
timer.it_value.tv_nsec = POLL_INTERVAL_MS * 1000000; // initial expiration
if (timerfd_settime(tfd, 0, &timer, NULL) == -1) {
perror("timerfd_settime");
close(tfd);
return EXIT_FAILURE;
}
struct pollfd fds;
fds.fd = tfd;
fds.events = POLLIN;
for (;;) {
int ret = poll(&fds, 1, -1);
if (ret == -1) {
perror("poll");
break;
}
if (fds.revents & POLLIN) {
uint64_t expirations;
read(tfd, &expirations, sizeof(expirations)); // clear timer
unsigned long curr_total = read_interface_bytes(iface);
unsigned long diff = (curr_total >= prev_total) ? (curr_total - prev_total) : 0;
prev_total = curr_total;
FILE *out = fopen(TMPFILE, "w");
if (out) {
fprintf(out, "%lu\n", diff);
fclose(out);
}
}
}
close(tfd);
return EXIT_SUCCESS;
}

View File

@@ -80,3 +80,5 @@ www-data ALL=(ALL) NOPASSWD:/bin/truncate -s 0 /tmp/*.log,/bin/truncate -s 0 /va
www-data ALL=(ALL) NOPASSWD:/usr/bin/vnstat *
www-data ALL=(ALL) NOPASSWD:/usr/sbin/visudo -cf *
www-data ALL=(ALL) NOPASSWD:/etc/raspap/plugins/plugin_helper.sh
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start raspap-network-activity@*.service
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop raspap-network-activity@*.service

View File

@@ -16,7 +16,7 @@ After=multi-user.target
[Service]
Type=oneshot
ExecStart=/bin/bash /etc/raspap/hostapd/servicestart.sh --interface uap0 --seconds 3
ExecStart=/bin/bash /etc/raspap/hostapd/servicestart.sh --seconds 1
RemainAfterExit=no
[Install]

View File

@@ -34,6 +34,20 @@ esac
done
set -- "${positional[@]}"
# Load config file into associative array
declare -A config
if [ -r "$CONFIGFILE" ]; then
while IFS=" = " read -r key value; do
config["$key"]="$value"
done < "$CONFIGFILE"
fi
# Set interface from config if not set by parameter
if [ -z "$interface" ] && [ -n "${config[WifiInterface]}" ]; then
interface="${config[WifiInterface]}"
echo "Interface not provided. Using interface from config: $interface"
fi
echo "Stopping network services..."
if [ $OPENVPNENABLED -eq 1 ]; then
systemctl stop openvpn-client@client
@@ -42,64 +56,58 @@ systemctl stop systemd-networkd
systemctl stop hostapd.service
systemctl stop dnsmasq.service
systemctl stop dhcpcd.service
systemctl stop 'raspap-network-activity@*.service'
if [ "${action}" = "stop" ]; then
echo "Services stopped. Exiting."
exit 0
fi
if [ -f "$DAEMONPATH" ] && [ ! -z "$interface" ]; then
if [ -f "$DAEMONPATH" ] && [ -n "$interface" ]; then
echo "Changing RaspAP Daemon --interface to $interface"
sed -i "s/\(--interface \)[[:alnum:]]*/\1$interface/" "$DAEMONPATH"
fi
if [ -r "$CONFIGFILE" ]; then
declare -A config
while IFS=" = " read -r key value; do
config["$key"]="$value"
done < "$CONFIGFILE"
if [ "${config[BridgedEnable]}" = 1 ]; then
if [ "${interface}" = "br0" ]; then
echo "Stopping systemd-networkd"
systemctl stop systemd-networkd
if [ "${config[BridgedEnable]}" = 1 ]; then
if [ "${interface}" = "br0" ]; then
echo "Stopping systemd-networkd"
systemctl stop systemd-networkd
echo "Restarting eth0 interface..."
ip link set down eth0
ip link set up eth0
echo "Restarting eth0 interface..."
ip link set down eth0
ip link set up eth0
echo "Removing uap0 interface..."
iw dev uap0 del
echo "Removing uap0 interface..."
iw dev uap0 del
echo "Enabling systemd-networkd"
systemctl start systemd-networkd
systemctl enable systemd-networkd
fi
else
echo "Disabling systemd-networkd"
systemctl disable systemd-networkd
echo "Enabling systemd-networkd"
systemctl start systemd-networkd
systemctl enable systemd-networkd
fi
else
echo "Disabling systemd-networkd"
systemctl disable systemd-networkd
ip link ls up | grep -q 'br0' &> /dev/null
if [ $? == 0 ]; then
echo "Removing br0 interface..."
ip link set down br0
ip link del dev br0
fi
ip link ls up | grep -q 'br0' &> /dev/null
if [ $? == 0 ]; then
echo "Removing br0 interface..."
ip link set down br0
ip link del dev br0
fi
if [ "${config[WifiAPEnable]}" = 1 ]; then
if [ "${interface}" = "uap0" ]; then
if [ "${config[WifiAPEnable]}" = 1 ]; then
if [ "${interface}" = "uap0" ]; then
ip link ls up | grep -q 'uap0' &> /dev/null
if [ $? == 0 ]; then
echo "Removing uap0 interface..."
iw dev uap0 del
fi
echo "Adding uap0 interface to ${config[WifiManaged]}"
iw dev ${config[WifiManaged]} interface add uap0 type __ap
# Bring up uap0 interface
ifconfig uap0 up
ip link ls up | grep -q 'uap0' &> /dev/null
if [ $? == 0 ]; then
echo "Removing uap0 interface..."
iw dev uap0 del
fi
echo "Adding uap0 interface to ${config[WifiManaged]}"
iw dev ${config[WifiManaged]} interface add uap0 type __ap
# Bring up uap0 interface
ifconfig uap0 up
fi
fi
fi
@@ -114,12 +122,13 @@ sleep "${seconds}"
systemctl start dnsmasq.service
echo "Starting raspap-network-activity@${interface}.service"
systemctl start raspap-network-activity@${interface}.service
if [ $OPENVPNENABLED -eq 1 ]; then
systemctl start openvpn-client@client
fi
# @mp035 found that the wifi client interface would stop every 8 seconds
# for about 16 seconds. Reassociating seems to solve this
if [ "${config[WifiAPEnable]}" = 1 ]; then
echo "Reassociating wifi client interface..."
sleep "${seconds}"

View File

@@ -20,7 +20,7 @@
</div>
<div class="col">
<button class="btn btn-light btn-icon-split btn-sm service-status float-end">
<span class="icon"><i class="fas fa-circle service-status-<?php echo $state ?>"></i></span>
<span class="icon"><i class="fas fa-circle hostapd-led service-status-<?php echo $state ?>"></i></span>
<span class="text service-status"><?php echo strtolower($interface) .' '. _($state) ?></span>
</button>
</div>

View File

@@ -40,7 +40,7 @@
</div>
<div class="col">
<button class="btn btn-light btn-icon-split btn-sm service-status float-end">
<span class="icon text-gray-600"><i class="fas fa-circle service-status-<?php echo $serviceStatus ?>"></i></span>
<span class="icon text-gray-600"><i class="fas fa-circle hostapd-led service-status-<?php echo $serviceStatus ?>"></i></span>
<span class="text service-status">hostapd <?php echo _($serviceStatus) ?></span>
</button>
</div>