commit 6ee97f876075cdf3f6535c33273fcaed952e8966 Author: tcsenpai Date: Thu Jan 30 15:41:36 2025 +0100 first version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dda6723 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# uv files +pyproject.toml +.python-version +uv.lock + +# Virtual environments +.venv + +# PyInstaller files +*.spec +pyinstall.sh + +# Plots +plots/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3434996 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Sigmon + +A tool to monitor WiFi signal strength and metrics continuously in the terminal. +It uses `iwconfig` to get the metrics and `plotext` to plot them. + +![sigmon](./sigmon.png) + + +## Features + +- Fast, lightweight and simple +- Continuous monitoring of WiFi signal strength and metrics +- Average signal strength is displayed too +- Plots are saved as PNG files with averages + +## Requirements + +- `iw` +- Python 3.10+ (may work on older versions, but not tested) + +### Install `iw` on Ubuntu (and probably other Debian-based systems) + +```bash +sudo apt-get install iw +``` + +## Usage from binary + +Download the binary from the [releases](https://github.com/tcsenpai/sigmon/releases) page. + +```bash +./sigmon +``` + +## Usage from source + +### Installation + +```bash +pip install -r requirements.txt +``` + +### Usage + +```bash +python src/main.py +``` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5fdc25d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +plotext \ No newline at end of file diff --git a/sigmon.png b/sigmon.png new file mode 100644 index 0000000..6d4b952 Binary files /dev/null and b/sigmon.png differ diff --git a/src/libs/metrics.py b/src/libs/metrics.py new file mode 100644 index 0000000..4100aed --- /dev/null +++ b/src/libs/metrics.py @@ -0,0 +1,11 @@ +class Metrics: + """ + Class to store the metrics of the network adapter + """ + def __init__(self, signal_strength: str, bitrate: str, is_power_save_enabled: bool): + self.signal_strength = signal_strength + self.bitrate = bitrate + self.is_power_save_enabled = is_power_save_enabled + + def __str__(self): + return f"Signal strength: {self.signal_strength} dBm, Bitrate: {self.bitrate} Mb/s, Power save enabled: {self.is_power_save_enabled}" diff --git a/src/libs/plotter.py b/src/libs/plotter.py new file mode 100644 index 0000000..7dd1b5b --- /dev/null +++ b/src/libs/plotter.py @@ -0,0 +1,159 @@ +""" +This module is used to plot the metrics of the network adapter on the cli +""" + +import plotext as plt +import time +from typing import List +from datetime import datetime +from libs.metrics import Metrics +from libs.session import SessionMetrics +import os + + +class MetricsPlotter: + def __init__(self, max_points: int = 50): + self.signal_strengths: List[float] = [] + self.timestamps: List[str] = [] + self.max_points = max_points + self.min_seen = 0 + self.max_seen = -100 + self.session = SessionMetrics() + + # Create plots directory if it doesn't exist + os.makedirs("plots", exist_ok=True) + + def save_plot(self): + """Save the current plot with averages""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"plots/sigmon_{timestamp}.png" + + # Calculate average + avg_signal = sum(self.signal_strengths) / len(self.signal_strengths) + + # Create the final plot + plt.clf() + plt.plotsize(150, 50) # Larger size for better image quality + + # Calculate dynamic y-axis limits + y_min = max(-100, self.min_seen - 10) + y_max = min(0, self.max_seen + 10) + plt.ylim(y_min, y_max) + + # Set x-axis limits + x_max = len(self.signal_strengths) + plt.xlim(0, x_max) + + # Plot signal strength + plt.plot(self.signal_strengths, marker="dot", color="green", label="Signal") + + # Plot average line + avg_line = [avg_signal] * len(self.signal_strengths) + plt.plot(avg_line, color="red", label=f"Avg: {avg_signal:.1f} dBm") + + plt.theme("matrix") + plt.title("WiFi Signal Strength Over Time") + plt.xlabel("Time (seconds)") + plt.ylabel("Signal Strength (dBm)") + + # Build and save the plot + plt.build() + plt.save_fig(filename) + + print(f"\nPlot saved as: {filename}") + + def update_plot(self, metrics): + # Convert signal strength to float and ensure it's negative + try: + signal = float(metrics.signal_strength) + # Update min/max seen values + self.min_seen = min(self.min_seen, signal) + self.max_seen = max(self.max_seen, signal) + except ValueError: + print(f"Warning: Invalid signal strength value: {metrics.signal_strength}") + signal = 0 + + current_time = datetime.now().strftime("%H:%M:%S") + + # Add new data points + self.signal_strengths.append(signal) + self.timestamps.append(current_time) + + # Keep only last max_points + if len(self.signal_strengths) > self.max_points: + self.signal_strengths.pop(0) + self.timestamps.pop(0) + + # Clear the terminal + plt.clear_terminal() + + # Create the plot + plt.clf() + plt.plotsize(100, 30) + + # Calculate dynamic y-axis limits + y_min = max(-100, self.min_seen - 10) # Don't go below -100 + y_max = min(0, self.max_seen + 10) # Don't go above 0 + plt.ylim(y_min, y_max) + + # Set x-axis limits + x_max = len(self.signal_strengths) + plt.xlim(0, x_max) + + # Plot with a thicker line + # Color depends on the signal strength: + # - Red if signal strength is less than -60 + # - Orange if signal strength is less than -50 + # - Green if signal strength is greater than -50 + signal_color = "red" if signal < -60 else "orange" if signal < -50 else "green" + plt.plot(self.signal_strengths, color="green") + plt.theme("matrix") + plt.title("WiFi Signal Strength Over Time") + plt.xlabel("Time (seconds)") + plt.ylabel("Signal Strength (dBm)") + + # Show the plot + plt.show() + + # Print current metrics below the plot + print( + f"Signal: {signal:.1f} dBm | Bitrate: {metrics.bitrate} Mb/s | Power Save: {'On' if metrics.is_power_save_enabled else 'Off'}" + ) + status = ( + "Strong signal" + if signal > -50 + else ( + "Good signal" + if signal > -60 + else "Weak signal" if signal > -70 else "Very bad signal" + ) + ) + print(f"Status: {status}") + summary = self.session.get_session_summary() + print(summary) + + self.session.add_metrics(metrics) + + +def plot_metrics_live( + get_metrics_func, adapter_name: str = "wlan0", interval: float = 1.0 +): + """ + Continuously plot metrics in real-time + + Args: + get_metrics_func: Function to get metrics + adapter_name: Name of the network adapter + interval: Update interval in seconds + """ + plotter = MetricsPlotter() + + try: + while True: + metrics = get_metrics_func(adapter_name) + plotter.update_plot(metrics) + time.sleep(interval) + except KeyboardInterrupt: + print("\nStopping metrics plotting...") + print(plotter.session.get_session_summary()) + plotter.save_plot() diff --git a/src/libs/session.py b/src/libs/session.py new file mode 100644 index 0000000..3ffcacc --- /dev/null +++ b/src/libs/session.py @@ -0,0 +1,35 @@ +from typing import List +from .metrics import Metrics +from datetime import datetime + + +class SessionMetrics: + def __init__(self): + self.signal_readings: List[float] = [] + self.bitrate_readings: List[float] = [] + self.start_time = datetime.now() + + def add_metrics(self, metrics: Metrics): + try: + self.signal_readings.append(float(metrics.signal_strength)) + self.bitrate_readings.append(float(metrics.bitrate)) + except ValueError: + print("Warning: Invalid metrics value encountered") + + def get_session_summary(self) -> str: + if not self.signal_readings or not self.bitrate_readings: + return "No data collected in this session" + + avg_signal = sum(self.signal_readings) / len(self.signal_readings) + avg_bitrate = sum(self.bitrate_readings) / len(self.bitrate_readings) + + duration = datetime.now() - self.start_time + minutes = duration.total_seconds() / 60 + + return ( + f"Session Summary:\n" + f"Duration: {minutes:.1f} minutes\n" + f"Average Signal: {avg_signal:.1f} dBm\n" + f"Average Bitrate: {avg_bitrate:.1f} Mb/s\n" + f"Samples collected: {len(self.signal_readings)}" + ) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..e130ff8 --- /dev/null +++ b/src/main.py @@ -0,0 +1,92 @@ +import os +import sys +import subprocess +import argparse +from libs.metrics import Metrics +from libs.plotter import plot_metrics_live + + +def get_metrics(adapter_name: str = "wlan0") -> Metrics: + """ + Get the metrics of the network adapter + """ + # Execute iwconfig command and return the output + iwconfig_output = subprocess.run( + ["iwconfig", adapter_name], capture_output=True, text=True + ) + + output = iwconfig_output.stdout + + # Initialize default values + signal_strength = "0" + bitrate = "0" + is_power_save_enabled = False + + # Try different possible formats for signal level + try: + if "Signal level=" in output: + signal_strength = output.split("Signal level=")[1].split(" dBm")[0].strip() + elif "Signal level" in output: + signal_strength = output.split("Signal level")[1].split("dBm")[0].strip() + elif "signal:" in output.lower(): + signal_strength = output.lower().split("signal:")[1].split("dBm")[0].strip() + + # Try different possible formats for bit rate + if "Bit Rate=" in output: + bitrate = output.split("Bit Rate=")[1].split(" Mb/s")[0].strip() + elif "Bit Rate:" in output: + bitrate = output.split("Bit Rate:")[1].split(" Mb/s")[0].strip() + elif "tx bitrate:" in output.lower(): + bitrate = output.lower().split("tx bitrate:")[1].split("mbit/s")[0].strip() + + # Try different possible formats for power management + if "Power Management:" in output: + is_power_save_enabled = ( + "on" in output.split("Power Management:")[1].lower().strip() + ) + elif "power management" in output.lower(): + is_power_save_enabled = ( + "on" in output.lower().split("power management")[1].strip() + ) + + except (IndexError, ValueError) as e: + print(f"Warning: Error parsing iwconfig output: {e}") + print(f"Raw output: {output}") + + return Metrics(signal_strength, bitrate, is_power_save_enabled) + + +def get_active_interface(): + """ + Get the active interface name + """ + all_interfaces = subprocess.run(["iwconfig"], capture_output=True, text=True) + for interface in all_interfaces.stdout.split("\n"): + if "ESSID" in interface: + return interface.split(" ")[0].strip() + return None + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--interface", type=str, help="The interface to use") + args = parser.parse_args() # Check if we got an interface name + if not args.interface: + print("No interface name provided: we will try to find the active interface") + try: + active_interface = get_active_interface() + print(f"The active interface is {active_interface}") + except Exception as e: + print(f"Error getting active interface: {e}") + print("Using wlan0 as default interface") + active_interface = "wlan0" + else: + print(f"Using interface {args.interface}") + active_interface = args.interface + # Execute iwconfig command + metrics = get_metrics(active_interface) + print(metrics) + try: + plot_metrics_live(get_metrics, active_interface) + except KeyboardInterrupt: + print("[+] Done")