├── .gitignore ├── README.md ├── requirements.txt ├── sigmon.png └── src ├── libs ├── metrics.py ├── plotter.py └── session.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # uv files 10 | pyproject.toml 11 | .python-version 12 | uv.lock 13 | 14 | # Virtual environments 15 | .venv 16 | 17 | # PyInstaller files 18 | *.spec 19 | pyinstall.sh 20 | 21 | # Plots 22 | plots/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sigmon 2 | 3 | A tool to monitor WiFi signal strength and metrics continuously in the terminal. 4 | It uses `iwconfig` to get the metrics and `plotext` to plot them. 5 | 6 | ![sigmon](./sigmon.png) 7 | 8 | 9 | ## Features 10 | 11 | - Fast, lightweight and simple 12 | - Continuous monitoring of WiFi signal strength and metrics 13 | - Average signal strength is displayed too 14 | - Plots are saved as PNG files with averages 15 | 16 | ## Requirements 17 | 18 | - `iw` 19 | - Python 3.10+ (may work on older versions, but not tested) 20 | 21 | ### Install `iw` on Ubuntu (and probably other Debian-based systems) 22 | 23 | ```bash 24 | sudo apt-get install iw 25 | ``` 26 | 27 | ## Usage from binary 28 | 29 | Download the binary from the [releases](https://github.com/tcsenpai/sigmon/releases) page. 30 | 31 | ```bash 32 | ./sigmon 33 | ``` 34 | 35 | ## Usage from source 36 | 37 | ### Installation 38 | 39 | ```bash 40 | pip install -r requirements.txt 41 | ``` 42 | 43 | ### Usage 44 | 45 | ```bash 46 | python src/main.py 47 | ``` 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | plotext -------------------------------------------------------------------------------- /sigmon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcsenpai/sigmon/6ee97f876075cdf3f6535c33273fcaed952e8966/sigmon.png -------------------------------------------------------------------------------- /src/libs/metrics.py: -------------------------------------------------------------------------------- 1 | class Metrics: 2 | """ 3 | Class to store the metrics of the network adapter 4 | """ 5 | def __init__(self, signal_strength: str, bitrate: str, is_power_save_enabled: bool): 6 | self.signal_strength = signal_strength 7 | self.bitrate = bitrate 8 | self.is_power_save_enabled = is_power_save_enabled 9 | 10 | def __str__(self): 11 | return f"Signal strength: {self.signal_strength} dBm, Bitrate: {self.bitrate} Mb/s, Power save enabled: {self.is_power_save_enabled}" 12 | -------------------------------------------------------------------------------- /src/libs/plotter.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is used to plot the metrics of the network adapter on the cli 3 | """ 4 | 5 | import plotext as plt 6 | import time 7 | from typing import List 8 | from datetime import datetime 9 | from libs.metrics import Metrics 10 | from libs.session import SessionMetrics 11 | import os 12 | 13 | 14 | class MetricsPlotter: 15 | def __init__(self, max_points: int = 50): 16 | self.signal_strengths: List[float] = [] 17 | self.timestamps: List[str] = [] 18 | self.max_points = max_points 19 | self.min_seen = 0 20 | self.max_seen = -100 21 | self.session = SessionMetrics() 22 | 23 | # Create plots directory if it doesn't exist 24 | os.makedirs("plots", exist_ok=True) 25 | 26 | def save_plot(self): 27 | """Save the current plot with averages""" 28 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 29 | filename = f"plots/sigmon_{timestamp}.png" 30 | 31 | # Calculate average 32 | avg_signal = sum(self.signal_strengths) / len(self.signal_strengths) 33 | 34 | # Create the final plot 35 | plt.clf() 36 | plt.plotsize(150, 50) # Larger size for better image quality 37 | 38 | # Calculate dynamic y-axis limits 39 | y_min = max(-100, self.min_seen - 10) 40 | y_max = min(0, self.max_seen + 10) 41 | plt.ylim(y_min, y_max) 42 | 43 | # Set x-axis limits 44 | x_max = len(self.signal_strengths) 45 | plt.xlim(0, x_max) 46 | 47 | # Plot signal strength 48 | plt.plot(self.signal_strengths, marker="dot", color="green", label="Signal") 49 | 50 | # Plot average line 51 | avg_line = [avg_signal] * len(self.signal_strengths) 52 | plt.plot(avg_line, color="red", label=f"Avg: {avg_signal:.1f} dBm") 53 | 54 | plt.theme("matrix") 55 | plt.title("WiFi Signal Strength Over Time") 56 | plt.xlabel("Time (seconds)") 57 | plt.ylabel("Signal Strength (dBm)") 58 | 59 | # Build and save the plot 60 | plt.build() 61 | plt.save_fig(filename) 62 | 63 | print(f"\nPlot saved as: {filename}") 64 | 65 | def update_plot(self, metrics): 66 | # Convert signal strength to float and ensure it's negative 67 | try: 68 | signal = float(metrics.signal_strength) 69 | # Update min/max seen values 70 | self.min_seen = min(self.min_seen, signal) 71 | self.max_seen = max(self.max_seen, signal) 72 | except ValueError: 73 | print(f"Warning: Invalid signal strength value: {metrics.signal_strength}") 74 | signal = 0 75 | 76 | current_time = datetime.now().strftime("%H:%M:%S") 77 | 78 | # Add new data points 79 | self.signal_strengths.append(signal) 80 | self.timestamps.append(current_time) 81 | 82 | # Keep only last max_points 83 | if len(self.signal_strengths) > self.max_points: 84 | self.signal_strengths.pop(0) 85 | self.timestamps.pop(0) 86 | 87 | # Clear the terminal 88 | plt.clear_terminal() 89 | 90 | # Create the plot 91 | plt.clf() 92 | plt.plotsize(100, 30) 93 | 94 | # Calculate dynamic y-axis limits 95 | y_min = max(-100, self.min_seen - 10) # Don't go below -100 96 | y_max = min(0, self.max_seen + 10) # Don't go above 0 97 | plt.ylim(y_min, y_max) 98 | 99 | # Set x-axis limits 100 | x_max = len(self.signal_strengths) 101 | plt.xlim(0, x_max) 102 | 103 | # Plot with a thicker line 104 | # Color depends on the signal strength: 105 | # - Red if signal strength is less than -60 106 | # - Orange if signal strength is less than -50 107 | # - Green if signal strength is greater than -50 108 | signal_color = "red" if signal < -60 else "orange" if signal < -50 else "green" 109 | plt.plot(self.signal_strengths, color="green") 110 | plt.theme("matrix") 111 | plt.title("WiFi Signal Strength Over Time") 112 | plt.xlabel("Time (seconds)") 113 | plt.ylabel("Signal Strength (dBm)") 114 | 115 | # Show the plot 116 | plt.show() 117 | 118 | # Print current metrics below the plot 119 | print( 120 | f"Signal: {signal:.1f} dBm | Bitrate: {metrics.bitrate} Mb/s | Power Save: {'On' if metrics.is_power_save_enabled else 'Off'}" 121 | ) 122 | status = ( 123 | "Strong signal" 124 | if signal > -50 125 | else ( 126 | "Good signal" 127 | if signal > -60 128 | else "Weak signal" if signal > -70 else "Very bad signal" 129 | ) 130 | ) 131 | print(f"Status: {status}") 132 | summary = self.session.get_session_summary() 133 | print(summary) 134 | 135 | self.session.add_metrics(metrics) 136 | 137 | 138 | def plot_metrics_live( 139 | get_metrics_func, adapter_name: str = "wlan0", interval: float = 1.0 140 | ): 141 | """ 142 | Continuously plot metrics in real-time 143 | 144 | Args: 145 | get_metrics_func: Function to get metrics 146 | adapter_name: Name of the network adapter 147 | interval: Update interval in seconds 148 | """ 149 | plotter = MetricsPlotter() 150 | 151 | try: 152 | while True: 153 | metrics = get_metrics_func(adapter_name) 154 | plotter.update_plot(metrics) 155 | time.sleep(interval) 156 | except KeyboardInterrupt: 157 | print("\nStopping metrics plotting...") 158 | print(plotter.session.get_session_summary()) 159 | plotter.save_plot() 160 | -------------------------------------------------------------------------------- /src/libs/session.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from .metrics import Metrics 3 | from datetime import datetime 4 | 5 | 6 | class SessionMetrics: 7 | def __init__(self): 8 | self.signal_readings: List[float] = [] 9 | self.bitrate_readings: List[float] = [] 10 | self.start_time = datetime.now() 11 | 12 | def add_metrics(self, metrics: Metrics): 13 | try: 14 | self.signal_readings.append(float(metrics.signal_strength)) 15 | self.bitrate_readings.append(float(metrics.bitrate)) 16 | except ValueError: 17 | print("Warning: Invalid metrics value encountered") 18 | 19 | def get_session_summary(self) -> str: 20 | if not self.signal_readings or not self.bitrate_readings: 21 | return "No data collected in this session" 22 | 23 | avg_signal = sum(self.signal_readings) / len(self.signal_readings) 24 | avg_bitrate = sum(self.bitrate_readings) / len(self.bitrate_readings) 25 | 26 | duration = datetime.now() - self.start_time 27 | minutes = duration.total_seconds() / 60 28 | 29 | return ( 30 | f"Session Summary:\n" 31 | f"Duration: {minutes:.1f} minutes\n" 32 | f"Average Signal: {avg_signal:.1f} dBm\n" 33 | f"Average Bitrate: {avg_bitrate:.1f} Mb/s\n" 34 | f"Samples collected: {len(self.signal_readings)}" 35 | ) 36 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import argparse 5 | from libs.metrics import Metrics 6 | from libs.plotter import plot_metrics_live 7 | 8 | 9 | def get_metrics(adapter_name: str = "wlan0") -> Metrics: 10 | """ 11 | Get the metrics of the network adapter 12 | """ 13 | # Execute iwconfig command and return the output 14 | iwconfig_output = subprocess.run( 15 | ["iwconfig", adapter_name], capture_output=True, text=True 16 | ) 17 | 18 | output = iwconfig_output.stdout 19 | 20 | # Initialize default values 21 | signal_strength = "0" 22 | bitrate = "0" 23 | is_power_save_enabled = False 24 | 25 | # Try different possible formats for signal level 26 | try: 27 | if "Signal level=" in output: 28 | signal_strength = output.split("Signal level=")[1].split(" dBm")[0].strip() 29 | elif "Signal level" in output: 30 | signal_strength = output.split("Signal level")[1].split("dBm")[0].strip() 31 | elif "signal:" in output.lower(): 32 | signal_strength = output.lower().split("signal:")[1].split("dBm")[0].strip() 33 | 34 | # Try different possible formats for bit rate 35 | if "Bit Rate=" in output: 36 | bitrate = output.split("Bit Rate=")[1].split(" Mb/s")[0].strip() 37 | elif "Bit Rate:" in output: 38 | bitrate = output.split("Bit Rate:")[1].split(" Mb/s")[0].strip() 39 | elif "tx bitrate:" in output.lower(): 40 | bitrate = output.lower().split("tx bitrate:")[1].split("mbit/s")[0].strip() 41 | 42 | # Try different possible formats for power management 43 | if "Power Management:" in output: 44 | is_power_save_enabled = ( 45 | "on" in output.split("Power Management:")[1].lower().strip() 46 | ) 47 | elif "power management" in output.lower(): 48 | is_power_save_enabled = ( 49 | "on" in output.lower().split("power management")[1].strip() 50 | ) 51 | 52 | except (IndexError, ValueError) as e: 53 | print(f"Warning: Error parsing iwconfig output: {e}") 54 | print(f"Raw output: {output}") 55 | 56 | return Metrics(signal_strength, bitrate, is_power_save_enabled) 57 | 58 | 59 | def get_active_interface(): 60 | """ 61 | Get the active interface name 62 | """ 63 | all_interfaces = subprocess.run(["iwconfig"], capture_output=True, text=True) 64 | for interface in all_interfaces.stdout.split("\n"): 65 | if "ESSID" in interface: 66 | return interface.split(" ")[0].strip() 67 | return None 68 | 69 | 70 | if __name__ == "__main__": 71 | parser = argparse.ArgumentParser() 72 | parser.add_argument("--interface", type=str, help="The interface to use") 73 | args = parser.parse_args() # Check if we got an interface name 74 | if not args.interface: 75 | print("No interface name provided: we will try to find the active interface") 76 | try: 77 | active_interface = get_active_interface() 78 | print(f"The active interface is {active_interface}") 79 | except Exception as e: 80 | print(f"Error getting active interface: {e}") 81 | print("Using wlan0 as default interface") 82 | active_interface = "wlan0" 83 | else: 84 | print(f"Using interface {args.interface}") 85 | active_interface = args.interface 86 | # Execute iwconfig command 87 | metrics = get_metrics(active_interface) 88 | print(metrics) 89 | try: 90 | plot_metrics_live(get_metrics, active_interface) 91 | except KeyboardInterrupt: 92 | print("[+] Done") 93 | --------------------------------------------------------------------------------