initial commit
Some checks failed
xiao_pet_tracker / semantic-pull-request (push) Has been cancelled
xiao_pet_tracker / build (push) Has been cancelled
xiao_pet_tracker / spell-check (push) Has been cancelled

This commit is contained in:
2025-02-23 20:50:34 +01:00
commit 2fe59fee0d
360 changed files with 14384 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
const uuidLSM6DS3TRService = '4c534d36-4453-3354-5253-657276696365';
const uuidAccelerationData = '61636365-6c65-7261-7469-6f6e44617461';

View File

@@ -0,0 +1,280 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
import 'package:uuid/uuid.dart';
import 'package:xiao_pet_tracker/bootstrap.dart';
import 'package:xiao_pet_tracker/objectbox.dart';
import 'package:xiao_pet_tracker/objectbox.g.dart';
import 'package:xiao_pet_tracker/xiao_connector/models/capture_box.dart';
import 'package:xiao_pet_tracker/xiao_connector/models/capture_point.dart';
import 'package:xiao_pet_tracker/xiao_connector/utils/utils.dart';
part 'xiao_connector_state.dart';
class XiaoConnectorCubit extends Cubit<XiaoConnectorState> {
XiaoConnectorCubit() : super(const XiaoConnectorState());
late BluetoothDevice device;
List<BluetoothService> _services = [];
late BluetoothCharacteristic _senseService;
late StreamSubscription<List<int>> dataCapturingSubscription;
final ObjectBox _objectBox = getIt<ObjectBox>();
late final Box<CapturePoint> _capturePointsBox;
late final Box<CaptureBox> _captureBoxes;
bool gotRotation = false;
bool gotAcceleration = false;
bool gotTimeStamp = false;
int _rotX = 0;
int _rotY = 0;
int _rotZ = 0;
int _accX = 0;
int _accY = 0;
int _accZ = 0;
int _millisecondsSinceEpochSend = 0;
String _captureType = '';
String _uuid = '';
bool isRecording = false;
Future<void> init() async {
_capturePointsBox = _objectBox.store.box<CapturePoint>();
_captureBoxes = _objectBox.store.box<CaptureBox>();
}
Future<void> connect() async {
emit(state.copyWith(status: XiaoConnectorStatus.loading));
try {
// scan for devices for 15 seconds
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
} catch (e) {
emit(state.copyWith(status: XiaoConnectorStatus.failure));
throw Exception('Start Scan Error: $e');
}
FlutterBluePlus.scanResults.listen(
(results) async {
for (final r in results) {
if (kDebugMode) {
print(r);
}
// set the xiao sense name
if (r.device.advName == 'Go Bluetooth 2') {
await FlutterBluePlus.stopScan();
device = r.device;
await device.connect();
try {
_services = await device.discoverServices();
} catch (e) {
emit(state.copyWith(status: XiaoConnectorStatus.failure));
throw Exception('Discover Services Error: $e');
}
device.connectionState
.listen((BluetoothConnectionState blueState) async {
if (blueState == BluetoothConnectionState.disconnected) {
debugPrint('DISCONNECTED!');
emit(state.copyWith(status: XiaoConnectorStatus.initial));
}
});
// device.cancelWhenDisconnected(deviceConnectionStream);
emit(state.copyWith(status: XiaoConnectorStatus.connected));
}
}
},
onDone: () {
debugPrint('DONE');
emit(state.copyWith(status: XiaoConnectorStatus.initial));
},
);
}
Future<void> setCapturingOn() async {
final senseService = _services
.where(
(s) => s.serviceUuid == Guid('4c534d36-4453-3354-5253-657276696365'),
)
.first
.characteristics
.where(
(c) =>
c.characteristicUuid ==
Guid('63617074-7572-696E-6753-657276696365'),
)
.first;
await senseService.write([1]);
}
Uint8List int64BigEndianBytes(int value) =>
Uint8List(8)..buffer.asByteData().setInt64(0, value);
Future<void> setTime(int millisSinceEpoch) async {
final senseService = _services
.where(
(s) => s.serviceUuid == Guid('4c534d36-4453-3354-5253-657276696365'),
)
.first
.characteristics
.where(
(c) =>
c.characteristicUuid ==
Guid('756E6978-5469-6D65-5374-616D70527374'),
)
.first;
final List<int> bytes = int64BigEndianBytes(millisSinceEpoch);
await senseService.write(bytes);
}
Future<void> startCapturing() async {
_senseService = _services
.where(
(s) => s.serviceUuid == Guid('4c534d36-4453-3354-5253-657276696365'),
)
.first
.characteristics
.where(
(c) =>
c.characteristicUuid ==
Guid('61636365-6c65-7261-7469-6f6e44617461'),
)
.first;
dataCapturingSubscription = _senseService.onValueReceived.listen((value) {
// rotation
if (value.last == 1) {
_rotX = fromBytesToInt32(value[0], value[1], value[2], value[3]);
_rotY = fromBytesToInt32(value[4], value[5], value[6], value[7]);
_rotZ = fromBytesToInt32(value[8], value[9], value[10], value[11]);
gotRotation = true;
}
// acceleration
if (value.last == 2) {
_accX = fromBytesToInt32(value[0], value[1], value[2], value[3]);
_accY = fromBytesToInt32(value[4], value[5], value[6], value[7]);
_accZ = fromBytesToInt32(value[8], value[9], value[10], value[11]);
gotAcceleration = true;
}
if (value.last == 3) {
final timeStamp = fromBytesToInt64(
value[0],
value[1],
value[2],
value[3],
value[4],
value[5],
value[6],
value[7],
);
_millisecondsSinceEpochSend = timeStamp;
gotTimeStamp = true;
}
if (gotAcceleration && gotRotation && gotTimeStamp) {
final capturePoint = CapturePoint(
type: _captureType,
uuid: _uuid,
rotationX: _rotX,
rotationY: _rotY,
rotationZ: _rotZ,
accelerationX: _accX,
accelerationY: _accY,
accelerationZ: _accZ,
millisecondsSinceEpochReceived:
DateTime.now().toUtc().millisecondsSinceEpoch,
millisecondsSinceEpochSend: _millisecondsSinceEpochSend,
);
emit(state.copyWith(lastCapturedPoint: capturePoint));
if (isRecording) {
_writeToObjectBox(capturePoint: capturePoint);
}
gotAcceleration = false;
gotRotation = false;
gotTimeStamp = false;
}
});
await setCapturingOn();
await setTime(DateTime.now().millisecondsSinceEpoch);
device.cancelWhenDisconnected(dataCapturingSubscription);
await _senseService.setNotifyValue(true);
emit(state.copyWith(status: XiaoConnectorStatus.capturing));
}
Future<void> stopCapturing() async {
await dataCapturingSubscription.cancel();
await _senseService.setNotifyValue(false);
emit(state.copyWith(status: XiaoConnectorStatus.connected));
}
Future<void> disconnectDevice() async {
await device.disconnect();
_services = [];
emit(state.copyWith(status: XiaoConnectorStatus.initial));
}
void toggleRecording({required String captureType}) {
isRecording = !isRecording;
if (isRecording) {
_captureType = captureType;
_uuid = const Uuid().v4();
_captureBoxes.put(
CaptureBox(
type: _captureType,
uuid: _uuid,
startTime: DateTime.now(),
),
);
}
}
void _writeToObjectBox({
required CapturePoint capturePoint,
}) {
_capturePointsBox.put(capturePoint);
}
List<CapturePoint> getCapturePointsOfUuid(String uuid) {
final query =
_capturePointsBox.query(CapturePoint_.uuid.equals(uuid)).build();
final points = query.find();
return points;
}
void deleteRecording(String uuid) {
_capturePointsBox.query(CapturePoint_.uuid.equals(uuid)).build().remove();
_captureBoxes.query(CaptureBox_.uuid.equals(uuid)).build().remove();
}
List<CaptureBox> getAllCaptureBoxes() {
final query = (_captureBoxes.query()
..order(CaptureBox_.startTime, flags: Order.descending))
.build();
final boxes = query.find();
return boxes;
}
List<CapturePoint> getAllCapturePoints() => _capturePointsBox.getAll();
}

