Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ An intelligent, aesthetic, and robust network latency monitor with automatic fai
* `E5` (659 Hz) for critical latency warnings.
* `D6` (1175 Hz) for packet loss errors.
* A C-Major arpeggio (`C6-E6-G6`) for a clear "Connection Restored!" notification.
* **📊 Persistent Statistics:** Displays comprehensive session statistics (Total, Lost, Average, Loss Rate) every 10 successful prings, ensuring an accurate long-term view of your connection's quality.
* **📊 Persistent Statistics:** Displays comprehensive session statistics (Total, Lost, Average, Loss Rate) every 10 successful pings, ensuring an accurate long-term view of your connection's quality.
* **🎨 Aesthetic Interface:** Uses a customizable rainbow color cycle for successful pings to provide a visually pleasing and informative display.

---
Expand All @@ -28,7 +28,7 @@ An intelligent, aesthetic, and robust network latency monitor with automatic fai
* **PowerShell 7.0+** (Required for optimal color and syntax compatibility)

#### Installation
1. Download the `ping-monitor.ps1` script to a convenient location on your computer (e.g., `C:\Scripts\`).
1. Download the `advanced-ping-monitor.ps1` script to a convenient location on your computer (e.g., `C:\Scripts\`).
2. Open a PowerShell 7 terminal.
3. Navigate to the directory where you saved the script:
```powershell
Expand All @@ -43,24 +43,24 @@ An intelligent, aesthetic, and robust network latency monitor with automatic fai
To run the script with its default settings (monitoring a FFXIV server, fallback to Google DNS), simply execute it:

```powershell
.\ping-monitor.ps1
.\advanced-ping-monitor.ps1
```

#### Custom Examples

* **Monitor a different target and use a different fallback:**
```powershell
.\ping-monitor.ps1 -PrimaryTarget "www.google.com" -FallbackTarget "1.1.1.1"
.\advanced-ping-monitor.ps1 -PrimaryTarget "www.google.com" -FallbackTarget "1.1.1.1"
```

* **Set a more aggressive latency threshold and mute all sounds:**
```powershell
.\ping-monitor.ps1 -CriticalMs 75 -Mute
.\advanced-ping-monitor.ps1 -CriticalMs 75 -Mute
```

* **Ping twice per second and increase the history size for the average calculation:**
```powershell
.\ping-monitor.ps1 -IntervalMs 500 -HistorySize 60
.\advanced-ping-monitor.ps1 -IntervalMs 500 -HistorySize 60
```

---
Expand All @@ -74,7 +74,7 @@ To have the monitor launch automatically when you log into Windows, create a sho
3. In the "Type the location of the item" field, paste the following command. **Make sure to adjust the path to your script file!**

```
"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -ExecutionPolicy Bypass -File "C:\Scripts\ping-monitor.ps1"
"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -ExecutionPolicy Bypass -File "C:\Scripts\advanced-ping-monitor.ps1"
```
4. Click `Next`, give the shortcut a name (e.g., "Ping Monitor"), and click `Finish`.

Expand Down
242 changes: 132 additions & 110 deletions advanced-ping-monitor.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@
.PARAMETER Mute
If specified, disables all audio alerts.

.EXAMPLE
PS C:\> .\ping-monitor.ps1
.EXAMPLE
PS C:\> .\advanced-ping-monitor.ps1

Launches the script with default settings. It monitors the FFXIV server (80.239.145.6)
and uses Google DNS (8.8.8.8) as its fallback target.

.EXAMPLE
PS C:\> .\ping-monitor.ps1 -PrimaryTarget "www.google.com" -FallbackTarget "1.1.1.1"
.EXAMPLE
PS C:\> .\advanced-ping-monitor.ps1 -PrimaryTarget "www.google.com" -FallbackTarget "1.1.1.1"

Monitors "www.google.com" as the primary target and uses Cloudflare DNS (1.1.1.1)
in case of failure.
Expand All @@ -68,9 +68,9 @@
2. In the folder that opens, right-click > New > Shortcut.
3. In the "Type the location of the item" field, paste the following command:

"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -ExecutionPolicy Bypass -File "C:\Path\To\Your\ping-monitor.ps1"
(Make sure to adjust the path to your ping-monitor.ps1 file if necessary)
"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -ExecutionPolicy Bypass -File "C:\Path\To\Your\advanced-ping-monitor.ps1"

(Make sure to adjust the path to your advanced-ping-monitor.ps1 file if necessary)
-----------------------------------------------------------------------------

.LINK
Expand Down Expand Up @@ -122,83 +122,103 @@ function Get-NextColor {
return $color
}

# Parses a PingReply or ErrorRecord to create a structured and comprehensive PingFailure object.
function Get-PingFailure {
param(
[System.Net.NetworkInformation.PingReply]$Reply,
[System.Management.Automation.ErrorRecord]$ErrorRecord
)

if ($ErrorRecord) {
return [PingFailure]::new([PingFailureType]::NetworkError, "Exception", "Network error (Exception)")
}

if ($Reply) {
$status = $Reply.Status.ToString()
$type = [PingFailureType]::Unknown # Default type
$message = ""

switch ($Reply.Status) {
'TimedOut' {
$type = [PingFailureType]::TimedOut; $message = "Request timed out"
}
'DestinationHostUnreachable' {
$type = [PingFailureType]::HostUnreachable; $message = "Destination host unreachable"
}
'DestinationNetworkUnreachable' {
$type = [PingFailureType]::HostUnreachable; $message = "Destination network unreachable"
}
'DestinationProhibited' {
$type = [PingFailureType]::HostUnreachable; $message = "Destination access prohibited (firewall)"
}
'DestinationPortUnreachable' {
$type = [PingFailureType]::HostUnreachable; $message = "Destination port unreachable"
}
'BadDestination' {
$type = [PingFailureType]::HostUnreachable; $message = "Invalid destination address"
}
'BadRoute' {
$type = [PingFailureType]::NetworkError; $message = "Bad network route"
}
'TtlExpired' {
$type = [PingFailureType]::NetworkError; $message = "Packet TTL (Time-To-Live) expired in transit"
}
'TtlReassemblyTimeExceeded' {
$type = [PingFailureType]::NetworkError; $message = "Packet reassembly time (TTL) exceeded"
}
'PacketTooBig' {
$type = [PingFailureType]::NetworkError; $message = "Packet too big (MTU issue)"
}
'BadOption' {
$type = [PingFailureType]::NetworkError; $message = "Invalid packet option"
}
'ParameterProblem' {
$type = [PingFailureType]::NetworkError; $message = "Parameter problem in IP header"
}
'BadHeader' {
$type = [PingFailureType]::NetworkError; $message = "Invalid packet header"
}
'HardwareError' {
$type = [PingFailureType]::NetworkError; $message = "Hardware error on the network"
}
'NoResources' {
$type = [PingFailureType]::NetworkError; $message = "Insufficient system resources (local)"
}
'SourceQuench' {
$type = [PingFailureType]::NetworkError; $message = "Network overload / Congestion"
}
'IcmpError' {
$type = [PingFailureType]::NetworkError; $message = "ICMP protocol error"
}
default {
$type = [PingFailureType]::Unknown; $message = "Unlisted error: $status"
}
}
return [PingFailure]::new($type, $status, $message)
}

return [PingFailure]::new([PingFailureType]::Unknown, "NoReply", "Indeterminate error")
}
# Parses a PingReply or ErrorRecord to create a structured and comprehensive PingFailure object.
function Get-PingFailure {
[CmdletBinding(DefaultParameterSetName = 'FromReply')]
param(
[Parameter(ParameterSetName = 'FromReply')]
[System.Net.NetworkInformation.PingReply]$Reply,

[Parameter(ParameterSetName = 'FromError', Mandatory = $true)]
[System.Management.Automation.ErrorRecord]$ErrorRecord,

[Parameter(ParameterSetName = 'FromStatus', Mandatory = $true)]
[System.Net.NetworkInformation.IPStatus]$Status
)

$resolveFromStatus = {
param([System.Net.NetworkInformation.IPStatus]$statusValue)

$status = $statusValue.ToString()
$type = [PingFailureType]::Unknown
$message = ""

switch ($status) {
'TimedOut' {
$type = [PingFailureType]::TimedOut; $message = "Request timed out"
}
'DestinationHostUnreachable' {
$type = [PingFailureType]::HostUnreachable; $message = "Destination host unreachable"
}
'DestinationNetworkUnreachable' {
$type = [PingFailureType]::HostUnreachable; $message = "Destination network unreachable"
}
'DestinationProhibited' {
$type = [PingFailureType]::HostUnreachable; $message = "Destination access prohibited (firewall)"
}
'DestinationPortUnreachable' {
$type = [PingFailureType]::HostUnreachable; $message = "Destination port unreachable"
}
'BadDestination' {
$type = [PingFailureType]::HostUnreachable; $message = "Invalid destination address"
}
'BadRoute' {
$type = [PingFailureType]::NetworkError; $message = "Bad network route"
}
'TtlExpired' {
$type = [PingFailureType]::NetworkError; $message = "Packet TTL (Time-To-Live) expired in transit"
}
'TtlReassemblyTimeExceeded' {
$type = [PingFailureType]::NetworkError; $message = "Packet reassembly time (TTL) exceeded"
}
'PacketTooBig' {
$type = [PingFailureType]::NetworkError; $message = "Packet too big (MTU issue)"
}
'BadOption' {
$type = [PingFailureType]::NetworkError; $message = "Invalid packet option"
}
'ParameterProblem' {
$type = [PingFailureType]::NetworkError; $message = "Parameter problem in IP header"
}
'BadHeader' {
$type = [PingFailureType]::NetworkError; $message = "Invalid packet header"
}
'HardwareError' {
$type = [PingFailureType]::NetworkError; $message = "Hardware error on the network"
}
'NoResources' {
$type = [PingFailureType]::NetworkError; $message = "Insufficient system resources (local)"
}
'SourceQuench' {
$type = [PingFailureType]::NetworkError; $message = "Network overload / Congestion"
}
'IcmpError' {
$type = [PingFailureType]::NetworkError; $message = "ICMP protocol error"
}
default {
$type = [PingFailureType]::Unknown; $message = "Unlisted error: $status"
}
}

return [PingFailure]::new($type, $status, $message)
}

switch ($PSCmdlet.ParameterSetName) {
'FromReply' {
if ($null -ne $Reply) {
return & $resolveFromStatus $Reply.Status
}
}
'FromStatus' {
return & $resolveFromStatus $Status
}
'FromError' {
return [PingFailure]::new([PingFailureType]::NetworkError, "Exception", "Network error (Exception)")
}
}

return [PingFailure]::new([PingFailureType]::Unknown, "NoReply", "Indeterminate error")
}

function Show-PingFailure {
param([string]$DisplayTarget, [PingFailure]$Failure, [bool]$MuteBeep = $false)
Expand Down Expand Up @@ -284,8 +304,9 @@ try {
Clear-Host; Wait-NetworkConnection -State $state
Clear-Host; Write-Host "Sending 'Ping' requests to $($state.PrimaryTarget) (fallback: $($state.FallbackTarget)):" -ForegroundColor White; Write-Host ""

while ($true) {
$stopwatch.Restart(); $statistics.Total++
while ($true) {
$stopwatch.Restart(); $statistics.Total++
$reply = $null

if ($state.IsOnFallback) {
if ($state.PrimaryCheckJob -and $state.PrimaryCheckJob.State -eq 'Completed') {
Expand All @@ -308,30 +329,31 @@ try {
}
}

try {
$reply = $ping.Send($state.CurrentTarget, $IntervalMs, $payload)
if ($reply.Status -eq 'Success') {
if ($state.IsQuietMode) { Invoke-ReconnectionAlert -MuteBeep $Mute }

$state.IsQuietMode = $false; $state.ConsecutiveLosses = 0

$latency = $reply.RoundtripTime; $history.Enqueue($latency); $historySum += $latency
Show-PingSuccess -DisplayTarget $state.CurrentTarget -Latency $latency -TTL ($reply.Options?.Ttl ?? 0)
} else { throw }
} catch {
$statistics.Lost++; $state.ConsecutiveLosses++
if (-not $state.IsOnFallback -and $state.ConsecutiveLosses -eq 1) {
Write-Host "`nPrimary target lost. Failing over to $($state.FallbackTarget)..." -ForegroundColor DarkYellow
$state.IsOnFallback = $true; $state.CurrentTarget = $state.FallbackTarget; $history.Clear(); $historySum = 0L
}
if ($state.ConsecutiveLosses -ge $state.MaxConsecutiveLosses) { $state.IsQuietMode = $true }

if ($state.IsQuietMode) {
Update-QuietFailureStatus -ConsecutiveCount $state.ConsecutiveLosses
} else {
Show-PingFailure -DisplayTarget $state.CurrentTarget -Failure (Get-PingFailure -Reply $reply -ErrorRecord $_) -MuteBeep $Mute
}
}
try {
$reply = $ping.Send($state.CurrentTarget, $IntervalMs, $payload)
if ($reply.Status -eq 'Success') {
if ($state.IsQuietMode) { Invoke-ReconnectionAlert -MuteBeep $Mute }

$state.IsQuietMode = $false; $state.ConsecutiveLosses = 0

$latency = $reply.RoundtripTime; $history.Enqueue($latency); $historySum += $latency
Show-PingSuccess -DisplayTarget $state.CurrentTarget -Latency $latency -TTL ($reply.Options?.Ttl ?? 0)
} else { throw }
} catch {
$statistics.Lost++; $state.ConsecutiveLosses++
if (-not $state.IsOnFallback -and $state.ConsecutiveLosses -eq 1) {
Write-Host "`nPrimary target lost. Failing over to $($state.FallbackTarget)..." -ForegroundColor DarkYellow
$state.IsOnFallback = $true; $state.CurrentTarget = $state.FallbackTarget; $history.Clear(); $historySum = 0L
}
if ($state.ConsecutiveLosses -ge $state.MaxConsecutiveLosses) { $state.IsQuietMode = $true }

if ($state.IsQuietMode) {
Update-QuietFailureStatus -ConsecutiveCount $state.ConsecutiveLosses
} else {
$failure = if ($null -ne $reply) { Get-PingFailure -Reply $reply } else { Get-PingFailure -ErrorRecord $_ }
Show-PingFailure -DisplayTarget $state.CurrentTarget -Failure $failure -MuteBeep $Mute
}
}

while ($history.Count -gt $HistorySize) { $historySum -= $history.Dequeue() }

Expand Down
Loading