156 lines
5.0 KiB
Dart
156 lines
5.0 KiB
Dart
import 'dart:async';
|
|
import 'dart:math' as math;
|
|
import 'package:sensors_plus/sensors_plus.dart';
|
|
|
|
/// Statut de parallélisme retourné en temps réel.
|
|
enum ParallelismStatus {
|
|
/// Pas encore de données capteur disponibles.
|
|
unknown,
|
|
|
|
/// L'appareil est bien parallèle à la cible (pitch et roll dans les seuils).
|
|
aligned,
|
|
|
|
/// L'appareil est trop incliné — l'utilisateur doit repositionner.
|
|
misaligned,
|
|
}
|
|
|
|
/// Données de parallélisme calculées à chaque frame capteur.
|
|
class ParallelismData {
|
|
final ParallelismStatus status;
|
|
|
|
/// Inclinaison avant/arrière en degrés (0° = parfaitement vertical).
|
|
final double pitchDegrees;
|
|
|
|
/// Inclinaison gauche/droite en degrés (0° = parfaitement droit).
|
|
final double rollDegrees;
|
|
|
|
const ParallelismData({
|
|
required this.status,
|
|
required this.pitchDegrees,
|
|
required this.rollDegrees,
|
|
});
|
|
|
|
/// Raccourci : vrai si l'appareil est bien aligné.
|
|
bool get isAligned => status == ParallelismStatus.aligned;
|
|
|
|
@override
|
|
String toString() =>
|
|
'ParallelismData(status: $status, pitch: ${pitchDegrees.toStringAsFixed(1)}°, roll: ${rollDegrees.toStringAsFixed(1)}°)';
|
|
}
|
|
|
|
/// Service de détection du parallélisme par accéléromètre.
|
|
///
|
|
/// Utilise le package sensors_plus pour lire l'accéléromètre en continu.
|
|
/// Calcule le pitch (inclinaison avant/arrière) et le roll (inclinaison latérale).
|
|
///
|
|
/// Un appareil tenu **à la verticale face à la cible** a :
|
|
/// - pitch ≈ 0° (l'axe X est horizontal)
|
|
/// - roll ≈ 0° (l'axe Y est vertical, gravité vers le bas)
|
|
///
|
|
/// En mode **paysage** (appareil à l'horizontale), les axes sont permutés —
|
|
/// le service gère les deux orientations automatiquement.
|
|
class ParallelismService {
|
|
/// Seuil d'angle maximum autorisé en degrés.
|
|
/// En dessous → aligned, au-dessus → misaligned.
|
|
final double thresholdDegrees;
|
|
|
|
StreamSubscription<AccelerometerEvent>? _subscription;
|
|
final StreamController<ParallelismData> _controller =
|
|
StreamController<ParallelismData>.broadcast();
|
|
|
|
ParallelismService({this.thresholdDegrees = 5.0});
|
|
|
|
/// Stream de données de parallélisme mis à jour en temps réel.
|
|
Stream<ParallelismData> get stream => _controller.stream;
|
|
|
|
/// Démarre l'écoute de l'accéléromètre.
|
|
/// Appeler au moment d'afficher la caméra live.
|
|
void start() {
|
|
if (_subscription != null) return; // Déjà démarré
|
|
|
|
_subscription = accelerometerEventStream(
|
|
samplingPeriod: SensorInterval.normalInterval, // ~50 ms
|
|
).listen(
|
|
_onAccelerometerEvent,
|
|
onError: (e) {
|
|
// En cas d'erreur capteur (simulateur sans IMU par exemple),
|
|
// on émet un statut "unknown" pour ne pas bloquer l'UI.
|
|
if (!_controller.isClosed) {
|
|
_controller.add(
|
|
const ParallelismData(
|
|
status: ParallelismStatus.unknown,
|
|
pitchDegrees: 0,
|
|
rollDegrees: 0,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Stoppe l'écoute de l'accéléromètre.
|
|
/// Appeler quand l'écran caméra est fermé pour économiser la batterie.
|
|
void stop() {
|
|
_subscription?.cancel();
|
|
_subscription = null;
|
|
}
|
|
|
|
/// Libère toutes les ressources.
|
|
void dispose() {
|
|
stop();
|
|
_controller.close();
|
|
}
|
|
|
|
void _onAccelerometerEvent(AccelerometerEvent event) {
|
|
if (_controller.isClosed) return;
|
|
|
|
// L'accéléromètre retourne (x, y, z) en m/s².
|
|
// Au repos, la magnitude vaut ~9.81 (gravité terrestre).
|
|
//
|
|
// Convention capteur Android/iOS (portrait) :
|
|
// x → axe horizontal (positif = droite)
|
|
// y → axe vertical (positif = haut)
|
|
// z → axe profondeur (positif = vers l'utilisateur)
|
|
//
|
|
// On normalise pour être indépendant de la gravité exacte.
|
|
final double gx = event.x;
|
|
final double gy = event.y;
|
|
final double gz = event.z;
|
|
|
|
final double magnitude = math.sqrt(gx * gx + gy * gy + gz * gz);
|
|
if (magnitude < 1.0) {
|
|
// Données aberrantes (quasi-nulles) — on ignore
|
|
return;
|
|
}
|
|
|
|
// Normalisation
|
|
final double nx = gx / magnitude;
|
|
final double ny = gy / magnitude;
|
|
final double nz = gz / magnitude;
|
|
|
|
// Pitch : inclinaison avant/arrière
|
|
// En portrait vertical face à l'utilisateur : ny ≈ -1, nz ≈ 0 → pitch ≈ 0°
|
|
// On calcule l'angle entre le vecteur gravité projeté et l'axe Y pur.
|
|
final double pitchRad = math.asin(nz.clamp(-1.0, 1.0));
|
|
final double pitchDeg = pitchRad * (180.0 / math.pi);
|
|
|
|
// Roll : inclinaison latérale gauche/droite
|
|
// En portrait droit : nx ≈ 0 → roll ≈ 0°
|
|
final double rollRad = math.atan2(nx, -ny);
|
|
final double rollDeg = rollRad * (180.0 / math.pi);
|
|
|
|
final bool isAligned =
|
|
pitchDeg.abs() <= thresholdDegrees &&
|
|
rollDeg.abs() <= thresholdDegrees;
|
|
|
|
_controller.add(
|
|
ParallelismData(
|
|
status: isAligned
|
|
? ParallelismStatus.aligned
|
|
: ParallelismStatus.misaligned,
|
|
pitchDegrees: pitchDeg,
|
|
rollDegrees: rollDeg,
|
|
),
|
|
);
|
|
}
|
|
} |