Skip to content
Closed
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
33 changes: 33 additions & 0 deletions definitions/network/network_diagnostics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: NetworkDiagnostics
description: Network diagnostic operations (Ping, Traceroute)
type: operate
category: network

operations:
- name: ping
path: Device.IP.Diagnostics.IPPing()
description: Run ICMP ping diagnostic
inputs:
- path: Host
field: host
type: string
required: true
- path: NumberOfRepetitions
field: numberOfRepetitions
type: string
required: false
default: "3"

- name: traceRoute
path: Device.IP.Diagnostics.TraceRoute()
description: Run traceroute diagnostic
inputs:
- path: Host
field: host
type: string
required: true
- path: MaxHopCount
field: maxHopCount
type: string
required: false
default: "30"
5 changes: 5 additions & 0 deletions definitions/wifi/wi_fi_radios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,8 @@ parameters:
type: boolean
writable: true
description: Enables IEEE 802.11h on this radio, which activates both Dynamic Frequency Selection (DFS) and Transmit Power Control (TPC)

- field_name: supportedOperatingChannelBandwidths
path: .SupportedOperatingChannelBandwidths
type: string
description: Comma-separated list of supported channel bandwidths (e.g. "Auto,20MHz,40MHz,80MHz")
191 changes: 101 additions & 90 deletions doc/usp/integration/roadmap_m2.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/generated/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export 'lan_network_info.g.dart';
export 'ipv6settings.g.dart';
export 'dhcp_clients.g.dart';
export 'wan_traffic_stats.g.dart';
export 'network_diagnostics.g.dart';
export 'dhcp_reservations.g.dart';
export 'wan_operations.g.dart';
export 'multi_interface_traffic_stats.g.dart';
Expand Down
34 changes: 34 additions & 0 deletions lib/generated/network_diagnostics.g.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// AUTO-GENERATED CODE - DO NOT EDIT
// This file was generated by usp-codegen
// Any modifications will be overwritten on next generation

import 'package:privacy_gui/usp/services/usp_service.dart';