View File

@@ -0,0 +1,40 @@
part of 'xiao_connector_cubit.dart';
enum XiaoConnectorStatus {
initial,
loading,
connected,
capturing,
failure,
}
extension XiaoConnectorStatusX on XiaoConnectorStatus {
bool get isInitial => this == XiaoConnectorStatus.initial;
bool get isLoading => this == XiaoConnectorStatus.loading;
bool get isConnected => this == XiaoConnectorStatus.connected;
bool get isCapturing => this == XiaoConnectorStatus.capturing;
bool get isFailure => this == XiaoConnectorStatus.failure;
}
final class XiaoConnectorState extends Equatable {
const XiaoConnectorState({
this.status = XiaoConnectorStatus.initial,
this.lastCapturedPoint,
});
final XiaoConnectorStatus status;
final CapturePoint? lastCapturedPoint;
XiaoConnectorState copyWith({
XiaoConnectorStatus? status,
CapturePoint? lastCapturedPoint,
}) {
return XiaoConnectorState(
status: status ?? this.status,
lastCapturedPoint: lastCapturedPoint ?? this.lastCapturedPoint,
);
}
@override
List<Object?> get props => [status, lastCapturedPoint];
}

View File

@@ -0,0 +1,18 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class CaptureBox {
CaptureBox({
required this.type,
required this.uuid,
required this.startTime,
});
@Id()
int id = 0;
String type;
String uuid;
@Property(type: PropertyType.date)
DateTime startTime;
}

