Skip to content
Merged
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
40 changes: 40 additions & 0 deletions lib/page/nodes/models/backhaul_info_ui_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:convert';

import 'package:equatable/equatable.dart';

/// UI-friendly representation of backhaul information
///
/// Replaces BackHaulInfoData (JNAP model) in State/Provider layers.
/// Per constitution Article V Section 5.3.1 - separate models per layer.
class BackhaulInfoUIModel extends Equatable {
final String deviceUUID;
final String connectionType;
final String timestamp;

const BackhaulInfoUIModel({
required this.deviceUUID,
required this.connectionType,
required this.timestamp,
});

@override
List<Object?> get props => [deviceUUID, connectionType, timestamp];

Map<String, dynamic> toMap() => {
'deviceUUID': deviceUUID,
'connectionType': connectionType,
'timestamp': timestamp,
};

factory BackhaulInfoUIModel.fromMap(Map<String, dynamic> map) =>
BackhaulInfoUIModel(
deviceUUID: map['deviceUUID'] ?? '',
connectionType: map['connectionType'] ?? '',
timestamp: map['timestamp'] ?? '',
);

String toJson() => json.encode(toMap());

factory BackhaulInfoUIModel.fromJson(String source) =>
BackhaulInfoUIModel.fromMap(json.decode(source));
}
233 changes: 33 additions & 200 deletions lib/page/nodes/providers/add_nodes_provider.dart
Original file line number Diff line number Diff line change
@@ -1,65 +1,33 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:privacy_gui/core/jnap/actions/better_action.dart';
import 'package:privacy_gui/core/jnap/models/back_haul_info.dart';
import 'package:privacy_gui/core/data/providers/device_manager_state.dart';
import 'package:privacy_gui/core/data/providers/polling_provider.dart';
import 'package:privacy_gui/core/jnap/result/jnap_result.dart';
import 'package:privacy_gui/core/jnap/router_repository.dart';
import 'package:privacy_gui/core/utils/bench_mark.dart';
import 'package:privacy_gui/core/utils/devices.dart';
import 'package:privacy_gui/core/utils/logger.dart';
import 'package:privacy_gui/page/nodes/providers/add_nodes_state.dart';
import 'package:privacy_gui/page/nodes/services/add_nodes_service.dart';

final addNodesProvider =
NotifierProvider.autoDispose<AddNodesNotifier, AddNodesState>(
() => AddNodesNotifier());

