I have a question regarding displaying ping results

Good morning,
I have a new question, where I am trying to put together an app for my less tech savy folks. I have it running, and things seem to work fine, except for devices that respond under 1ms. It says they are online but are N/A for times. Is there anyway to fix that?

import subprocess
from time import time, sleep
import re
from datetime import datetime
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
import threading
import socket
#
DEBUG = True  # Print messages to terminal
IPS = ["10.1.1.1", "10.10.254.1", "8.8.8.8"]  # List of IPs to ping
OUTAGE_DURATION_SEC = 60  # Shortest detectable outage

ONLINE = "online"
OUTAGE = "outage"
UNREACHABLE = "network unreachable"

class PingApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Ping Monitor")
        self.root.geometry("1024x768")

        # Set default background color
        self.root.configure(bg="#f0f0f0")  # Light gray background

        # Add a ScrolledText area with customized background and font color
        self.text_area = ScrolledText(
            root, wrap=tk.WORD, font=("Consolas", 16), bg="#ffffff", fg="#000000"
        )
        self.text_area.pack(fill=tk.BOTH, expand=True)

        self.stop_event = threading.Event()

        # Add a start and stop button with consistent background color
        button_frame = tk.Frame(root, bg="#f0f0f0")
        button_frame.pack(fill=tk.X)
        start_button = tk.Button(button_frame, text="Start", command=self.start_monitoring, bg="#d0eaff", fg="#000000")
        start_button.pack(side=tk.LEFT, padx=10, pady=5)
        stop_button = tk.Button(button_frame, text="Stop", command=self.stop_monitoring, bg="#ffcccc", fg="#000000")
        stop_button.pack(side=tk.LEFT, padx=10, pady=5)

        # Add a button to show the client's IP in a pop-up
        client_ip_button = tk.Button(button_frame, text="Show Client IP", command=self.show_client_ip_popup, bg="#ccffcc", fg="#000000")
        client_ip_button.pack(side=tk.LEFT, padx=10, pady=5)

        # User-defined IP section
        self.ip_entries = []
        ip_frame = tk.Frame(root, bg="#f0f0f0")
        ip_frame.pack(fill=tk.X, padx=10, pady=10)

        tk.Label(ip_frame, text="Enter IPs to Monitor (4):", bg="#f0f0f0", fg="#000000").pack(anchor=tk.W)

        for i in range(4):
            entry = tk.Entry(ip_frame, width=30, bg="#ffffff", fg="#000000")
            entry.pack(padx=5, pady=2)
            self.ip_entries.append(entry)

        submit_button = tk.Button(ip_frame, text="Submit IPs", command=self.update_ips, bg="#d9d9d9", fg="#000000")
        submit_button.pack(pady=5)

        self.ips = []  # Placeholder for user-defined IPs

    def log(self, message):
        """Log messages to the GUI and optionally to the console."""
        self.text_area.insert(tk.END, message + "\n")
        self.text_area.see(tk.END)
        if DEBUG:
            print(message)

    def update_ips(self):
        """Update the list of IPs based on user input."""
        self.ips = []
        for entry in self.ip_entries:
            ip = entry.get().strip()
            if self.validate_ip(ip):
                self.ips.append(ip)
            else:
                self.log(f"Invalid IP address: {ip}")

        if len(self.ips) == 4:
            self.log(f"Updated IP list: {', '.join(self.ips)}")
        else:
            self.log("Please provide exactly 4 valid IP addresses.")

    @staticmethod
    def validate_ip(ip):
        """Validate if the string is a valid IP address."""
        pattern = re.compile(r"^(?:\d{1,3}\.){3}\d{1,3}$")
        return bool(pattern.match(ip)) and all(0 <= int(octet) <= 255 for octet in ip.split("."))

    def show_client_ip_popup(self):
        """Display the client's IP address in a pop-up window."""
        ip_address = self.get_client_ip()
        popup = tk.Toplevel(self.root)
        popup.title("Client IP Address")
        popup.geometry("300x100")

        # Add a label to show the IP address with a light background
        ip_label = tk.Label(popup, text=f"Client IP: {ip_address}", font=("Arial", 12), bg="#f7f7f7", fg="#000000")
        ip_label.pack(pady=10)

        # Add a close button with a consistent style
        close_button = tk.Button(popup, text="Close", command=popup.destroy, font=("Arial", 10), bg="#d9d9d9", fg="#000000")
        close_button.pack(pady=5)

    @staticmethod
    def get_client_ip():
        """Retrieve the local IP address of the client."""
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(0)
        try:
            s.connect(('10.254.254.254', 1))  # Arbitrary external IP
            ip = s.getsockname()[0]
        except Exception:
            ip = '127.0.0.1'  # Fallback to localhost
        finally:
            s.close()
        return ip

    def start_monitoring(self):
        if len(self.ips) != 4:
            self.log("Error: Please ensure 4 valid IPs are provided before starting.")
            return
        self.stop_event.clear()
        self.monitor_thread = threading.Thread(target=self.monitor_ips, daemon=True)
        self.monitor_thread.start()

    def stop_monitoring(self):
        self.stop_event.set()

    def monitor_ips(self):
        poll_sec = OUTAGE_DURATION_SEC / 2.0
        while not self.stop_event.is_set():
            for ip in self.ips:
                ping_msg = get_ping_msg(ip)
                network_status = get_network_status(ping_msg)
                ping_time = get_ping_time(ping_msg)
                timestamp = time()
                date_str = get_date_str(timestamp)
                time_str = get_time_str(timestamp)

                log_message = f"{date_str} {time_str} | IP: {ip} | Status: {network_status} | Time: {ping_time if ping_time else 'N/A'} ms"
                self.log(log_message)

            sleep(poll_sec)