View File

@@ -0,0 +1,32 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class CapturePoint {
CapturePoint({
required this.type,
required this.uuid,
required this.rotationX,
required this.rotationY,
required this.rotationZ,
required this.accelerationX,
required this.accelerationY,
required this.accelerationZ,
required this.millisecondsSinceEpochReceived,
required this.millisecondsSinceEpochSend,
});
@Id()
int id = 0;
String type;
String uuid;
int rotationX;
int rotationY;
int rotationZ;
int accelerationX;
int accelerationY;
int accelerationZ;
int millisecondsSinceEpochReceived;
int millisecondsSinceEpochSend;
}

View File

@@ -0,0 +1,32 @@
import 'dart:typed_data';
int fromBytesToInt32(int b3, int b2, int b1, int b0) {
final int8List = Int8List(4)
..[3] = b3
..[2] = b2
..[1] = b1
..[0] = b0;
return int8List.buffer.asByteData().getInt32(0);
}
int fromBytesToInt64(
int b7,
int b6,
int b5,
int b4,
int b3,
int b2,
int b1,
int b0,
) {
final int8List = Int8List(8)
..[7] = b7
..[6] = b6
..[5] = b5
..[4] = b4
..[3] = b3
..[2] = b2
..[1] = b1
..[0] = b0;
return int8List.buffer.asByteData().getInt64(0);
}

View File

