148 lines
5.2 KiB
Dart
148 lines
5.2 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'dart:io';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
import 'package:crypto/crypto.dart';
|
|
import '../data/models/shot.dart';
|
|
import '../data/models/target_type.dart';
|
|
import 'wallet_identity_service.dart';
|
|
|
|
class AiExportService {
|
|
// Utilise 10.0.2.2 pour l'émulateur Android, sinon localhost.
|
|
// Pour un appareil physique, il faudra utiliser l'IP locale du PC (ex: 192.168.1.X).
|
|
static String get _defaultApiUrl {
|
|
if (Platform.isAndroid) {
|
|
return 'http://10.0.2.2:3000/api/upload';
|
|
}
|
|
return 'http://localhost:3000/api/upload';
|
|
}
|
|
|
|
/// Extrait les informations de l'appareil
|
|
Future<Map<String, dynamic>> _getDeviceInfo() async {
|
|
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
|
|
Map<String, dynamic> deviceData = {'model': 'Unknown', 'os': 'Unknown'};
|
|
|
|
try {
|
|
if (Platform.isAndroid) {
|
|
final androidInfo = await deviceInfoPlugin.androidInfo;
|
|
deviceData['model'] = '${androidInfo.brand} ${androidInfo.model}';
|
|
deviceData['os'] = 'Android ${androidInfo.version.release}';
|
|
} else if (Platform.isIOS) {
|
|
final iosInfo = await deviceInfoPlugin.iosInfo;
|
|
deviceData['model'] = iosInfo.name;
|
|
deviceData['os'] = '${iosInfo.systemName} ${iosInfo.systemVersion}';
|
|
} else if (Platform.isWindows) {
|
|
final windowsInfo = await deviceInfoPlugin.windowsInfo;
|
|
deviceData['model'] = 'Windows PC';
|
|
deviceData['os'] = 'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}';
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la récupération des infos appareil: $e');
|
|
}
|
|
|
|
return deviceData;
|
|
}
|
|
|
|
/// Exporte l'image et les données de plotting vers le serveur
|
|
Future<bool> exportData({
|
|
required String imagePath,
|
|
required String sessionId,
|
|
required TargetType targetType,
|
|
required double targetCenterX,
|
|
required double targetCenterY,
|
|
required double targetRadius,
|
|
required List<Shot> shots,
|
|
String? apiUrl,
|
|
}) async {
|
|
try {
|
|
final url = Uri.parse(apiUrl ?? _defaultApiUrl);
|
|
final request = http.MultipartRequest('POST', url);
|
|
|
|
// 1. Prepare image
|
|
final file = File(imagePath);
|
|
if (!await file.exists()) {
|
|
throw Exception('Le fichier image n\'existe pas');
|
|
}
|
|
|
|
// Read image metadata (approximate dimensions since decoding image can be heavy)
|
|
// On the frontend we usually have aspectRatio, here we use generic values if not available.
|
|
final deviceData = await _getDeviceInfo();
|
|
|
|
// We approximate the target corners from center and radius
|
|
// radius is relative (0 to 1). We need image width/height to get pixels.
|
|
// But we can just pass relative corners as well, or a normalized bounding box.
|
|
// Let's create normalized corners (0 to 1).
|
|
final corners = [
|
|
{"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY - targetRadius},
|
|
{"norm_x": targetCenterX + targetRadius, "norm_y": targetCenterY - targetRadius},
|
|
{"norm_x": targetCenterX + targetRadius, "norm_y": targetCenterY + targetRadius},
|
|
{"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY + targetRadius},
|
|
];
|
|
|
|
// Format the impacts
|
|
final formattedImpacts = shots.asMap().entries.map((entry) {
|
|
final index = entry.key;
|
|
final shot = entry.value;
|
|
return {
|
|
"id": index + 1,
|
|
"label": "bullet_hole",
|
|
"score": shot.score,
|
|
"coords": {
|
|
"norm_x": shot.x,
|
|
"norm_y": shot.y
|
|
}
|
|
};
|
|
}).toList();
|
|
|
|
// Get and hash the wallet identity
|
|
final walletService = WalletIdentityService();
|
|
final phrase = await walletService.getIdentityPhrase();
|
|
final phraseBytes = utf8.encode(phrase);
|
|
final walletHash = sha256.convert(phraseBytes).toString();
|
|
|
|
// Build JSON payload
|
|
final plottingJson = {
|
|
"session_id": sessionId,
|
|
"wallet_hash": walletHash,
|
|
"timestamp": DateTime.now().toIso8601String(),
|
|
"device_info": deviceData,
|
|
"target_metadata": {
|
|
"type": targetType.name,
|
|
"distance_meters": 25, // Default/placeholder
|
|
"weapon": "Unknown", // Default/placeholder
|
|
// The backend could extract exact width/height from the image.
|
|
},
|
|
"plotting": {
|
|
"target_corners": corners,
|
|
"impacts": formattedImpacts
|
|
}
|
|
};
|
|
|
|
// Add fields to request
|
|
request.fields['plotting'] = jsonEncode(plottingJson);
|
|
|
|
// Add file
|
|
request.files.add(
|
|
await http.MultipartFile.fromPath('photo', imagePath),
|
|
);
|
|
|
|
// Send request
|
|
final response = await request.send();
|
|
|
|
if (response.statusCode == 200) {
|
|
final responseData = await response.stream.bytesToString();
|
|
debugPrint('Export réussi: $responseData');
|
|
return true;
|
|
} else {
|
|
final errorData = await response.stream.bytesToString();
|
|
debugPrint('Erreur d\'export: ${response.statusCode} - $errorData');
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Exception lors de l\'export: $e');
|
|
return false;
|
|
}
|
|
}
|
|
}
|