1002 lines
25 KiB
Markdown
1002 lines
25 KiB
Markdown
# iperf3-gui Implementation Plan
|
|
|
|
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
|
|
|
**Goal:** Create a Flutter Android app that bundles iperf3 and provides a simple GUI for running network bandwidth tests (client mode only, MVP).
|
|
|
|
**Architecture:** MVVM layered architecture following flutter-apply-architecture-best-practices — UI layer (views/ViewModels), data layer (iperf service, result parser), domain layer (test configuration, result model). Extensible for future server mode, history, etc.
|
|
|
|
**Tech Stack:**
|
|
- Flutter 3.47.1 (Dart 3.13.1)
|
|
- Android API level 28+ (matches bundled iperf3 binary requirements)
|
|
- Provider for state management (consistent with gps-speedo project)
|
|
- iperf3 v3.21 (bundled prebuilt binaries for arm64-v8a, armeabi-v7a)
|
|
- MIT License
|
|
|
|
---
|
|
|
|
## Task 1: Scaffold Flutter project with correct package name
|
|
|
|
**Objective:** Create Flutter project with com.novoyuuparosk.iperf3gui package name.
|
|
|
|
**Files:**
|
|
- Create: `pubspec.yaml`
|
|
- Create: `lib/main.dart`
|
|
- Create: `android/app/build.gradle.kts` (updated package name)
|
|
|
|
**Step 1: Scaffold Flutter project**
|
|
|
|
Run:
|
|
```bash
|
|
cd ~/dev/android-stuff/iperf3-gui
|
|
flutter create . --org novoyuuparosk --project-name iperf3-gui --android-language kotlin --ios-language swift 2>&1
|
|
```
|
|
|
|
Expected output: Flutter project scaffolded with all default files.
|
|
|
|
**Step 2: Verify package name**
|
|
|
|
Check `android/app/build.gradle.kts` contains:
|
|
```kotlin
|
|
defaultConfig {
|
|
applicationId = "com.novoyuuparosk.iperf3gui"
|
|
...
|
|
}
|
|
```
|
|
|
|
Verify:
|
|
```bash
|
|
grep -r "com.novoyuuparosk.iperf3gui" android/
|
|
```
|
|
|
|
Expected: Found at least one match.
|
|
|
|
**Step 3: Verify build**
|
|
|
|
Run:
|
|
```bash
|
|
flutter analyze 2>&1
|
|
```
|
|
|
|
Expected: No errors or warnings.
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add .
|
|
git commit -m "chore: scaffold Flutter project with correct package name"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Add dependencies and configure analysis options
|
|
|
|
**Objective:** Add required dependencies (provider) and set up analysis_options.yaml for Dart/Flutter linting.
|
|
|
|
**Files:**
|
|
- Create: `pubspec.yaml` (add dependencies)
|
|
- Create: `analysis_options.yaml`
|
|
|
|
**Step 1: Add dependencies to pubspec.yaml**
|
|
|
|
```yaml
|
|
dependencies:
|
|
flutter:
|
|
sdk: flutter
|
|
provider: ^6.1.2
|
|
|
|
dev_dependencies:
|
|
flutter_test:
|
|
sdk: flutter
|
|
flutter_lints: ^5.0.0
|
|
```
|
|
|
|
**Step 2: Create analysis_options.yaml**
|
|
|
|
```yaml
|
|
include: package:flutter_lints/flutter.yaml
|
|
|
|
analyzer:
|
|
language:
|
|
strict-casts: true
|
|
strict-inference: true
|
|
strict-raw-types: true
|
|
|
|
linter:
|
|
rules:
|
|
use_super_parameters: true
|
|
prefer_const_constructors: true
|
|
avoid_print: true
|
|
```
|
|
|
|
**Step 3: Get dependencies**
|
|
|
|
```bash
|
|
flutter pub get 2>&1
|
|
```
|
|
|
|
Expected: Dependencies fetched successfully.
|
|
|
|
**Step 4: Run analysis**
|
|
|
|
```bash
|
|
flutter analyze 2>&1
|
|
```
|
|
|
|
Expected: No errors or warnings.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add pubspec.yaml analysis_options.yaml
|
|
git commit -m "feat: add provider dependency and analysis options"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: Create domain models for test configuration and results
|
|
|
|
**Objective:** Create immutable domain models for test configuration and iperf3 output parsing.
|
|
|
|
**Files:**
|
|
- Create: `lib/domain/models/test_configuration.dart`
|
|
- Create: `lib/domain/models/iperf3_result.dart`
|
|
- Test: `test/domain/models/test_configuration_test.dart`
|
|
- Test: `test/domain/models/iperf3_result_test.dart`
|
|
|
|
**Step 1: Create TestConfiguration model**
|
|
|
|
Create `lib/domain/models/test_configuration.dart`:
|
|
|
|
```dart
|
|
/// Configuration for an iperf3 client test.
|
|
class TestConfiguration {
|
|
final String server;
|
|
final int port;
|
|
final Duration duration;
|
|
final int interval;
|
|
final int parallelStreams;
|
|
|
|
const TestConfiguration({
|
|
this.server = 'iperf.es.net',
|
|
this.port = 5201,
|
|
this.duration = const Duration(seconds: 10),
|
|
this.interval = 1,
|
|
this.parallelStreams = 1,
|
|
});
|
|
|
|
/// Convert to iperf3 CLI arguments.
|
|
List<String> toCliArgs() {
|
|
final args = <String>[
|
|
'-c', server,
|
|
'-p', '$port',
|
|
'-t', '${duration.inSeconds}',
|
|
'-i', '$interval',
|
|
'-P', '$parallelStreams',
|
|
];
|
|
return args;
|
|
}
|
|
|
|
/// Validate the configuration.
|
|
bool get isValid {
|
|
if (server.isEmpty) return false;
|
|
if (port < 1 || port > 65535) return false;
|
|
if (duration.inSeconds < 1) return false;
|
|
if (interval < 1) return false;
|
|
if (parallelStreams < 1) return false;
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Create Iperf3Result model**
|
|
|
|
Create `lib/domain/models/iperf3_result.dart`:
|
|
|
|
```dart
|
|
/// Raw result from iperf3 execution.
|
|
class Iperf3Result {
|
|
final String rawOutput;
|
|
final int exitCode;
|
|
|
|
const Iperf3Result({
|
|
required this.rawOutput,
|
|
required this.exitCode,
|
|
});
|
|
}
|
|
```
|
|
|
|
**Step 3: Write tests**
|
|
|
|
Create `test/domain/models/test_configuration_test.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
|
|
|
void main() {
|
|
group('TestConfiguration', () {
|
|
test('defaults produce valid configuration', () {
|
|
const config = TestConfiguration();
|
|
expect(config.isValid, isTrue);
|
|
});
|
|
|
|
test('empty server is invalid', () {
|
|
const config = TestConfiguration(server: '');
|
|
expect(config.isValid, isFalse);
|
|
});
|
|
|
|
test('port out of range is invalid', () {
|
|
const config = TestConfiguration(port: 0);
|
|
expect(config.isValid, isFalse);
|
|
});
|
|
|
|
test('CLI args contain server and port', () {
|
|
const config = TestConfiguration(server: '192.168.1.1', port: 1234);
|
|
final args = config.toCliArgs();
|
|
expect(args, contains('-c'));
|
|
expect(args, contains('192.168.1.1'));
|
|
expect(args, contains('-p'));
|
|
expect(args, contains('1234'));
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
Create `test/domain/models/iperf3_result_test.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:iperf3gui/domain/models/iperf3_result.dart';
|
|
|
|
void main() {
|
|
group('Iperf3Result', () {
|
|
test('can be instantiated with raw output', () {
|
|
const result = Iperf3Result(
|
|
rawOutput: 'Test complete',
|
|
exitCode: 0,
|
|
);
|
|
expect(result.rawOutput, equals('Test complete'));
|
|
expect(result.exitCode, equals(0));
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
**Step 4: Run tests**
|
|
|
|
```bash
|
|
flutter test test/domain/models/ 2>&1
|
|
```
|
|
|
|
Expected: All tests pass.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add lib/domain/models/ test/domain/models/
|
|
git commit -m "feat: add domain models for test configuration and results"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Implement iperf3 binary service (Process runner)
|
|
|
|
**Objective:** Create a service that spawns iperf3 as a subprocess and captures output. Handles binary extraction from assets.
|
|
|
|
**Files:**
|
|
- Create: `lib/data/services/iperf3_service.dart`
|
|
- Test: `test/data/services/iperf3_service_test.dart`
|
|
|
|
**Step 1: Create iperf3_service.dart**
|
|
|
|
Create `lib/data/services/iperf3_service.dart`:
|
|
|
|
```dart
|
|
import 'dart:io';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
|
import 'package:iperf3gui/domain/models/iperf3_result.dart';
|
|
|
|
/// Service that runs iperf3 via Process.spawn and captures output.
|
|
class Iperf3Service {
|
|
final String _binaryPath;
|
|
|
|
Iperf3Service({required String binaryPath}) : _binaryPath = binaryPath;
|
|
|
|
/// Run iperf3 with the given configuration.
|
|
///
|
|
/// Returns an [Iperf3Result] with stdout/stderr and exit code.
|
|
Future<Iperf3Result> run(TestConfiguration config) async {
|
|
final args = config.toCliArgs();
|
|
|
|
final process = await Process.start(
|
|
_binaryPath,
|
|
args,
|
|
runInShell: true,
|
|
);
|
|
|
|
final stdoutBuffer = <int>[];
|
|
final stderrBuffer = <int>[];
|
|
|
|
final stdoutStream = process.stdout.transform(utf8.decoder).listen(
|
|
(data) => stdoutBuffer.addAll(utf8.encode(data)),
|
|
);
|
|
final stderrStream = process.stderr.transform(utf8.decoder).listen(
|
|
(data) => stderrBuffer.addAll(utf8.encode(data)),
|
|
);
|
|
|
|
await process.exitCode;
|
|
await stdoutStream.cancel();
|
|
await stderrStream.cancel();
|
|
|
|
final rawOutput = utf8.decode(stdoutBuffer) + utf8.decode(stderrBuffer);
|
|
|
|
return Iperf3Result(
|
|
rawOutput: rawOutput,
|
|
exitCode: await process.exitCode,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Create placeholder binary path**
|
|
|
|
Create `lib/data/services/iperf3_binary_path.dart`:
|
|
|
|
```dart
|
|
/// Placeholder for iperf3 binary path — will be overridden at runtime.
|
|
///
|
|
/// On Android, the binary should be bundled as an asset and
|
|
/// extracted to a temporary directory before first use.
|
|
class Iperf3BinaryPathProvider {
|
|
static String get path {
|
|
// TODO: Implement asset extraction on first run
|
|
return '/data/local/tmp/iperf3';
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 3: Write tests**
|
|
|
|
Create `test/data/services/iperf3_service_test.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
|
import 'package:iperf3gui/domain/models/iperf3_result.dart';
|
|
|
|
void main() {
|
|
group('Iperf3Service', () {
|
|
test('can be instantiated', () {
|
|
final service = Iperf3Service(binaryPath: '/tmp/iperf3');
|
|
expect(service, isNotNull);
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
**Step 4: Run tests**
|
|
|
|
```bash
|
|
flutter test test/data/services/ 2>&1
|
|
```
|
|
|
|
Expected: Tests pass.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add lib/data/services/ test/data/services/
|
|
git commit -m "feat: add iperf3 service to spawn subprocess"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Create ViewModel for test run state
|
|
|
|
**Objective:** Create a ViewModel that manages the state of a test run (idle, running, completed, error).
|
|
|
|
**Files:**
|
|
- Create: `lib/ui/features/test_run/test_run_view_model.dart`
|
|
- Test: `test/ui/features/test_run/test_run_view_model_test.dart`
|
|
|
|
**Step 1: Create TestRunViewModel**
|
|
|
|
Create `lib/ui/features/test_run/test_run_view_model.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:iperf3gui/data/services/iperf3_service.dart';
|
|
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
|
import 'package:iperf3gui/domain/models/iperf3_result.dart';
|
|
|
|
/// State of the test run.
|
|
enum TestRunState { idle, running, completed, error }
|
|
|
|
/// ViewModel for managing test run state.
|
|
class TestRunViewModel extends ChangeNotifier {
|
|
final Iperf3Service _iperf3Service;
|
|
|
|
TestRunState _state = TestRunState.idle;
|
|
TestRunState get state => _state;
|
|
|
|
Iperf3Result? _result;
|
|
Iperf3Result? get result => _result;
|
|
|
|
TestConfiguration? _currentConfig;
|
|
TestConfiguration? get currentConfig => _currentConfig;
|
|
|
|
TestRunViewModel({required Iperf3Service iperf3Service})
|
|
: _iperf3Service = iperf3Service;
|
|
|
|
/// Start a test with the given configuration.
|
|
Future<void> runTest(TestConfiguration config) async {
|
|
if (!config.isValid) {
|
|
_state = TestRunState.error;
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
|
|
_state = TestRunState.running;
|
|
_currentConfig = config;
|
|
_result = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final result = await _iperf3Service.run(config);
|
|
_result = result;
|
|
_state = result.exitCode == 0 ? TestRunState.completed : TestRunState.error;
|
|
} catch (e) {
|
|
_state = TestRunState.error;
|
|
} finally {
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Reset to idle state.
|
|
void reset() {
|
|
_state = TestRunState.idle;
|
|
_result = null;
|
|
_currentConfig = null;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Write tests**
|
|
|
|
Create `test/ui/features/test_run/test_run_view_model_test.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:iperf3gui/ui/features/test_run/test_run_view_model.dart';
|
|
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
|
|
|
void main() {
|
|
group('TestRunViewModel', () {
|
|
test('starts in idle state', () {
|
|
// Mock not needed for basic state test
|
|
final vm = TestRunViewModel(
|
|
iperf3Service: _MockIperf3Service(),
|
|
);
|
|
expect(vm.state, equals(TestRunState.idle));
|
|
});
|
|
|
|
test('can be reset', () {
|
|
final vm = TestRunViewModel(
|
|
iperf3Service: _MockIperf3Service(),
|
|
);
|
|
vm.reset();
|
|
expect(vm.state, equals(TestRunState.idle));
|
|
});
|
|
});
|
|
}
|
|
|
|
class _MockIperf3Service {
|
|
Future<void> run(dynamic config) async {}
|
|
}
|
|
```
|
|
|
|
**Step 3: Run tests**
|
|
|
|
```bash
|
|
flutter test test/ui/features/test_run/ 2>&1
|
|
```
|
|
|
|
Expected: Tests pass.
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add lib/ui/features/test_run/ test/ui/features/test_run/
|
|
git commit -m "feat: add TestRunViewModel with state management"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Create the main test screen UI
|
|
|
|
**Objective:** Build the test screen with host/port input fields, "go" button, and raw output text box. Uses Provider for state.
|
|
|
|
**Files:**
|
|
- Create: `lib/ui/features/test_run/test_run_screen.dart`
|
|
- Modify: `lib/main.dart`
|
|
|
|
**Step 1: Create test_run_screen.dart**
|
|
|
|
Create `lib/ui/features/test_run/test_run_screen.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:iperf3gui/ui/features/test_run/test_run_view_model.dart';
|
|
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
|
|
|
class TestRunScreen extends StatelessWidget {
|
|
const TestRunScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ChangeNotifierProvider(
|
|
create: (_) => TestRunViewModel(
|
|
iperf3Service: Iperf3Service(
|
|
binaryPath: Iperf3BinaryPathProvider.path,
|
|
),
|
|
),
|
|
child: _TestRunContent(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TestRunContent extends StatelessWidget {
|
|
final _serverController = TextEditingController(text: 'iperf.es.net');
|
|
final _portController = TextEditingController(text: '5201');
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('iperf3-gui'),
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Server input
|
|
TextField(
|
|
controller: _serverController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Server',
|
|
hintText: 'e.g. iperf.es.net or 192.168.1.1',
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Port input
|
|
TextField(
|
|
controller: _portController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Port',
|
|
hintText: '5201',
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Go button
|
|
Consumer<TestRunViewModel>(
|
|
builder: (_, vm, __) {
|
|
return ElevatedButton(
|
|
onPressed: vm.state == TestRunState.running
|
|
? null
|
|
: () {
|
|
final config = TestConfiguration(
|
|
server: _serverController.text,
|
|
port: int.tryParse(_portController.text) ?? 5201,
|
|
);
|
|
vm.runTest(config);
|
|
},
|
|
child: vm.state == TestRunState.running
|
|
? const CircularProgressIndicator()
|
|
: const Text('GO'),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Status indicator
|
|
Consumer<TestRunViewModel>(
|
|
builder: (_, vm, __) {
|
|
final statusText = switch (vm.state) {
|
|
TestRunState.idle => '',
|
|
TestRunState.running => 'Running...',
|
|
TestRunState.completed => 'Test complete',
|
|
TestRunState.error => 'Error',
|
|
};
|
|
return Text(
|
|
statusText,
|
|
style: TextStyle(
|
|
color: vm.state == TestRunState.error ? Colors.red : Colors.grey,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Raw output text box
|
|
Expanded(
|
|
child: Consumer<TestRunViewModel>(
|
|
builder: (_, vm, __) {
|
|
final output = vm.result?.rawOutput ?? '';
|
|
return TextField(
|
|
controller: TextEditingController(text: output),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Raw Output',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
maxLines: null,
|
|
readOnly: true,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Update main.dart**
|
|
|
|
Modify `lib/main.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:iperf3gui/ui/features/test_run/test_run_screen.dart';
|
|
|
|
void main() {
|
|
runApp(const Iperf3GuiApp());
|
|
}
|
|
|
|
class Iperf3GuiApp extends StatelessWidget {
|
|
const Iperf3GuiApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'iperf3-gui',
|
|
theme: ThemeData(
|
|
colorSchemeSeed: Colors.blue,
|
|
useMaterial3: true,
|
|
),
|
|
home: const TestRunScreen(),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 3: Verify build**
|
|
|
|
```bash
|
|
flutter analyze 2>&1
|
|
```
|
|
|
|
Expected: No errors or warnings.
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add lib/main.dart lib/ui/features/test_run/
|
|
git commit -m "feat: add test run screen with host/port inputs and go button"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Bundle iperf3 binary assets
|
|
|
|
**Objective:** Add iperf3 prebuilt binaries to the app as assets. Download from davidBar-On/android-iperf3 gh-pages.
|
|
|
|
**Files:**
|
|
- Create: `assets/iperf3/` directory
|
|
- Modify: `pubspec.yaml` (add assets)
|
|
- Modify: `android/app/src/main/AndroidManifest.xml` (add INTERNET permission)
|
|
|
|
**Step 1: Create assets directory**
|
|
|
|
```bash
|
|
mkdir -p assets/iperf3/arm64-v8a
|
|
mkdir -p assets/iperf3/armeabi-v7a
|
|
```
|
|
|
|
**Step 2: Download iperf3 v3.21 binaries**
|
|
|
|
Download from https://github.com/davidBar-On/android-iperf3/tree/gh-pages/libs/iperf-3.21/
|
|
|
|
For arm64-v8a:
|
|
```bash
|
|
curl -L -o assets/iperf3/arm64-v8a/iperf3 "https://raw.githubusercontent.com/davidBar-On/android-iperf3/gh-pages/libs/iperf-3.21/arm64-v8a/iperf3"
|
|
```
|
|
|
|
For armeabi-v7a:
|
|
```bash
|
|
curl -L -o assets/iperf3/armeabi-v7a/iperf3 "https://raw.githubusercontent.com/davidBar-On/android-iperf3/gh-pages/libs/iperf-3.21/armeabi-v7a/iperf3"
|
|
```
|
|
|
|
Verify:
|
|
```bash
|
|
file assets/iperf3/arm64-v8a/iperf3 2>&1 | grep ELF
|
|
file assets/iperf3/armeabi-v7a/iperf3 2>&1 | grep ELF
|
|
```
|
|
|
|
Expected: Both files are ELF executables.
|
|
|
|
**Step 3: Update pubspec.yaml**
|
|
|
|
Add under `flutter:` section:
|
|
```yaml
|
|
assets:
|
|
- assets/iperf3/arm64-v8a/
|
|
- assets/iperf3/armeabi-v7a/
|
|
```
|
|
|
|
**Step 4: Add INTERNET permission to AndroidManifest.xml**
|
|
|
|
Modify `android/app/src/main/AndroidManifest.xml`:
|
|
|
|
```xml
|
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
```
|
|
|
|
**Step 5: Run pub get**
|
|
|
|
```bash
|
|
flutter pub get 2>&1
|
|
```
|
|
|
|
Expected: Dependencies and assets fetched.
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add assets/ pubspec.yaml android/app/src/main/AndroidManifest.xml
|
|
git commit -m "feat: bundle iperf3 v3.21 binaries and add INTERNET permission"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Implement binary extraction at runtime
|
|
|
|
**Objective:** Extract bundled iperf3 binary to device storage on first run and provide the path to Iperf3Service.
|
|
|
|
**Files:**
|
|
- Modify: `lib/data/services/iperf3_service.dart`
|
|
- Modify: `lib/main.dart` (add initialization)
|
|
|
|
**Step 1: Update iperf3_service.dart with asset extraction**
|
|
|
|
Replace `lib/data/services/iperf3_binary_path.dart` with:
|
|
|
|
```dart
|
|
import 'dart:io';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
/// Provides the path to the iperf3 binary on the device.
|
|
///
|
|
/// On first run, extracts the bundled asset to the app's cache
|
|
/// directory. Subsequent runs reuse the extracted binary.
|
|
class Iperf3BinaryPathProvider {
|
|
static String? _cachedPath;
|
|
|
|
static Future<String> get path async {
|
|
if (_cachedPath != null) return _cachedPath!;
|
|
|
|
final cacheDir = await getApplicationCacheDirectory();
|
|
final binaryDir = Directory('${cacheDir.path}/iperf3');
|
|
if (!await binaryDir.exists()) {
|
|
await binaryDir.create(recursive: true);
|
|
}
|
|
|
|
// Detect ABI and extract
|
|
final abi = await _detectAbi();
|
|
final assetPath = 'assets/iperf3/$abi/iperf3';
|
|
|
|
final bytes = await rootBundle.load(assetPath);
|
|
final file = File('${binaryDir.path}/iperf3');
|
|
await file.writeAsBytes(bytes.buffer.asUint8List());
|
|
await file.setPermissions('rwxr-xr-x');
|
|
|
|
_cachedPath = file.path;
|
|
return file.path;
|
|
}
|
|
|
|
static Future<String> _detectAbi() async {
|
|
// Use Process.run to query ro.product.cpu.abi
|
|
final result = await Process.run(
|
|
'getprop',
|
|
['ro.product.cpu.abi'],
|
|
);
|
|
final abi = result.stdout.toString().trim();
|
|
if (abi.contains('arm64')) return 'arm64-v8a';
|
|
return 'armeabi-v7a';
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Add path_provider dependency**
|
|
|
|
Modify `pubspec.yaml`:
|
|
```yaml
|
|
dependencies:
|
|
...
|
|
path_provider: ^2.1.4
|
|
```
|
|
|
|
Then:
|
|
```bash
|
|
flutter pub get 2>&1
|
|
```
|
|
|
|
**Step 3: Update main.dart to initialize binary**
|
|
|
|
Modify `lib/main.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:iperf3gui/data/services/iperf3_binary_path.dart';
|
|
import 'package:iperf3gui/ui/features/test_run/test_run_screen.dart';
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
// Ensure iperf3 binary is available
|
|
await Iperf3BinaryPathProvider.path;
|
|
runApp(const Iperf3GuiApp());
|
|
}
|
|
```
|
|
|
|
**Step 4: Verify build**
|
|
|
|
```bash
|
|
flutter analyze 2>&1
|
|
```
|
|
|
|
Expected: No errors or warnings.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add lib/data/services/ iperf3_binary_path.dart
|
|
git commit -m "feat: implement iperf3 binary extraction from assets"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 9: Add iperf3 license file
|
|
|
|
**Objective:** Include the iperf3 BSD license in the app to comply with redistribution requirements.
|
|
|
|
**Files:**
|
|
- Create: `assets/LICENSE-iperf3`
|
|
|
|
**Step 1: Create license file**
|
|
|
|
Create `assets/LICENSE-iperf3`:
|
|
|
|
```
|
|
Copyright (c) 2014-2024, The Regents of the University of California,
|
|
through Lawrence Berkeley National Laboratory (subject to receipt of any
|
|
required approvals from the U.S. Dept. of Energy). All rights reserved.
|
|
|
|
If you have questions about your rights to use or distribute this software,
|
|
please contact Berkeley Lab's Intellectual Property Office at
|
|
IPO@lbl.gov.
|
|
|
|
NOTICE. This Software was developed under funding from the U.S. Department
|
|
of Energy and the U.S. Government consequently retains certain rights. As
|
|
such, the U.S. Government has been granted for itself and others acting on
|
|
its behalf a paid-up, nonexclusive, irrevocable, worldwide license in the
|
|
Software to reproduce, distribute copies to the public, prepare derivative
|
|
works, and perform publicly and display publicly, and to permit others to do so.
|
|
```
|
|
|
|
**Step 2: Add to pubspec.yaml as an asset**
|
|
|
|
```yaml
|
|
assets:
|
|
- assets/iperf3/arm64-v8a/
|
|
- assets/iperf3/armeabi-v7a/
|
|
- assets/LICENSE-iperf3
|
|
```
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add assets/LICENSE-iperf3 pubspec.yaml
|
|
git commit -m "docs: add iperf3 BSD license for redistribution compliance"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 10: Test on device and verify
|
|
|
|
**Objective:** Build and install on the Android device, verify the app works end-to-end.
|
|
|
|
**Files:**
|
|
- Verify: `~/dev/android-stuff/iperf3-gui/` builds successfully
|
|
|
|
**Step 1: Check device connection**
|
|
|
|
```bash
|
|
adb devices 2>&1
|
|
```
|
|
|
|
Expected: At least one device listed (QV7100JQ52 for Sony XQ-AS72).
|
|
|
|
**Step 2: Build APK**
|
|
|
|
```bash
|
|
flutter build apk --release 2>&1
|
|
```
|
|
|
|
Expected: APK built to `build/app/outputs/flutter-apk/app-release.apk`.
|
|
|
|
**Step 3: Install on device**
|
|
|
|
```bash
|
|
adb install build/app/outputs/flutter-apk/app-release.apk 2>&1
|
|
```
|
|
|
|
Expected: "Success" message.
|
|
|
|
**Step 4: Launch and test**
|
|
|
|
```bash
|
|
adb shell am start -n com.novoyuuparosk.iperf3gui/.MainActivity 2>&1
|
|
```
|
|
|
|
**Step 5: Verify app opens**
|
|
|
|
Take screenshot:
|
|
```bash
|
|
adb shell screencap -p /sdcard/test_screenshot.png
|
|
adb pull /sdcard/test_screenshot.png 2>&1
|
|
```
|
|
|
|
Expected: Screenshot shows the iperf3-gui app UI.
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add .
|
|
git commit -m "chore: verify MVP build and device installation"
|
|
```
|
|
|
|
---
|
|
|
|
## Future Extensions (out of scope for MVP)
|
|
|
|
These are designed into the architecture but not implemented in v1:
|
|
|
|
- **Server mode**: Add server mode with similar UI
|
|
- **History**: Store test results in SharedPreferences or SQLite
|
|
- **Advanced options**: Duration, parallel streams, interval, bandwidth limit
|
|
- **Results parsing**: Parse iperf3 stdout into structured data instead of raw text
|
|
- **Protocol selection**: TCP, UDP, SCTP options
|
|
- **WiFi scanning**: Helper to find iperf3 servers on local network
|
|
|
|
---
|
|
|
|
## Risks and Mitigations
|
|
|
|
| Risk | Mitigation |
|
|
|------|------------|
|
|
| iperf3 binary download blocked | Use raw.githubusercontent.com or mirror |
|
|
| Binary extraction permission issues | Use cache directory (app-writable) |
|
|
| ABI detection fails | Fallback to armeabi-v7a |
|
|
| iperf3 requires root | Document that it runs as non-root |
|
|
| Process spawn fails on some devices | Add error handling and user feedback |
|