153 lines
6.4 KiB
Dart
153 lines
6.4 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:image/image.dart' as img;
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
|
import 'package:usb_serial/usb_serial.dart';
|
|
|
|
import 'location_service.dart';
|
|
import 'marine_api_service.dart';
|
|
import '../models/in_situ_sampling_data.dart';
|
|
import '../bluetooth/bluetooth_manager.dart';
|
|
import '../serial/serial_manager.dart';
|
|
|
|
/// A dedicated service to handle all business logic for the In-Situ Sampling feature.
|
|
/// This includes location, image processing, device communication, and data submission.
|
|
class InSituSamplingService {
|
|
final LocationService _locationService = LocationService();
|
|
final MarineApiService _marineApiService = MarineApiService();
|
|
final BluetoothManager _bluetoothManager = BluetoothManager();
|
|
final SerialManager _serialManager = SerialManager();
|
|
|
|
// This channel name MUST match the one defined in MainActivity.kt
|
|
static const platform = MethodChannel('com.example.environment_monitoring_app/usb');
|
|
|
|
|
|
// --- Location Services ---
|
|
Future<Position> getCurrentLocation() => _locationService.getCurrentLocation();
|
|
double calculateDistance(double lat1, double lon1, double lat2, double lon2) => _locationService.calculateDistance(lat1, lon1, lat2, lon2);
|
|
|
|
// --- Image Processing ---
|
|
Future<File?> pickAndProcessImage(ImageSource source, {
|
|
required InSituSamplingData data,
|
|
required String imageInfo,
|
|
bool isRequired = false,
|
|
}) async {
|
|
final picker = ImagePicker();
|
|
final XFile? photo = await picker.pickImage(source: source, imageQuality: 85, maxWidth: 1024);
|
|
if (photo == null) return null;
|
|
|
|
final bytes = await photo.readAsBytes();
|
|
img.Image? originalImage = img.decodeImage(bytes);
|
|
if (originalImage == null) return null;
|
|
|
|
if (isRequired && originalImage.height > originalImage.width) {
|
|
debugPrint("Image rejected: Must be in landscape orientation.");
|
|
return null;
|
|
}
|
|
|
|
final String watermarkTimestamp = "${data.samplingDate} ${data.samplingTime}";
|
|
final font = img.arial24;
|
|
final textWidth = watermarkTimestamp.length * 12;
|
|
img.fillRect(originalImage, x1: 5, y1: 5, x2: textWidth + 15, y2: 35, color: img.ColorRgb8(255, 255, 255));
|
|
img.drawString(originalImage, watermarkTimestamp, font: font, x: 10, y: 10, color: img.ColorRgb8(0, 0, 0));
|
|
|
|
final tempDir = await getTemporaryDirectory();
|
|
final stationCode = data.selectedStation?['man_station_code'] ?? 'NA';
|
|
final fileTimestamp = "${data.samplingDate}-${data.samplingTime}".replaceAll(':', '-');
|
|
final newFileName = "${stationCode}_${fileTimestamp}_${imageInfo.replaceAll(' ', '')}.jpg";
|
|
final filePath = path.join(tempDir.path, newFileName);
|
|
|
|
return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage));
|
|
}
|
|
|
|
// --- Device Connection (Delegated to Managers) ---
|
|
ValueNotifier<BluetoothConnectionState> get bluetoothConnectionState => _bluetoothManager.connectionState;
|
|
ValueNotifier<SerialConnectionState> get serialConnectionState => _serialManager.connectionState;
|
|
|
|
// REPAIRED: This getter now dynamically returns the correct Sonde ID notifier
|
|
// based on the active connection, which is essential for the UI.
|
|
ValueNotifier<String?> get sondeId {
|
|
if (_bluetoothManager.connectionState.value != BluetoothConnectionState.disconnected) {
|
|
return _bluetoothManager.sondeId;
|
|
}
|
|
return _serialManager.sondeId;
|
|
}
|
|
|
|
Stream<Map<String, double>> get bluetoothDataStream => _bluetoothManager.dataStream;
|
|
Stream<Map<String, double>> get serialDataStream => _serialManager.dataStream;
|
|
|
|
// REPAIRED: Added .value to both getters for consistency and to prevent errors.
|
|
String? get connectedBluetoothDeviceName => _bluetoothManager.connectedDeviceName.value;
|
|
String? get connectedSerialDeviceName => _serialManager.connectedDeviceName.value;
|
|
|
|
// --- Permissions ---
|
|
Future<bool> requestDevicePermissions() async {
|
|
Map<Permission, PermissionStatus> statuses = await [
|
|
Permission.bluetoothScan,
|
|
Permission.bluetoothConnect,
|
|
Permission.locationWhenInUse,
|
|
].request();
|
|
|
|
// Return true only if the essential permissions are granted.
|
|
if (statuses[Permission.bluetoothScan] == PermissionStatus.granted &&
|
|
statuses[Permission.bluetoothConnect] == PermissionStatus.granted) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// --- Bluetooth Methods ---
|
|
Future<List<BluetoothDevice>> getPairedBluetoothDevices() => _bluetoothManager.getPairedDevices();
|
|
Future<void> connectToBluetoothDevice(BluetoothDevice device) => _bluetoothManager.connect(device);
|
|
void disconnectFromBluetooth() => _bluetoothManager.disconnect();
|
|
void startBluetoothAutoReading({Duration? interval}) => _bluetoothManager.startAutoReading(interval: interval ?? const Duration(seconds: 5));
|
|
void stopBluetoothAutoReading() => _bluetoothManager.stopAutoReading();
|
|
|
|
|
|
// --- USB Serial Methods ---
|
|
Future<List<UsbDevice>> getAvailableSerialDevices() => _serialManager.getAvailableDevices();
|
|
|
|
Future<bool> requestUsbPermission(UsbDevice device) async {
|
|
try {
|
|
return await platform.invokeMethod('requestUsbPermission', {'vid': device.vid, 'pid': device.pid}) ?? false;
|
|
} on PlatformException catch (e) {
|
|
debugPrint("Failed to request USB permission: '${e.message}'.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> connectToSerialDevice(UsbDevice device) async {
|
|
final bool permissionGranted = await requestUsbPermission(device);
|
|
if (permissionGranted) {
|
|
await _serialManager.connect(device);
|
|
} else {
|
|
throw Exception("USB permission was not granted.");
|
|
}
|
|
}
|
|
|
|
void disconnectFromSerial() => _serialManager.disconnect();
|
|
void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 5));
|
|
void stopSerialAutoReading() => _serialManager.stopAutoReading();
|
|
|
|
|
|
void dispose() {
|
|
_bluetoothManager.dispose();
|
|
_serialManager.dispose();
|
|
}
|
|
|
|
// --- Data Submission ---
|
|
Future<Map<String, dynamic>> submitData(InSituSamplingData data) {
|
|
return _marineApiService.submitInSituSample(
|
|
formData: data.toApiFormData(),
|
|
imageFiles: data.toApiImageFiles(),
|
|
);
|
|
}
|
|
} |