def get_filename():
    timestamp = time()
    date_str = get_date_str(timestamp)
    filename = f"{date_str}.csv"
    return filename

def get_ping_msg(ip):
    ping_process = subprocess.Popen(
        ["ping", "-c", "1", ip], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
    )
    pingp = str(ping_process.stdout.read())
    return pingp

def get_network_status(ping_msg):
    if re.search(r"100.0% packet loss|100% packet loss", ping_msg) is not None:
        return OUTAGE
    elif re.search(r"Network is unreachable", ping_msg) is not None:
        return UNREACHABLE
    else:
        return ONLINE

def get_ping_time(ping_msg):
    match = re.search(r"/\d{2,4}\.\d{3}/", ping_msg)
    if match is not None:
        ping_time = float(match.group(0).replace("/", ""))
    else:
        ping_time = None
    return ping_time

def get_date_str(timestamp):
    dt = datetime.fromtimestamp(timestamp)
    return dt.strftime("%Y-%m-%d")

def get_time_str(timestamp):
    dt = datetime.fromtimestamp(timestamp)
    return dt.strftime("%H:%M:%S")

if __name__ == "__main__":
    root = tk.Tk()
    app = PingApp(root)
    root.mainloop()

It doesnt bother me, but for my two test users, its their biggest critique. Thanks for any help you can give.

This won’t match anything under 10 ms. I would change the pattern to

r"/\d{1,4}\.\d{3}/"

This will also catch ping times less than 1 ms if the value is written with a leading zero (e.g. /0.123/)

Alternatively you could change your log message logic from

log_message = f"{date_str} {time_str} | IP: {ip} | Status: {network_status} | Time: {ping_time if ping_time else 'N/A'} ms"

to

log_message = f"{date_str} {time_str} | IP: {ip} | Status: {network_status} | Time: {ping_time if ping_time else '<10' if network_status == 'online' else 'N/A'} ms"

1 Like

You must be using windows. It’s ping does not support <1ms ping time reports.

You could send the ICMP messages from your python code and measure the time to get the response yourself. Maybe use icmplib · PyPI

FYI i would suggest you use subprocess.run() as a simpler API to run ping and capture the output.

Thank you I will give the second one a go and see what it does.

Im on a Mac, and my teacher/student fleets are Macs.

My Mac is happy to show ping time less than 1ms.

If you run the ping that leads to the N/A from the terminal can you reproduce it? Maybe you process the ping output incorrectly?

I think @steven.rumbalski has hit the nail on the head. The regex doesn’t match the full range of times.

But why @gcarmichael would you not take the first idea?

I think there might be a problem with this matching the IP address, but the solution is simply to include more context:

>>> reply = "Reply from 151.101.192.81: bytes=32 time=15ms TTL=59"
>>> re.search(r"time=([0-9.]+)ms", reply)[1]
'15'

As you see, with more context, one can be more tolerant about the exact form of the number. (The message is the one I get from Windows ping.)

In practice I’d probably extract all the fields in one regex match, but that’s not the issue.

I am not convienced that this will get all the ping output - using subprocess.run(cmd, capture_output=True) will.