@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
class CaptureView extends StatefulWidget {
const CaptureView({super.key});
@override
State<CaptureView> createState() => _CaptureViewState();
}
class _CaptureViewState extends State<CaptureView> {
late TextEditingController _controller;
bool _isTextFieldFocused = false;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// final count = context.select((XiaoConnectorCubit cubit) => cubit.state);
final lastCapturePoint = context
.select((XiaoConnectorCubit cubit) => cubit.state.lastCapturedPoint);
return Scaffold(
appBar: AppBar(
title: const Text('Live Feed'),
),
body: SingleChildScrollView(
child: Center(
child: Column(
children: [
const SizedBox(
height: 8,
),
ElevatedButton(
onPressed: () {
context.read<XiaoConnectorCubit>().stopCapturing();
},
child: const Text('Close Live Feed'),
),
const SizedBox(
height: 12,
),
const Text(
'Last Captured Point',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Divider(
indent: 80,
endIndent: 80,
),
const Text(
'Received Time',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'${DateTime.fromMillisecondsSinceEpoch(
lastCapturePoint?.millisecondsSinceEpochReceived ?? 0,
)}',
),
const Text(
'Send Time',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Text('(time the controller send the data)'),
Text(
'${DateTime.fromMillisecondsSinceEpoch(
lastCapturePoint?.millisecondsSinceEpochSend ?? 0,
)}',
),
const SizedBox(
height: 12,
),
ElevatedButton.icon(
onPressed: () {
// context.read<XiaoConnectorCubit>().startCapturing(
// captureType: 'test',
// );
final timeToSend = DateTime.now().millisecondsSinceEpoch;
context.read<XiaoConnectorCubit>().setTime(timeToSend);
},
icon: const Icon(Icons.watch_later_outlined),
label: const Text('Sync Clocks'),
),
const SizedBox(
height: 12,
),
const Divider(
indent: 80,
endIndent: 80,
),
Text('Acceleration X: ${lastCapturePoint?.accelerationX}'),
Text('Acceleration Y: ${lastCapturePoint?.accelerationY}'),
Text('Acceleration Z: ${lastCapturePoint?.accelerationZ}'),
const Divider(
indent: 80,
endIndent: 80,
),
Text('Rotation X: ${lastCapturePoint?.rotationX}'),
Text('Rotation Y: ${lastCapturePoint?.rotationY}'),
Text('Rotation Z: ${lastCapturePoint?.rotationZ}'),
const SizedBox(
height: 16,
),
Padding(
padding: const EdgeInsets.only(left: 32, right: 32, top: 16),
child: FocusScope(
child: Focus(
onFocusChange: (focus) {
_isTextFieldFocused = focus;
setState(() {});
},
child: TextField(
controller: _controller,
decoration: const InputDecoration(
label: Text('Capture Name'),
),
),
),
),
),
const SizedBox(
height: 16,
)
],
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _isTextFieldFocused
? null
: FloatingActionButton.large(
child: context.read<XiaoConnectorCubit>().isRecording
? const Stack(
children: [
Align(
child: Icon(Icons.pause),
),
Align(
child: SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(12),
child: CircularProgressIndicator(),
),
),
),
],
)
: const Icon(Icons.play_arrow),
onPressed: () {
if (_controller.value.text == '') {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
'Please enter Capture Name',
),
content: const Text(
'You need to enter a capture name before you can start capturing data.',
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
'Ok',
),
),
],
);
},
);
} else {
context.read<XiaoConnectorCubit>().toggleRecording(
captureType: _controller.value.text,
);
}
},
),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
class ConnectedView extends StatelessWidget {
const ConnectedView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Xiao Connector'),
),
body: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'''
You are connected to the Xiao Sense.
Now you can open the live feed.''',
textAlign: TextAlign.center,
),
const SizedBox(
height: 48,
),
ElevatedButton(
onPressed: () {
context.read<XiaoConnectorCubit>().startCapturing();
},
child: const Text('Open Live Feed'),
),
const SizedBox(
height: 48,
),
ElevatedButton(
onPressed: () {
context.read<XiaoConnectorCubit>().disconnectDevice();
},
child: const Text('Disconnect'),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class FailureView extends StatelessWidget {
const FailureView({super.key});
@override
Widget build(BuildContext context) {
return const Text('Oops. Something bad happened! :(');
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
class InitialView extends StatelessWidget {
const InitialView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Xiao Connector'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'''
You are currently not connected to the Xiao Sense.
Click on `Connect` to try to connect to the device.''',
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
ElevatedButton(
onPressed: () {
context.read<XiaoConnectorCubit>().connect();
},
child: const Text('Connect'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class LoadingView extends StatelessWidget {
const LoadingView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}

View File

@@ -0,0 +1 @@
export 'xiao_connector_page.dart';

View File

@@ -0,0 +1,40 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/capture_view.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/connected_view.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/failure_view.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/initial_view.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/loading_view.dart';
@RoutePage()
class XiaoConnectorPage extends StatelessWidget {
const XiaoConnectorPage({super.key});
@override
Widget build(BuildContext context) {
return const XiaoConnectorView();
}
}
class XiaoConnectorView extends StatelessWidget {
const XiaoConnectorView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<XiaoConnectorCubit, XiaoConnectorState>(
builder: (context, state) {
return switch (state.status) {
XiaoConnectorStatus.initial => const InitialView(),
XiaoConnectorStatus.loading => const LoadingView(),
XiaoConnectorStatus.connected => const ConnectedView(),
XiaoConnectorStatus.capturing => const CaptureView(),
XiaoConnectorStatus.failure => const FailureView(),
};
},
),
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@RoutePage()
class XiaoDataPage extends StatelessWidget {
const XiaoDataPage({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
class XiaoDataView extends StatelessWidget {
const XiaoDataView({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -0,0 +1 @@
export 'view/view.dart';