diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3c8f67c..cc28182 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - lsa_gloves + GuantesLSA CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/main.dart b/lib/main.dart index 6fb4217..9ac1465 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ import 'package:assets_audio_player/assets_audio_player.dart'; import 'package:flutter/material.dart'; +import 'package:lsa_gloves/screens/edgeimpulse/api_client.dart'; import 'package:lsa_gloves/screens/files/file_content.dart'; import 'package:lsa_gloves/screens/files/file_list.dart'; import 'dart:developer'; -import 'package:lsa_gloves/screens/connection/connection.dart'; +import 'package:lsa_gloves/screens/connection/find_connection.dart'; void main() { @@ -143,6 +144,7 @@ class _MyHomePageState extends State { ), FloatingActionButton( onPressed: () => { + uploadFile(), Navigator.of(context).push(MaterialPageRoute( builder: (context) => FileManagerPage() )) diff --git a/lib/screens/connection/connection.dart b/lib/screens/connection/bluetooth_device.dart similarity index 58% rename from lib/screens/connection/connection.dart rename to lib/screens/connection/bluetooth_device.dart index 612e376..a19d61a 100644 --- a/lib/screens/connection/connection.dart +++ b/lib/screens/connection/bluetooth_device.dart @@ -1,146 +1,9 @@ import 'dart:convert'; -import 'dart:async'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue/flutter_blue.dart'; import 'package:lsa_gloves/screens/files/storage.dart'; -import 'package:lsa_gloves/screens/connection/widgets.dart'; - -class GloveConnectionPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: FlutterBlue.instance.state, - initialData: BluetoothState.unknown, - builder: (c, snapshot) { - final state = snapshot.data; - if (state == BluetoothState.on) { - return FindDevicesScreen(); - } - return BluetoothOffScreen(state: state); - }); - } -} - -class BluetoothOffScreen extends StatelessWidget { - const BluetoothOffScreen({Key? key, this.state}) : super(key: key); - - final BluetoothState? state; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.lightBlue, - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.bluetooth_disabled, - size: 200.0, - color: Colors.white54, - ), - Text( - 'Bluetooth Adapter is ${state != null ? state.toString().substring(15) : 'not available'}.', - style: Theme.of(context) - .primaryTextTheme - .subhead - ?.copyWith(color: Colors.white), - ), - ], - ), - ), - ); - } -} - -class FindDevicesScreen extends StatelessWidget { - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Find Devices'), - ), - body: RefreshIndicator( - onRefresh: () => - FlutterBlue.instance.startScan(timeout: Duration(seconds: 4)), - child: SingleChildScrollView( - child: Column( - children: [ - StreamBuilder>( - stream: Stream.periodic(Duration(seconds: 2)) - .asyncMap((_) => FlutterBlue.instance.connectedDevices), - initialData: [], - builder: (c, snapshot) => Column( - children: snapshot.data! - .map((d) => ListTile( - title: Text(d.name), - subtitle: Text(d.id.toString()), - trailing: StreamBuilder( - stream: d.state, - initialData: BluetoothDeviceState.disconnected, - builder: (c, snapshot) { - if (snapshot.data == - BluetoothDeviceState.connected) { - return RaisedButton( - child: Text('OPEN'), - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - DeviceScreen(device: d))), - ); - } - return Text(snapshot.data.toString()); - }, - ), - )) - .toList(), - ), - ), - StreamBuilder>( - stream: FlutterBlue.instance.scanResults, - initialData: [], - builder: (c, snapshot) => Column( - children: snapshot.data! - .map( - (r) => ScanResultTile( - result: r, - onTap: () => Navigator.of(context) - .push(MaterialPageRoute(builder: (context) { - r.device.connect(); - return DeviceScreen(device: r.device); - })), - ), - ) - .toList(), - ), - ), - ], - ), - ), - ), - floatingActionButton: StreamBuilder( - stream: FlutterBlue.instance.isScanning, - initialData: false, - builder: (c, snapshot) { - if (snapshot.data!) { - return FloatingActionButton( - child: Icon(Icons.stop), - onPressed: () => FlutterBlue.instance.stopScan(), - backgroundColor: Colors.red, - ); - } else { - return FloatingActionButton( - child: Icon(Icons.search), - onPressed: () => FlutterBlue.instance.startScan(timeout: Duration(seconds: 4))); - } - }, - ), - ); - } -} +import 'package:lsa_gloves/screens/connection/bluetooth_widgets.dart'; class DeviceScreen extends StatefulWidget { const DeviceScreen({Key? key, required this.device}) : super(key: key); @@ -167,7 +30,7 @@ class _DeviceScreenState extends State { _readGloveMovements(BluetoothCharacteristic characteristic) async { var d= DateTime.now(); - var f = await new GloveEventsStorage().createFile("$d.txt"); + var f = await new GloveEventsStorage().createFile("$d"); var mf = new MeasurementsFile(f, d); while(true){ diff --git a/lib/screens/connection/widgets.dart b/lib/screens/connection/bluetooth_widgets.dart similarity index 63% rename from lib/screens/connection/widgets.dart rename to lib/screens/connection/bluetooth_widgets.dart index d2fc0d2..e745e72 100644 --- a/lib/screens/connection/widgets.dart +++ b/lib/screens/connection/bluetooth_widgets.dart @@ -1,117 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_blue/flutter_blue.dart'; -class ScanResultTile extends StatelessWidget { - const ScanResultTile({Key? key, required this.result, this.onTap}) - : super(key: key); - - final ScanResult result; - final VoidCallback? onTap; - - Widget _buildTitle(BuildContext context) { - if (result.device.name.length > 0) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - result.device.name, - overflow: TextOverflow.ellipsis, - ), - Text( - result.device.id.toString(), - style: Theme.of(context).textTheme.caption, - ) - ], - ); - } else { - return Text(result.device.id.toString()); - } - } - - Widget _buildAdvRow(BuildContext context, String title, String value) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: Theme.of(context).textTheme.caption), - SizedBox( - width: 12.0, - ), - Expanded( - child: Text( - value, - style: Theme.of(context) - .textTheme - .caption - ?.apply(color: Colors.black), - softWrap: true, - ), - ), - ], - ), - ); - } - - String getNiceHexArray(List bytes) { - return '[${bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join(', ')}]' - .toUpperCase(); - } - - String getNiceManufacturerData(Map> data) { - if (data.isEmpty) { - return 'N/A'; - } - List res = []; - data.forEach((id, bytes) { - res.add( - '${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}'); - }); - return res.join(', '); - } - - String getNiceServiceData(Map> data) { - if (data.isEmpty) { - return 'N/A'; - } - List res = []; - data.forEach((id, bytes) { - res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}'); - }); - return res.join(', '); - } - - @override - Widget build(BuildContext context) { - return ExpansionTile( - title: _buildTitle(context), - leading: Text(result.rssi.toString()), - trailing: RaisedButton( - child: Text('CONNECT'), - color: Colors.black, - textColor: Colors.white, - onPressed: (result.advertisementData.connectable) ? onTap : null, - ), - children: [ - _buildAdvRow( - context, 'Complete Local Name', result.advertisementData.localName), - _buildAdvRow(context, 'Tx Power Level', - '${result.advertisementData.txPowerLevel ?? 'N/A'}'), - _buildAdvRow(context, 'Manufacturer Data', - getNiceManufacturerData(result.advertisementData.manufacturerData)), - _buildAdvRow( - context, - 'Service UUIDs', - (result.advertisementData.serviceUuids.isNotEmpty) - ? result.advertisementData.serviceUuids.join(', ').toUpperCase() - : 'N/A'), - _buildAdvRow(context, 'Service Data', - getNiceServiceData(result.advertisementData.serviceData)), - ], - ); - } -} class ServiceTile extends StatelessWidget { final BluetoothService service; diff --git a/lib/screens/connection/find_connection.dart b/lib/screens/connection/find_connection.dart new file mode 100644 index 0000000..597a44b --- /dev/null +++ b/lib/screens/connection/find_connection.dart @@ -0,0 +1,255 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_blue/flutter_blue.dart'; +import 'bluetooth_device.dart'; + +class GloveConnectionPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: FlutterBlue.instance.state, + initialData: BluetoothState.unknown, + builder: (c, snapshot) { + final state = snapshot.data; + if (state == BluetoothState.on) { + return FindDevicesScreen(); + } + return BluetoothOffScreen(state: state); + }); + } +} + +class BluetoothOffScreen extends StatelessWidget { + const BluetoothOffScreen({Key? key, this.state}) : super(key: key); + + final BluetoothState? state; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.lightBlue, + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.bluetooth_disabled, + size: 200.0, + color: Colors.white54, + ), + Text( + 'Bluetooth Adapter is ${state != null ? state.toString().substring(15) : 'not available'}.', + style: Theme.of(context) + .primaryTextTheme + .subhead + ?.copyWith(color: Colors.white), + ), + ], + ), + ), + ); + } +} + +class FindDevicesScreen extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Find Devices'), + ), + body: RefreshIndicator( + onRefresh: () => + FlutterBlue.instance.startScan(timeout: Duration(seconds: 4)), + child: SingleChildScrollView( + child: Column( + children: [ + StreamBuilder>( + stream: Stream.periodic(Duration(seconds: 2)) + .asyncMap((_) => FlutterBlue.instance.connectedDevices), + initialData: [], + builder: (c, snapshot) => Column( + children: snapshot.data! + .map((d) => ListTile( + title: Text(d.name), + subtitle: Text(d.id.toString()), + trailing: StreamBuilder( + stream: d.state, + initialData: BluetoothDeviceState.disconnected, + builder: (c, snapshot) { + if (snapshot.data == + BluetoothDeviceState.connected) { + return RaisedButton( + child: Text('OPEN'), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + DeviceScreen(device: d))), + ); + } + return Text(snapshot.data.toString()); + }, + ), + )) + .toList(), + ), + ), + StreamBuilder>( + stream: FlutterBlue.instance.scanResults, + initialData: [], + builder: (c, snapshot) => Column( + children: snapshot.data! + .map( + (r) => ScanResultTile( + result: r, + onTap: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + r.device.connect(); + return DeviceScreen(device: r.device); + })), + ), + ) + .toList(), + ), + ), + ], + ), + ), + ), + floatingActionButton: StreamBuilder( + stream: FlutterBlue.instance.isScanning, + initialData: false, + builder: (c, snapshot) { + if (snapshot.data!) { + return FloatingActionButton( + child: Icon(Icons.stop), + onPressed: () => FlutterBlue.instance.stopScan(), + backgroundColor: Colors.red, + ); + } else { + return FloatingActionButton( + child: Icon(Icons.search), + onPressed: () => FlutterBlue.instance.startScan(timeout: Duration(seconds: 4))); + } + }, + ), + ); + } +} + +class ScanResultTile extends StatelessWidget { + const ScanResultTile({Key? key, required this.result, this.onTap}) + : super(key: key); + + final ScanResult result; + final VoidCallback? onTap; + + Widget _buildTitle(BuildContext context) { + if (result.device.name.length > 0) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + result.device.name, + overflow: TextOverflow.ellipsis, + ), + Text( + result.device.id.toString(), + style: Theme.of(context).textTheme.caption, + ) + ], + ); + } else { + return Text(result.device.id.toString()); + } + } + + Widget _buildAdvRow(BuildContext context, String title, String value) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.caption), + SizedBox( + width: 12.0, + ), + Expanded( + child: Text( + value, + style: Theme.of(context) + .textTheme + .caption + ?.apply(color: Colors.black), + softWrap: true, + ), + ), + ], + ), + ); + } + + String getNiceHexArray(List bytes) { + return '[${bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join(', ')}]' + .toUpperCase(); + } + + String getNiceManufacturerData(Map> data) { + if (data.isEmpty) { + return 'N/A'; + } + List res = []; + data.forEach((id, bytes) { + res.add( + '${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}'); + }); + return res.join(', '); + } + + String getNiceServiceData(Map> data) { + if (data.isEmpty) { + return 'N/A'; + } + List res = []; + data.forEach((id, bytes) { + res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}'); + }); + return res.join(', '); + } + + @override + Widget build(BuildContext context) { + return ExpansionTile( + title: _buildTitle(context), + leading: Text(result.rssi.toString()), + trailing: RaisedButton( + child: Text('CONNECT'), + color: Colors.black, + textColor: Colors.white, + onPressed: (result.advertisementData.connectable) ? onTap : null, + ), + children: [ + _buildAdvRow( + context, 'Complete Local Name', result.advertisementData.localName), + _buildAdvRow(context, 'Tx Power Level', + '${result.advertisementData.txPowerLevel ?? 'N/A'}'), + _buildAdvRow(context, 'Manufacturer Data', + getNiceManufacturerData(result.advertisementData.manufacturerData)), + _buildAdvRow( + context, + 'Service UUIDs', + (result.advertisementData.serviceUuids.isNotEmpty) + ? result.advertisementData.serviceUuids.join(', ').toUpperCase() + : 'N/A'), + _buildAdvRow(context, 'Service Data', + getNiceServiceData(result.advertisementData.serviceData)), + ], + ); + } +} + + diff --git a/lib/screens/edgeimpulse/api_client.dart b/lib/screens/edgeimpulse/api_client.dart new file mode 100644 index 0000000..d953edb --- /dev/null +++ b/lib/screens/edgeimpulse/api_client.dart @@ -0,0 +1,148 @@ +import 'dart:convert'; +import 'dart:io'; + + +void uploadFile() async { + + var values = [ + [ -9.81, 0.03, 1.21 ], + [ -9.83, 0.04, 1.27 ], + [ -9.12, 0.03, 1.23 ], + [ -9.14, 0.01, 1.25 ] + ]; + + var sensor = [Sensor("accX","m/s2"), Sensor("accY","m/s2"), Sensor("accZ","m/s2")]; + + var payload = Payload( + deviceName: "ac:87:a3:0a:2d:1b", + deviceType: "DISCO-L475VG-IOT01A", + intervalMs: 10, + sensors: sensor, + values: values + ); + var protected = Protected( + ver: "v1", + alg: "HS256", + iat: 1625527314 + ); + var edgeImpulseBody = EdgeImpulseBody(protected: protected, signature: "empty", payload: payload); + print("sending post"); + print(edgeImpulseBody.toJson()); + + HttpClient httpClient = new HttpClient(); + HttpClientRequest request = await httpClient.postUrl(Uri.parse('https://ingestion.edgeimpulse.com/api/training/data')); + request.headers.set('Content-type', 'application/json'); + request.headers.set('x-api-key', 'ei_7223fabc8d9843d8fff520e22a5b6fc5578444d02ea8b610398089e6cbc175ea'); + request.headers.set('x-file-name', 'desdeLaApp'); + request.add(utf8.encode(json.encode(edgeImpulseBody))); + HttpClientResponse response = await request.close(); + String reply = await response.transform(utf8.decoder).join(); + print(reply ); + httpClient.close(); +} +/* +* { + "protected": { + "ver": "v1", + "alg": "HS256", + "iat": 1625527314 + }, + "signature": "emptySignature", + "payload": { + "device_name": "ac:87:a3:0a:2d:1b", + "device_type": "DISCO-L475VG-IOT01A", + "interval_ms": 10, + "sensors": [ + { "name": "accX", "units": "m/s2" }, + { "name": "accY", "units": "m/s2" }, + { "name": "accZ", "units": "m/s2" } + ], + "values": [ + [ -9.81, 0.03, 1.21 ], + [ -9.83, 0.04, 1.27 ], + [ -9.12, 0.03, 1.23 ], + [ -9.14, 0.01, 1.25 ] + ] + } +} +*/ + +class EdgeImpulseBody { + final Protected protected; + final String signature; + final Payload payload; + EdgeImpulseBody({required this.protected, required this.signature, required this.payload}); + + Map toJson() { + return { + 'protected': protected.toJson(), + 'signature': signature, + 'payload': payload.toJson(), + }; + } + +} + +class Protected { + final String ver; + final String alg; + final int iat; + Protected({required this.ver, required this.alg, required this.iat}); + + Map toJson() { + return { + 'ver': ver, + 'alg': alg, + 'iat': iat + }; + } + +} + + +class Payload { + final String deviceName; + final String deviceType; + final int intervalMs; + final List sensors; + final List> values; + + Payload({ + required this.deviceName, + required this.deviceType, + required this.intervalMs, + required this.sensors, + required this.values }); + + + Map toJson() { + return { + 'device_name': deviceName, + 'device_type': deviceType, + 'interval_ms': intervalMs, + 'sensors': sensors.map((s) => s.toJson()).toList(), + 'values': values, + }; + } +} + +class Sensor { + late final String name; + late final String units; + + Sensor(String name, String units){ + this.name = name; + this.units = units; + } + + + Map toJson() { + return { + 'name': name, + 'units': units, + }; + } + + +} + diff --git a/lib/screens/files/storage.dart b/lib/screens/files/storage.dart index 7b79ef7..351d1b1 100644 --- a/lib/screens/files/storage.dart +++ b/lib/screens/files/storage.dart @@ -45,7 +45,7 @@ class GloveEventsStorage { Future createFile(String name) async { final path = await _localPath; - return File('$path/$name.txt'); + return File('$path/$name.csv'); } }