/// Network diagnostic operations (Ping, Traceroute)
class NetworkDiagnostics {
/// Run ICMP ping diagnostic
static Future<Map<String, dynamic>> ping(
UspService client, {
required String host,
String? numberOfRepetitions,
}) async {
final inputs = <String, String>{};
inputs['Host'] = host;
if (numberOfRepetitions != null)
inputs['NumberOfRepetitions'] = numberOfRepetitions;
Comment on lines +17 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Missing braces in ping() 📘 Rule violation ✓ Correctness

The generated NetworkDiagnostics.ping() uses an if statement without curly braces, which is
likely to violate flutter_lints (e.g., curly_braces_in_flow_control_structures). This can cause
flutter analyze to fail for the PR.
Agent Prompt
## Issue description
The generated file `lib/generated/network_diagnostics.g.dart` contains an `if` statement without curly braces, which is likely to fail `flutter analyze` under `flutter_lints`.

## Issue Context
This file is marked as auto-generated, so fixing the generator/template is preferable to manual edits (otherwise the issue will return on the next codegen run).

## Fix Focus Areas
- lib/generated/network_diagnostics.g.dart[15-20]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

return await client.operate('Device.IP.Diagnostics.IPPing()', args: inputs);
}

/// Run traceroute diagnostic
static Future<Map<String, dynamic>> traceRoute(
UspService client, {
required String host,
String? maxHopCount,
}) async {
final inputs = <String, String>{};
inputs['Host'] = host;
if (maxHopCount != null) inputs['MaxHopCount'] = maxHopCount;
return await client.operate('Device.IP.Diagnostics.TraceRoute()',
args: inputs);
}
}
9 changes: 5 additions & 4 deletions lib/generated/wan_operations.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import 'package:privacy_gui/usp/services/usp_service.dart';
/// WAN DHCP lease renewal operations
class WanOperations {
/// Renew DHCPv4 WAN lease
static Future<void> renewDhcpLease(UspService client) async {
await client.operate('Device.DHCPv4.Client.1.Renew()');
static Future<Map<String, dynamic>> renewDhcpLease(UspService client) async {
return await client.operate('Device.DHCPv4.Client.1.Renew()');
}

/// Renew DHCPv6 WAN lease
static Future<void> renewDhcpv6Lease(UspService client) async {
await client.operate('Device.DHCPv6.Client.1.Renew()');
static Future<Map<String, dynamic>> renewDhcpv6Lease(
UspService client) async {
return await client.operate('Device.DHCPv6.Client.1.Renew()');
}
Comment on lines +10 to 18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Renew return type mismatch 🐞 Bug ✓ Correctness

UspInternetSettingsService.renewDhcpLease() / renewDhcpv6Lease() are declared as Future<void>
but directly return WanOperations.renewDhcpLease() / renewDhcpv6Lease(), which now return
Future<Map<String,dynamic>>. This introduces a Dart static type error and will fail compilation
for USP Internet Settings.
Agent Prompt
### Issue description
`WanOperations.renewDhcpLease()` and `renewDhcpv6Lease()` now return `Future<Map<String,dynamic>>`, but `UspInternetSettingsService.renewDhcpLease()` / `renewDhcpv6Lease()` still declare `Future<void>` and return the generated futures directly, which is a compile-time type error.

### Issue Context
Internet Settings UI/notifier code expects `Future<void>` semantics for renew operations.

### Fix Focus Areas
- lib/usp_page/internet_settings/services/usp_internet_settings_service.dart[89-93]

### Suggested change
Convert the wrappers to `async` and `await` the generated operations, ignoring the returned map:
```dart
Future<void> renewDhcpLease() async {
  await WanOperations.renewDhcpLease(_usp);
}

Future<void> renewDhcpv6Lease() async {
  await WanOperations.renewDhcpv6Lease(_usp);
}
```
(Alternatively, change wrapper + all callers to return/handle the `Map`.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
9 changes: 8 additions & 1 deletion lib/generated/wi_fi_radios.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class WiFiRadio {
final int maxBitRate;
final bool autoChannelEnable;
final bool ieee80211hEnabled;
final String supportedOperatingChannelBandwidths;

const WiFiRadio({
required this.instancePath,
Expand All @@ -34,6 +35,7 @@ class WiFiRadio {
required this.maxBitRate,
required this.autoChannelEnable,
required this.ieee80211hEnabled,
required this.supportedOperatingChannelBandwidths,
});
}

Expand Down Expand Up @@ -77,6 +79,7 @@ class WiFiRadios {
'Device.WiFi.Radio.*.MaxBitRate',
'Device.WiFi.Radio.*.AutoChannelEnable',
'Device.WiFi.Radio.*.IEEE80211hEnabled',
'Device.WiFi.Radio.*.SupportedOperatingChannelBandwidths',
];

/// Fetch all instances via USP Get message
Expand Down Expand Up @@ -112,7 +115,8 @@ class WiFiRadios {
response['${p}TransmitPower'],
response['${p}MaxBitRate'],
response['${p}AutoChannelEnable'],
response['${p}IEEE80211hEnabled']
response['${p}IEEE80211hEnabled'],
response['${p}SupportedOperatingChannelBandwidths']
].every((v) =>
v == null ||
v == '' ||
Expand Down Expand Up @@ -146,6 +150,9 @@ class WiFiRadios {
ieee80211hEnabled: response['${p}IEEE80211hEnabled'] == true ||
response['${p}IEEE80211hEnabled'] == 'true' ||
response['${p}IEEE80211hEnabled'] == '1',
supportedOperatingChannelBandwidths:
(response['${p}SupportedOperatingChannelBandwidths'] ?? '')
as String,
));
}
return WiFiRadios(items: items);
Expand Down
6 changes: 3 additions & 3 deletions lib/page/usp_test/usp_test_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,11 @@ class _UspTestPageState extends State<UspTestPage> {
try {
final args = Map<String, String>.from(jsonDecode(argsJson) as Map? ?? {});
final response = await _service!.operate(command, args: args);
_log(' commandKey = ${response.commandKey}');
if (response.data.isEmpty) {
_log(' commandKey = ${response['commandKey']}');
if (response.isEmpty) {
_log('OPERATE OK (no output)');
} else {
for (final entry in response.data.entries) {
for (final entry in response.entries) {
_log(' ${entry.key} = ${entry.value}');
}
}
Expand Down
8 changes: 8 additions & 0 deletions lib/providers/preservable_notifier_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ mixin PreservableNotifierMixin<
settings: Preservable(original: newSettings, current: newSettings),
status: newStatus ?? state.status,
) as TState;
} else if (newStatus != null) {
// Settings unavailable but status returned (e.g. error) — apply status
// so the UI can exit the loading state and display the error.
state = state.copyWith(status: newStatus) as TState;
}
}
return state;
Expand Down Expand Up @@ -104,6 +108,10 @@ mixin PreservableAutoDisposeNotifierMixin<
settings: Preservable(original: newSettings, current: newSettings),
status: newStatus ?? state.status,
) as TState;
} else if (newStatus != null) {
// Settings unavailable but status returned (e.g. error) — apply status
// so the UI can exit the loading state and display the error.
state = state.copyWith(status: newStatus) as TState;
}
}
return state;
Expand Down
20 changes: 0 additions & 20 deletions lib/usp/models/usp_response.dart

This file was deleted.

2 changes: 1 addition & 1 deletion lib/usp/services/sse_operation_awaiter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class SseOperationAwaiter {

// Step 2: Fire the operate command and capture commandKey for correlation
final operateResponse = await _usp.operate(operateCommand, args: args);
final expectedKey = operateResponse.commandKey;
final expectedKey = operateResponse['commandKey'] as String?;

logger.d('[SSE Operate] Starting $operateCommand '
'(sub=$subscriptionId, commandKey=$expectedKey)');
Expand Down
12 changes: 6 additions & 6 deletions lib/usp/services/usp_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:privacy_gui/core/utils/logger.dart';
import 'package:privacy_gui/usp/models/usp_response.dart';

// Conditional import: use WASM client on Web, stub on other platforms (VM/tests).
import '../stub/usp_client_stub.dart'
Expand Down Expand Up @@ -341,8 +340,9 @@ class UspService {
/// [command] is the command path (e.g., "Device.Reboot()" or
/// "Device.IP.Diagnostics.Ping()").
/// [args] are the input arguments for the command.
/// Returns [UspResponse] with commandKey (for SSE correlation) and output arguments.
Future<UspResponse<Map<String, String>>> operate(String command,
/// Returns a flat map containing `commandKey` (for SSE correlation) and
/// all output arguments from the Operate response.
Future<Map<String, dynamic>> operate(String command,
{Map<String, String> args = const {}}) async {
final id = ++_reqId;
final sw = Stopwatch()..start();
Expand All @@ -351,10 +351,10 @@ class UspService {
sw.stop();
logger.d('[UspService]:#$id OPERATE $command'
'${args.isNotEmpty ? ' — ${args.length} args' : ''}'
' → key=${response.commandKey}, ${response.data.length} output keys'
' → key=${response['commandKey']}, ${response.length} output keys'
' (${sw.elapsedMilliseconds}ms)');
if (response.data.isNotEmpty) {
logger.d('[UspService]:#$id ← ${_mapSummary(response.data)}');
if (response.isNotEmpty) {
logger.d('[UspService]:#$id ← ${_mapSummary(response)}');
}
return response;
}
Expand Down
4 changes: 1 addition & 3 deletions lib/usp/stub/usp_client_stub.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'package:privacy_gui/usp/models/usp_response.dart';

/// Stub implementation of UspClientWeb for non-Web platforms (Dart VM / tests).
///
/// This file is selected by conditional import when dart.library.js_interop
Expand Down Expand Up @@ -54,7 +52,7 @@ class UspClientWeb {
{bool allowPartial = false}) =>
throw UnsupportedError('USP is only available on Web');

Future<UspResponse<Map<String, String>>> operate(String command,
Future<Map<String, dynamic>> operate(String command,
{Map<String, String> args = const {}}) =>
throw UnsupportedError('USP is only available on Web');

Expand Down
25 changes: 13 additions & 12 deletions lib/usp/web/usp_client_wasm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ library usp_client;

import 'dart:js_interop';

import 'package:privacy_gui/usp/models/usp_response.dart';

// Bind to the UspClient class exported in usp_client.js
@JS('UspClient')
extension type UspClientJS._(JSObject _) implements JSObject {
Expand Down Expand Up @@ -157,26 +155,29 @@ class UspClientWeb {
}

/// Executes a USP Operate command.
/// Returns [UspResponse] with commandKey and output arguments.
Future<UspResponse<Map<String, String>>> operate(String command,
///
/// Returns a flat map containing:
/// - `commandKey`: UUID correlator from the USP agent (may be absent)
/// - all output arguments from the Operate response
Future<Map<String, dynamic>> operate(String command,
{Map<String, String> args = const {}}) async {
final result = await _client.operate(command, args.jsify()!).toDart;
if (result == null || result.isUndefinedOrNull) {
return UspResponse(data: {});
}
if (result == null || result.isUndefinedOrNull) return {};
final map = result.dartify() as Map?;
if (map == null) return UspResponse(data: {});
if (map == null) return {};

final output = <String, dynamic>{};
final commandKey = map['commandKey']?.toString();
if (commandKey != null && commandKey.isNotEmpty) {
output['commandKey'] = commandKey;
}
final rawOutputArgs = map['outputArgs'];
final outputArgs = <String, String>{};
if (rawOutputArgs is Map) {
for (final entry in rawOutputArgs.entries) {
outputArgs[entry.key.toString()] = entry.value.toString();
output[entry.key.toString()] = entry.value.toString();
}
}

return UspResponse(data: outputArgs, commandKey: commandKey);
return output;
}

/// Lists all active OBUSPA subscriptions on the router.
Expand Down
Loading
Loading