Skip to content

Commit 048be5a

Browse files
authored
Feature: Speedtest (#3440)
1 parent 3e05641 commit 048be5a

25 files changed

Lines changed: 1476 additions & 45 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ NETworManager has integrated some **optional** third-party services to enhance f
250250
- [api.github.com](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement) - Check for application updates.
251251
- [ipify.org](https://www.ipify.org/) - Retrieve the public IP address used by the client.
252252
- [ip-api.com](https://ip-api.com/docs/legal) - Retrieve network information (e.g., geolocation, ISP, DNS resolver) used by the client.
253+
- [speed.cloudflare.com](https://www.cloudflare.com/privacypolicy/) - Measure download/upload speed, latency and jitter.
253254
254255
## 📝 License
255256

Source/GlobalAssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
[assembly: AssemblyTrademark("")]
77
[assembly: AssemblyCulture("")]
88

9-
[assembly: AssemblyVersion("2026.5.17.0")]
10-
[assembly: AssemblyFileVersion("2026.5.17.0")]
9+
[assembly: AssemblyVersion("2026.5.24.0")]
10+
[assembly: AssemblyFileVersion("2026.5.24.0")]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Windows.Data;
4+
5+
namespace NETworkManager.Converters;
6+
7+
/// <summary>
8+
/// Converts a nullable <see cref="double"/> to a formatted string, returning "-/-" for null.
9+
/// Pass a ConverterParameter of the form "F0|ms" or "F1|Mbps" to control the numeric format
10+
/// specifier and the unit suffix, separated by '|'.
11+
/// </summary>
12+
public sealed class NullableDoubleToStringConverter : IValueConverter
13+
{
14+
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
15+
{
16+
if (value is not double d)
17+
return "-/-";
18+
19+
if (parameter is string fmt)
20+
{
21+
var parts = fmt.Split('|');
22+
var format = parts.Length > 0 ? parts[0] : "G";
23+
var unit = parts.Length > 1 ? " " + parts[1] : string.Empty;
24+
return d.ToString(format, culture) + unit;
25+
}
26+
27+
return d.ToString(culture);
28+
}
29+
30+
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
31+
throw new NotImplementedException();
32+
}

Source/NETworkManager.Documentation/ExternalServicesManager.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public static class ExternalServicesManager
1616
new ExternalServicesInfo("ip-api.com", "https://ip-api.com/",
1717
Strings.ExternalService_ip_api_Description),
1818
new ExternalServicesInfo("ipify.org", "https://www.ipify.org/",
19-
Strings.ExternalService_ipify_Description)
19+
Strings.ExternalService_ipify_Description),
20+
new ExternalServicesInfo("speed.cloudflare.com", "https://speed.cloudflare.com/",
21+
Strings.ExternalService_speed_cloudflare_Description)
2022
};
2123
}

Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Lines changed: 100 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Source/NETworkManager.Localization/Resources/Strings.resx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4306,4 +4306,38 @@ You can copy your profile files from “{0}” to “{1}” to migrate your exis
43064306
<data name="ImportProfilesDots" xml:space="preserve">
43074307
<value>Import profiles...</value>
43084308
</data>
4309+
<data name="SpeedTest" xml:space="preserve">
4310+
<value>Speed Test</value>
4311+
</data>
4312+
<data name="RunSpeedTest" xml:space="preserve">
4313+
<value>Run speed test</value>
4314+
</data>
4315+
<data name="FetchingMetadataDots" xml:space="preserve">
4316+
<value>Fetching metadata...</value>
4317+
</data>
4318+
<data name="MeasuringLatencyDots" xml:space="preserve">
4319+
<value>Measuring latency...</value>
4320+
</data>
4321+
<data name="MeasuringDownloadSpeedDots" xml:space="preserve">
4322+
<value>Measuring download speed...</value>
4323+
</data>
4324+
<data name="MeasuringUploadSpeedDots" xml:space="preserve">
4325+
<value>Measuring upload speed...</value>
4326+
</data>
4327+
<data name="SpeedTestDisclaimerMessage" xml:space="preserve">
4328+
<value>Measure download and upload speeds, latency, and jitter with speed.cloudflare.com.
4329+
Cloudflare may log your IP address and network information. See Cloudflare's privacy policy for details.</value>
4330+
</data>
4331+
<data name="Latency" xml:space="preserve">
4332+
<value>Latency</value>
4333+
</data>
4334+
<data name="Jitter" xml:space="preserve">
4335+
<value>Jitter</value>
4336+
</data>
4337+
<data name="ExternalService_speed_cloudflare_Description" xml:space="preserve">
4338+
<value>Speed test service used to measure download speed, upload speed, latency, and jitter.</value>
4339+
</data>
4340+
<data name="Stop" xml:space="preserve">
4341+
<value>Stop</value>
4342+
</data>
43094343
</root>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Newtonsoft.Json;
2+
3+
namespace NETworkManager.Models.Cloudflare;
4+
5+
/// <summary>
6+
/// Cloudflare PoP (Point of Presence) information returned by the
7+
/// <c>speed.cloudflare.com/meta</c> endpoint.
8+
/// </summary>
9+
public class SpeedTestMetaColo
10+
{
11+
/// <summary>
12+
/// IATA airport code of the PoP (e.g. "FRA").
13+
/// </summary>
14+
[JsonProperty("iata")]
15+
public string Iata { get; set; }
16+
17+
/// <summary>
18+
/// City of the PoP (e.g. "Frankfurt-am-Main").
19+
/// </summary>
20+
[JsonProperty("city")]
21+
public string City { get; set; }
22+
23+
/// <summary>
24+
/// ISO 3166-1 alpha-2 country code of the PoP (e.g. "DE").
25+
/// </summary>
26+
[JsonProperty("cca2")]
27+
public string Cca2 { get; set; }
28+
}
29+
30+
/// <summary>
31+
/// Deserialized response of the <c>speed.cloudflare.com/meta</c> endpoint.
32+
/// Provides client and Cloudflare PoP metadata used to enrich the speed
33+
/// test result. Requires the <c>Origin: https://speed.cloudflare.com</c>
34+
/// header on the request, otherwise an empty object is returned.
35+
/// </summary>
36+
public class SpeedTestMetaInfo
37+
{
38+
/// <summary>
39+
/// Public IP address of the requesting client as seen by Cloudflare.
40+
/// </summary>
41+
[JsonProperty("clientIp")]
42+
public string ClientIp { get; set; }
43+
44+
/// <summary>
45+
/// Autonomous System Number of the client's ISP.
46+
/// </summary>
47+
[JsonProperty("asn")]
48+
public int Asn { get; set; }
49+
50+
/// <summary>
51+
/// Human-readable ISP name (e.g. "innogy TelNet").
52+
/// </summary>
53+
[JsonProperty("asOrganization")]
54+
public string AsOrganization { get; set; }
55+
56+
/// <summary>
57+
/// ISO 3166-1 alpha-2 country code of the client (e.g. "DE").
58+
/// </summary>
59+
[JsonProperty("country")]
60+
public string Country { get; set; }
61+
62+
/// <summary>
63+
/// City of the client (e.g. "Bochum").
64+
/// </summary>
65+
[JsonProperty("city")]
66+
public string City { get; set; }
67+
68+
/// <summary>
69+
/// Cloudflare PoP (Point of Presence) details.
70+
/// </summary>
71+
[JsonProperty("colo")]
72+
public SpeedTestMetaColo Colo { get; set; }
73+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace NETworkManager.Models.Cloudflare;
2+
3+
/// <summary>
4+
/// Phase of a Cloudflare speed test run, reported by
5+
/// <see cref="SpeedTestService"/> via <see cref="System.IProgress{T}"/>.
6+
/// </summary>
7+
public enum SpeedTestPhase
8+
{
9+
FetchingMetadata,
10+
MeasuringLatency,
11+
MeasuringDownload,
12+
MeasuringUpload
13+
}
14+
15+
/// <summary>
16+
/// Progress event passed by <see cref="SpeedTestService"/> to update the UI
17+
/// with the currently running measurement phase and the latest live estimate
18+
/// of each metric. Values are <c>null</c> until the first sample for that
19+
/// metric has been collected.
20+
/// </summary>
21+
/// <param name="Phase">Current measurement phase.</param>
22+
/// <param name="DownloadMbps">Live download throughput estimate (Mbps).</param>
23+
/// <param name="UploadMbps">Live upload throughput estimate (Mbps).</param>
24+
/// <param name="LatencyMs">Live latency estimate (50th percentile of probes).</param>
25+
/// <param name="JitterMs">Live jitter estimate (average consecutive delta).</param>
26+
/// <param name="NewDownloadSampleMbps">
27+
/// Set when this emission marks a freshly completed download sample (one HTTP
28+
/// request finished). Mid-stream live updates leave this <c>null</c>.
29+
/// </param>
30+
/// <param name="NewUploadSampleMbps">
31+
/// Set when this emission marks a freshly completed upload sample.
32+
/// </param>
33+
/// <param name="Meta">
34+
/// Cloudflare <c>/meta</c> response, emitted once after metadata is fetched
35+
/// so ISP / location / server details can be displayed before the bandwidth
36+
/// measurements complete.
37+
/// </param>
38+
public record SpeedTestProgress(
39+
SpeedTestPhase Phase,
40+
double? DownloadMbps = null,
41+
double? UploadMbps = null,
42+
double? LatencyMs = null,
43+
double? JitterMs = null,
44+
double? NewDownloadSampleMbps = null,
45+
double? NewUploadSampleMbps = null,
46+
SpeedTestMetaInfo Meta = null);

0 commit comments

Comments
 (0)