class AddNodesNotifier extends AutoDisposeNotifier<AddNodesState> {
/// Get AddNodesService instance
AddNodesService get _service => ref.read(addNodesServiceProvider);

@override
AddNodesState build() => const AddNodesState();

Future<void> setAutoOnboardingSettings() {
return ref
.read(routerRepositoryProvider)
.send(JNAPAction.setBluetoothAutoOnboardingSettings,
data: {
'isAutoOnboardingEnabled': true,
},
auth: true);
return _service.setAutoOnboardingSettings();
}

Future<bool> getAutoOnboardingSettings() {
return ref
.read(routerRepositoryProvider)
.send(JNAPAction.getBluetoothAutoOnboardingSettings, auth: true)
.then(
(response) => response.output['isAutoOnboardingEnabled'] ?? false);
}

FutureOr getAutoOnboardingStatus() {
return pollAutoOnboardingStatus(oneTake: true).first;
return _service.getAutoOnboardingSettings();
}

Stream<JNAPResult> pollAutoOnboardingStatus({bool oneTake = false}) {
return ref.read(routerRepositoryProvider).scheduledCommand(
action: JNAPAction.getBluetoothAutoOnboardingStatus,
maxRetry: oneTake ? 1 : 18,
retryDelayInMilliSec: 10000,
firstDelayInMilliSec: oneTake ? 100 : 3000,
condition: (result) {
if (result is JNAPSuccess) {
final status = result.output['autoOnboardingStatus'];
return status == 'Idle' || status == 'Complete';
}
return false;
},
onCompleted: (_) {
logger.d('[AddNodes]: GetAutoOnboardingStatus Done!');
},
auth: true,
);
FutureOr<Map<String, dynamic>> getAutoOnboardingStatus() {
return _service.pollAutoOnboardingStatus(oneTake: true).first;
}

Future startAutoOnboarding() async {
Expand All @@ -69,13 +37,8 @@ class AddNodesNotifier extends AutoDisposeNotifier<AddNodesState> {

ref.read(pollingProvider.notifier).stopPolling();

// final nodeSnapshot =
// List<LinksysDevice>.from(ref.read(deviceManagerProvider).deviceList)
// .toList();

// Commence the auto-onboarding process
final repo = ref.read(routerRepositoryProvider);
await repo.send(JNAPAction.startBlueboothAutoOnboarding, auth: true);
await _service.startAutoOnboarding();

bool onboardingProceed = false;
// For AutoOnboarding 2 service, there has no deviceOnboardingStatus
Expand All @@ -85,16 +48,14 @@ class AddNodesNotifier extends AutoDisposeNotifier<AddNodesState> {

state = state.copyWith(isLoading: true, loadingMessage: 'searching');

await for (final result in pollAutoOnboardingStatus()) {
await for (final result in _service.pollAutoOnboardingStatus()) {
logger.d('[AddNodes]: GetAutoOnboardingStatus result: $result');
// Update onboarding status
if (result is JNAPSuccess) {
if (result.output['autoOnboardingStatus'] == 'Onboarding') {
onboardingProceed = true;
}
// Set deviceOnboardingStatus data
deviceOnboardingStatus = result.output['deviceOnboardingStatus'] ?? [];
if (result['status'] == 'Onboarding') {
onboardingProceed = true;
}
// Set deviceOnboardingStatus data
deviceOnboardingStatus = result['deviceOnboardingStatus'] ?? [];
}
// Get onboarded device data
anyOnboarded = List.from(deviceOnboardingStatus)
Expand All @@ -112,10 +73,11 @@ class AddNodesNotifier extends AutoDisposeNotifier<AddNodesState> {
'[AddNodes]: Number of onboarded MAC addresses = ${onboardedMACList.length}');
List<LinksysDevice> addedDevices = [];
List<LinksysDevice> childNodes = [];
List<BackHaulInfoData> backhaulInfoList = [];
List<LinksysDevice> childNodesWithBackhaul = [];
state = state.copyWith(isLoading: true, loadingMessage: 'onboarding');
if (onboardingProceed && anyOnboarded) {
await for (final result in pollForNodesOnline(onboardedMACList)) {
await for (final result
in _service.pollForNodesOnline(onboardedMACList)) {
childNodes =
result.where((element) => element.nodeType != null).toList();
addedDevices = result
Expand All @@ -131,182 +93,53 @@ class AddNodesNotifier extends AutoDisposeNotifier<AddNodesState> {
logger.d(
'[AddNodes]: [pollForNodesOnline] added devices: ${addedDevices.map((d) => d.toJson()).join(', ')}');
}
await for (final result in pollNodesBackhaulInfo(childNodes)) {
backhaulInfoList = result;
// Poll and merge backhaul info using Service
await for (final result in _service.pollNodesBackhaulInfo(childNodes)) {
childNodesWithBackhaul =
_service.collectChildNodeData(childNodes, result);
}
}
childNodes.sort((a, b) => a.isAuthority ? -1 : 1);
final polling = ref.read(pollingProvider.notifier);
await polling.forcePolling().then((value) => polling.startPolling());
// logger.d('[AddNodes]: Update state: nodesSnapshot = $nodeSnapshot');
logger.d('[AddNodes]: Update state: addedDevices = $addedDevices');
logger.d(
'[AddNodes]: Update state: onboardingProceed = $onboardingProceed, anyOnboarded=$anyOnboarded');
benchMark.end();

state = state.copyWith(
// nodesSnapshot: nodeSnapshot,
onboardingProceed: onboardingProceed,
anyOnboarded: anyOnboarded,
addedNodes: addedDevices,
childNodes: collectChildNodeData(childNodes, backhaulInfoList),
childNodes: childNodesWithBackhaul.isNotEmpty
? childNodesWithBackhaul
: _service.collectChildNodeData(childNodes, []),
isLoading: false,
onboardedMACList: onboardedMACList,
);
}

Stream<List<LinksysDevice>> pollForNodesOnline(List<String> onboardedMACList,
{bool refreshing = false}) {
logger
.d('[AddNodes]: [pollForNodesOnline] Start by MACs: $onboardedMACList');
final repo = ref.read(routerRepositoryProvider);
return repo
.scheduledCommand(
firstDelayInMilliSec: refreshing ? 1000 : 20000,
retryDelayInMilliSec: refreshing ? 3000 : 20000,
// Basic 3 minutes, add 2 minutes for each one more node
maxRetry: refreshing ? 5 : 9 + onboardedMACList.length * 6,
auth: true,
action: JNAPAction.getDevices,
condition: (result) {
if (result is JNAPSuccess) {
final deviceList = List.from(
result.output['devices'],
)
.map((e) => LinksysDevice.fromMap(e))
.where((device) => device.isAuthority == false)
.toList();
// check all mac address in the list can be found on the device list
bool allFound = onboardedMACList.every((mac) => deviceList.any(
(device) =>
device.nodeType == 'Slave' &&
(device.knownInterfaces?.any((knownInterface) =>
knownInterface.macAddress == mac) ??
false)));
logger.d(
'[AddNodes]: [pollForNodesOnline] are All MACs in device list? $allFound');
// see any additional nodes || nodes in the mac list all has connections.
bool ret = deviceList
.where((device) =>
device.nodeType == 'Slave' &&
(device.knownInterfaces?.any((knownInterface) =>
onboardedMACList
.contains(knownInterface.macAddress)) ??
false))
.every((device) {
final hasConnections = device.isOnline();
logger.d(
'[AddNodes]: [pollForNodesOnline] <${device.getDeviceLocation()}> has connections: $hasConnections');
return hasConnections;
});
return allFound && ret;
}
return false;
},
onCompleted: (_) {
logger.d('[AddNodes]: [pollForNodesOnline] Done!');
})
.transform(
StreamTransformer<JNAPResult, List<LinksysDevice>>.fromHandlers(
handleData: (result, sink) {
if (result is JNAPSuccess) {
final deviceList = List.from(
result.output['devices'],
)
.map((e) => LinksysDevice.fromMap(e))
.where((device) => device.nodeType != null)
.toList();
sink.add(deviceList);
}
},
),
);
}

Stream<List<BackHaulInfoData>> pollNodesBackhaulInfo(
List<LinksysDevice> nodes,
{bool refreshing = false}) {
final childNodes =
nodes.where((e) => e.nodeType == 'Slave' && e.isOnline()).toList();
logger.d(
'[AddNodes]: [pollNodesBackhaulInfo] check child nodes backhaul info data: $childNodes');
final repo = ref.read(routerRepositoryProvider);
return repo
.scheduledCommand(
firstDelayInMilliSec: refreshing ? 1000 : 3000,
retryDelayInMilliSec: refreshing ? 3000 : 3000,
maxRetry: refreshing ? 1 : 20,
auth: true,
action: JNAPAction.getBackhaulInfo,
condition: (result) {
if (result is JNAPSuccess) {
final backhaulList = List.from(
result.output['backhaulDevices'] ?? [],
).map((e) => BackHaulInfoData.fromMap(e)).toList();
// check all mac address in the list can be found on the device list
bool allFound = backhaulList.isNotEmpty &&
childNodes.every((n) => backhaulList
.any((backhaul) => backhaul.deviceUUID == n.deviceID));
logger.d(
'[AddNodes]: [pollNodesBackhaulInfo] are All child deviceUUID in backhaul info data? $allFound');

return allFound;
}
return false;
},
onCompleted: (_) {
logger.d('[AddNodes]: [pollNodesBackhaulInfo] Done!');
})
.transform(
StreamTransformer<JNAPResult, List<BackHaulInfoData>>.fromHandlers(
handleData: (result, sink) {
if (result is JNAPSuccess) {
final backhaulList = List.from(
result.output['backhaulDevices'] ?? [],
).map((e) => BackHaulInfoData.fromMap(e)).toList();
sink.add(backhaulList);
}
},
),
);
}

Future startRefresh() async {
state = state.copyWith(isLoading: true, loadingMessage: 'searching');

List<LinksysDevice> childNodes = [];
List<BackHaulInfoData> backhaulInfoList = [];
List<LinksysDevice> childNodesWithBackhaul = [];

await for (final result
in pollForNodesOnline(state.onboardedMACList ?? [], refreshing: true)) {
await for (final result in _service
.pollForNodesOnline(state.onboardedMACList ?? [], refreshing: true)) {
childNodes = result.where((element) => element.nodeType != null).toList();
}
await for (final result
in pollNodesBackhaulInfo(childNodes, refreshing: true)) {
backhaulInfoList = result;
in _service.pollNodesBackhaulInfo(childNodes, refreshing: true)) {
childNodesWithBackhaul =
_service.collectChildNodeData(childNodes, result);
}
state = state.copyWith(
childNodes: collectChildNodeData(childNodes, backhaulInfoList),
childNodes: childNodesWithBackhaul.isNotEmpty
? childNodesWithBackhaul
: _service.collectChildNodeData(childNodes, []),
isLoading: false,
loadingMessage: '',
);
}

List<LinksysDevice> collectChildNodeData(
List<LinksysDevice> childNodes, List<BackHaulInfoData> backhaulInfoList) {
childNodes.sort((a, b) => a.isAuthority ? -1 : 1);
final newChildNodes = childNodes.map((e) {
final target =
backhaulInfoList.firstWhereOrNull((d) => d.deviceUUID == e.deviceID);
if (target != null) {
return e.copyWith(
wirelessConnectionInfo: target.wirelessConnectionInfo,
connectionType: target.connectionType,
);
} else {
return e;
}
}).toList();
return newChildNodes;
}
}
Loading