mirror of
https://github.com/gosticks/growme.git
synced 2025-10-16 11:45:38 +00:00
feat: enable full app motor controlls
This commit is contained in:
parent
8f1779fe38
commit
14ee41e4c2
@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '15.0'
|
||||
platform :ios, '15.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
127
app/lib/app.pb.dart
Normal file
127
app/lib/app.pb.dart
Normal file
@ -0,0 +1,127 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: app.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class KnownDevicesState extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'KnownDevicesState', createEmptyInstance: create)
|
||||
..pc<KnownDevice>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'devices', $pb.PbFieldType.PM, subBuilder: KnownDevice.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
KnownDevicesState._() : super();
|
||||
factory KnownDevicesState({
|
||||
$core.Iterable<KnownDevice>? devices,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (devices != null) {
|
||||
_result.devices.addAll(devices);
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory KnownDevicesState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory KnownDevicesState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
KnownDevicesState clone() => KnownDevicesState()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
KnownDevicesState copyWith(void Function(KnownDevicesState) updates) => super.copyWith((message) => updates(message as KnownDevicesState)) as KnownDevicesState; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static KnownDevicesState create() => KnownDevicesState._();
|
||||
KnownDevicesState createEmptyInstance() => create();
|
||||
static $pb.PbList<KnownDevicesState> createRepeated() => $pb.PbList<KnownDevicesState>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static KnownDevicesState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<KnownDevicesState>(create);
|
||||
static KnownDevicesState? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<KnownDevice> get devices => $_getList(0);
|
||||
}
|
||||
|
||||
class KnownDevice extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'KnownDevice', createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
|
||||
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'remoteId', protoName: 'remoteId')
|
||||
..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'remoteName', protoName: 'remoteName')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
KnownDevice._() : super();
|
||||
factory KnownDevice({
|
||||
$core.String? name,
|
||||
$core.String? remoteId,
|
||||
$core.String? remoteName,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (name != null) {
|
||||
_result.name = name;
|
||||
}
|
||||
if (remoteId != null) {
|
||||
_result.remoteId = remoteId;
|
||||
}
|
||||
if (remoteName != null) {
|
||||
_result.remoteName = remoteName;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory KnownDevice.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory KnownDevice.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
KnownDevice clone() => KnownDevice()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
KnownDevice copyWith(void Function(KnownDevice) updates) => super.copyWith((message) => updates(message as KnownDevice)) as KnownDevice; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static KnownDevice create() => KnownDevice._();
|
||||
KnownDevice createEmptyInstance() => create();
|
||||
static $pb.PbList<KnownDevice> createRepeated() => $pb.PbList<KnownDevice>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static KnownDevice getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<KnownDevice>(create);
|
||||
static KnownDevice? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get name => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set name($core.String v) { $_setString(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasName() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearName() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get remoteId => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set remoteId($core.String v) { $_setString(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasRemoteId() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearRemoteId() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.String get remoteName => $_getSZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set remoteName($core.String v) { $_setString(2, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasRemoteName() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearRemoteName() => clearField(3);
|
||||
}
|
||||
|
||||
7
app/lib/app.pbenum.dart
Normal file
7
app/lib/app.pbenum.dart
Normal file
@ -0,0 +1,7 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: app.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
32
app/lib/app.pbjson.dart
Normal file
32
app/lib/app.pbjson.dart
Normal file
@ -0,0 +1,32 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: app.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
@$core.Deprecated('Use knownDevicesStateDescriptor instead')
|
||||
const KnownDevicesState$json = const {
|
||||
'1': 'KnownDevicesState',
|
||||
'2': const [
|
||||
const {'1': 'devices', '3': 1, '4': 3, '5': 11, '6': '.KnownDevice', '10': 'devices'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `KnownDevicesState`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List knownDevicesStateDescriptor = $convert.base64Decode('ChFLbm93bkRldmljZXNTdGF0ZRImCgdkZXZpY2VzGAEgAygLMgwuS25vd25EZXZpY2VSB2RldmljZXM=');
|
||||
@$core.Deprecated('Use knownDeviceDescriptor instead')
|
||||
const KnownDevice$json = const {
|
||||
'1': 'KnownDevice',
|
||||
'2': const [
|
||||
const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
|
||||
const {'1': 'remoteId', '3': 2, '4': 1, '5': 9, '10': 'remoteId'},
|
||||
const {'1': 'remoteName', '3': 3, '4': 1, '5': 9, '10': 'remoteName'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `KnownDevice`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List knownDeviceDescriptor = $convert.base64Decode('CgtLbm93bkRldmljZRISCgRuYW1lGAEgASgJUgRuYW1lEhoKCHJlbW90ZUlkGAIgASgJUghyZW1vdGVJZBIeCgpyZW1vdGVOYW1lGAMgASgJUgpyZW1vdGVOYW1l');
|
||||
9
app/lib/app.pbserver.dart
Normal file
9
app/lib/app.pbserver.dart
Normal file
@ -0,0 +1,9 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: app.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
export 'app.pb.dart';
|
||||
|
||||
46
app/lib/components/bottom_sheet.dart
Normal file
46
app/lib/components/bottom_sheet.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomSheet extends StatelessWidget {
|
||||
const BottomSheet({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 30, vertical: 60),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
color: Colors.white,
|
||||
shape: BoxShape.rectangle,
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300, width: 1, style: BorderStyle.solid),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.4),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 10), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 15),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future showCustomModalBottomSheet(BuildContext context, Widget child) {
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return BottomSheet(child: child);
|
||||
});
|
||||
}
|
||||
26
app/lib/components/button.dart
Normal file
26
app/lib/components/button.dart
Normal file
@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Button extends StatelessWidget {
|
||||
const Button(this.text, this.onTap, {super.key});
|
||||
|
||||
final void Function() onTap;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 50.0,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
color: Colors.lightGreen[500],
|
||||
shape: BoxShape.rectangle,
|
||||
),
|
||||
child: Center(child: Text(text)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
app/lib/components/card.dart
Normal file
32
app/lib/components/card.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GrowMeCard extends StatelessWidget {
|
||||
const GrowMeCard({this.child, super.key});
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 50.0,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
color: Colors.white,
|
||||
shape: BoxShape.rectangle,
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade200, width: 2, style: BorderStyle.solid),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 10), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
152
app/lib/components/connection_sheet.dart
Normal file
152
app/lib/components/connection_sheet.dart
Normal file
@ -0,0 +1,152 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:grow_me_app/app.pb.dart';
|
||||
import 'package:grow_me_app/components/bottom_sheet.dart';
|
||||
import 'package:grow_me_app/components/button.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue/flutter_blue.dart';
|
||||
import 'package:grow_me_app/components/device_model.dart';
|
||||
import 'package:grow_me_app/components/edit_device.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const bleServiceName = "4FAFC201-1FB5-459E-8FCC-C5C9C331914B";
|
||||
const searchDuration = Duration(seconds: 20);
|
||||
|
||||
class ConnectionSheet extends StatefulWidget {
|
||||
const ConnectionSheet({super.key});
|
||||
|
||||
@override
|
||||
ConnectionSheetState createState() => ConnectionSheetState();
|
||||
}
|
||||
|
||||
class ConnectionSheetState extends State<ConnectionSheet> {
|
||||
bool isScanning = false;
|
||||
List<BluetoothDevice> candidates = [];
|
||||
BluetoothDevice? currentlyConnectingDevice;
|
||||
|
||||
Future _scan() async {
|
||||
if (isScanning) {
|
||||
return;
|
||||
}
|
||||
|
||||
isScanning = true;
|
||||
FlutterBlue flutterBlue = FlutterBlue.instance;
|
||||
|
||||
// Start scanning
|
||||
var resp = flutterBlue.startScan(timeout: searchDuration);
|
||||
|
||||
// Listen to scan results
|
||||
flutterBlue.scanResults.listen((results) {
|
||||
// do something with scan results
|
||||
for (ScanResult r in results) {
|
||||
if (r.advertisementData.serviceUuids.contains(bleServiceName)) {
|
||||
log("found device with known service type");
|
||||
if (candidates.indexWhere((d) => d.id == r.device.id) != -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
candidates.add(r.device);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: forget devices that did not appear after multiple scans
|
||||
}
|
||||
});
|
||||
|
||||
await resp;
|
||||
|
||||
// Stop scanning
|
||||
flutterBlue.stopScan();
|
||||
|
||||
isScanning = false;
|
||||
}
|
||||
|
||||
Widget _scanningProgress() {
|
||||
if (!isScanning) {
|
||||
return const Scaffold();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
const Text("scanning for nearby devices"),
|
||||
const SizedBox(height: 10),
|
||||
const LinearProgressIndicator(
|
||||
value: null,
|
||||
),
|
||||
].toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _onConnectDevice(
|
||||
DeviceModel model, BluetoothDevice device) async {
|
||||
if (currentlyConnectingDevice != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var resp = device
|
||||
.connect()
|
||||
.then((value) {
|
||||
Navigator.pop(context);
|
||||
// remove devive after successfull connection.
|
||||
setState(() {
|
||||
candidates.remove(device);
|
||||
});
|
||||
|
||||
var linkedDevice = model.addFromDevice(device);
|
||||
|
||||
showEditDeviceModal(context, linkedDevice);
|
||||
return true;
|
||||
})
|
||||
.catchError((err) => false)
|
||||
.whenComplete(() => setState(() {
|
||||
currentlyConnectingDevice = null;
|
||||
}));
|
||||
setState(() {
|
||||
currentlyConnectingDevice = device;
|
||||
});
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
List<Widget> _availableDeviceList(DeviceModel model) {
|
||||
if (candidates.isEmpty) {
|
||||
return [const ListTile(title: Text('No devices found yet'))].toList();
|
||||
}
|
||||
|
||||
return candidates.map((device) {
|
||||
return ListTile(
|
||||
title: Text(device.name),
|
||||
subtitle: Text(device.id.toString()),
|
||||
enabled: device.id != currentlyConnectingDevice?.id,
|
||||
onTap: () {
|
||||
_onConnectDevice(model, device);
|
||||
});
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DeviceModel>(
|
||||
builder: (context, model, child) => Button(
|
||||
"Add Product",
|
||||
() {
|
||||
// start scanning for nearby devices
|
||||
_scan();
|
||||
|
||||
showCustomModalBottomSheet(
|
||||
context,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_scanningProgress(),
|
||||
..._availableDeviceList(model),
|
||||
const SizedBox(height: 25),
|
||||
],
|
||||
));
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
69
app/lib/components/device_carousel.dart
Normal file
69
app/lib/components/device_carousel.dart
Normal file
@ -0,0 +1,69 @@
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:grow_me_app/components/button.dart';
|
||||
import 'package:grow_me_app/components/card.dart';
|
||||
import 'package:grow_me_app/components/device_debug.dart';
|
||||
import 'package:grow_me_app/components/device_model.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DeviceCarousel extends StatefulWidget {
|
||||
const DeviceCarousel({super.key, this.leadingCarouselItems});
|
||||
|
||||
final List<Widget>? leadingCarouselItems;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => DeviceCarouselState();
|
||||
}
|
||||
|
||||
class DeviceCarouselState extends State<DeviceCarousel> {
|
||||
List<Widget> _plantCards(DeviceModel model) {
|
||||
return model.devices.map((device) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: GrowMeCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(device.description.name, textScaleFactor: 2),
|
||||
const SizedBox(height: 15),
|
||||
const CircleAvatar(
|
||||
backgroundColor: Colors.lightGreen,
|
||||
backgroundImage:
|
||||
AssetImage('assets/prototype-render.png'),
|
||||
radius: 150.0,
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
showDeviceDebugView(context, device);
|
||||
},
|
||||
child: const Text("Open Debug menu"))
|
||||
].toList(),
|
||||
))));
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DeviceModel>(builder: (context, model, child) {
|
||||
return CarouselSlider(
|
||||
options: CarouselOptions(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
initialPage: model.devices.isNotEmpty ? 1 : 0,
|
||||
autoPlay: false,
|
||||
enlargeCenterPage: true,
|
||||
clipBehavior: Clip.none,
|
||||
enableInfiniteScroll: false),
|
||||
items: widget.leadingCarouselItems != null
|
||||
? [...widget.leadingCarouselItems!, ..._plantCards(model)]
|
||||
: _plantCards(model),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
117
app/lib/components/device_debug.dart
Normal file
117
app/lib/components/device_debug.dart
Normal file
@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue/flutter_blue.dart';
|
||||
import 'package:grow_me_app/components/bottom_sheet.dart';
|
||||
import 'package:grow_me_app/components/connection_sheet.dart';
|
||||
import 'package:grow_me_app/components/device_model.dart';
|
||||
import 'package:grow_me_app/message.pb.dart';
|
||||
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
class DeviceDebugView extends StatefulWidget {
|
||||
const DeviceDebugView({super.key, required this.device});
|
||||
|
||||
final LinkedDevice device;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => DeviceDebugViewState();
|
||||
}
|
||||
|
||||
class DeviceDebugViewState extends State<DeviceDebugView> {
|
||||
final List<double> _motorStepValues = [20, 20, 40, 50, 60, 20];
|
||||
|
||||
Future<BluetoothCharacteristic?> _getCharacteristic() async {
|
||||
var device = widget.device.device;
|
||||
try {
|
||||
var services = await device!.discoverServices();
|
||||
|
||||
BluetoothService service = services.firstWhere(
|
||||
(element) => element.uuid.toString() == bleServiceName.toLowerCase());
|
||||
if (service == null) {
|
||||
developer.log("device does not offer motor control service",
|
||||
name: "device.debug");
|
||||
}
|
||||
return service.characteristics.first;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _resetMotorPosition(int index) async {
|
||||
var ch = await _getCharacteristic();
|
||||
if (ch == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cmd = Command(reset: ResetMotorPositionCommand());
|
||||
|
||||
await ch.write(cmd.writeToBuffer(), withoutResponse: false);
|
||||
await ch.setNotifyValue(true);
|
||||
}
|
||||
|
||||
void _sendMotorControl(int index, bool isDecriment) async {
|
||||
var ch = await _getCharacteristic();
|
||||
if (ch == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find motor control service
|
||||
int factor = isDecriment ? -1 : 1;
|
||||
|
||||
var cmd = Command(
|
||||
move:
|
||||
MoveMotorCommand(target: factor * _motorStepValues[index].toInt()));
|
||||
|
||||
await ch.write(cmd.writeToBuffer(), withoutResponse: false);
|
||||
await ch.setNotifyValue(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
Text(widget.device.description.name),
|
||||
..._motorStepValues.asMap().entries.map((entry) => Column(children: [
|
||||
const SizedBox(height: 15),
|
||||
Text("Motor ${entry.key + 1}"),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_resetMotorPosition(entry.key);
|
||||
},
|
||||
icon: const Icon(Icons.replay_outlined)),
|
||||
const Spacer(),
|
||||
Slider(
|
||||
value: entry.value,
|
||||
max: 1000,
|
||||
divisions: 25,
|
||||
label: _motorStepValues[entry.key].round().toString(),
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_motorStepValues[entry.key] = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_sendMotorControl(entry.key, true);
|
||||
},
|
||||
icon: const Icon(Icons.remove_circle)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_sendMotorControl(entry.key, false);
|
||||
},
|
||||
icon: const Icon(Icons.add_circle))
|
||||
],
|
||||
)
|
||||
])),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Future showDeviceDebugView(BuildContext context, LinkedDevice device) {
|
||||
return showCustomModalBottomSheet(
|
||||
context,
|
||||
DeviceDebugView(device: device),
|
||||
);
|
||||
}
|
||||
111
app/lib/components/device_model.dart
Normal file
111
app/lib/components/device_model.dart
Normal file
@ -0,0 +1,111 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue/flutter_blue.dart';
|
||||
import 'package:grow_me_app/app.pb.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const storageKey = "known-devices";
|
||||
|
||||
class LinkedDevice {
|
||||
BluetoothDevice? device;
|
||||
KnownDevice description;
|
||||
BluetoothDeviceState _status = BluetoothDeviceState.disconnected;
|
||||
|
||||
bool get isConnected {
|
||||
return device != null && _status == BluetoothDeviceState.connected;
|
||||
}
|
||||
|
||||
LinkedDevice(this.description, this.device) {
|
||||
if (device != null) {
|
||||
device!.state.listen((event) {
|
||||
_status = event;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceModel extends ChangeNotifier {
|
||||
List<LinkedDevice> _devices = [];
|
||||
|
||||
UnmodifiableListView<LinkedDevice> get devices =>
|
||||
UnmodifiableListView(_devices);
|
||||
|
||||
DeviceModel() {
|
||||
// try reloading state
|
||||
_loadState();
|
||||
}
|
||||
|
||||
void add(LinkedDevice device) {
|
||||
_devices.add(device);
|
||||
notifyListeners();
|
||||
|
||||
_saveState();
|
||||
}
|
||||
|
||||
LinkedDevice addFromDevice(BluetoothDevice device) {
|
||||
var linkedDevice = LinkedDevice(
|
||||
KnownDevice(
|
||||
name: "Untitled",
|
||||
remoteId: device.id.toString(),
|
||||
remoteName: device.name),
|
||||
device,
|
||||
);
|
||||
|
||||
add(linkedDevice);
|
||||
|
||||
return linkedDevice;
|
||||
}
|
||||
|
||||
void remove(LinkedDevice device) {
|
||||
_devices.remove(device);
|
||||
notifyListeners();
|
||||
|
||||
_saveState();
|
||||
}
|
||||
|
||||
void removeById(String id) {
|
||||
var idx =
|
||||
_devices.indexWhere((element) => element.device?.id.toString() == id);
|
||||
if (idx == -1) {
|
||||
return;
|
||||
}
|
||||
_devices.removeAt(idx);
|
||||
notifyListeners();
|
||||
|
||||
_saveState();
|
||||
}
|
||||
|
||||
void updateEntry(String id, KnownDevice description) {
|
||||
var idx =
|
||||
_devices.indexWhere((element) => element.device?.id.toString() == id);
|
||||
if (idx == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// potential trouble if done without thread safety
|
||||
_devices[idx].description = description;
|
||||
notifyListeners();
|
||||
|
||||
_saveState();
|
||||
}
|
||||
|
||||
void _loadState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String? deviceData = prefs.getString(storageKey);
|
||||
if (deviceData != null) {
|
||||
var state = KnownDevicesState.fromJson(jsonDecode(deviceData));
|
||||
_devices = state.devices.map((d) => LinkedDevice(d, null)).toList();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _saveState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// encode current devices
|
||||
var state = KnownDevicesState(devices: devices.map((d) => d.description));
|
||||
prefs.setString(storageKey, jsonEncode(state.toProto3Json()));
|
||||
}
|
||||
}
|
||||
79
app/lib/components/edit_device.dart
Normal file
79
app/lib/components/edit_device.dart
Normal file
@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue/flutter_blue.dart';
|
||||
import 'package:grow_me_app/components/bottom_sheet.dart';
|
||||
import 'package:grow_me_app/components/button.dart';
|
||||
import 'package:grow_me_app/components/device_model.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EditDeviceView extends StatefulWidget {
|
||||
const EditDeviceView({
|
||||
required this.device,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final LinkedDevice device;
|
||||
|
||||
@override
|
||||
EditDeviceViewState createState() => EditDeviceViewState();
|
||||
}
|
||||
|
||||
class EditDeviceViewState extends State<EditDeviceView> {
|
||||
TextEditingController _deviceNameController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_deviceNameController =
|
||||
TextEditingController(text: widget.device.description.name);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up the controller when the widget is removed from the
|
||||
// widget tree.
|
||||
_deviceNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DeviceModel>(
|
||||
builder: (context, model, value) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 15),
|
||||
TextField(
|
||||
controller: _deviceNameController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'Enter a search term',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
const CircleAvatar(
|
||||
backgroundColor: Colors.lightGreen,
|
||||
backgroundImage: AssetImage('assets/prototype-render.png'),
|
||||
radius: 150.0,
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
Button("Save", () {
|
||||
widget.device.description.name =
|
||||
_deviceNameController.value.text;
|
||||
|
||||
model.updateEntry(widget.device.description.remoteId,
|
||||
widget.device.description);
|
||||
|
||||
Navigator.pop(context);
|
||||
})
|
||||
].toList(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future showEditDeviceModal(BuildContext context, LinkedDevice device) {
|
||||
return showCustomModalBottomSheet(
|
||||
context,
|
||||
EditDeviceView(
|
||||
device: device,
|
||||
));
|
||||
}
|
||||
283
app/lib/components/health.dart.tmp
Normal file
283
app/lib/components/health.dart.tmp
Normal file
@ -0,0 +1,283 @@
|
||||
List<HealthDataPoint> _healthDataList = [];
|
||||
AppState _state = AppState.DATA_NOT_FETCHED;
|
||||
int _nofSteps = 10;
|
||||
double _mgdl = 10.0;
|
||||
|
||||
// create a HealthFactory for use in the app
|
||||
HealthFactory health = HealthFactory();
|
||||
|
||||
/// Fetch data points from the health plugin and show them in the app.
|
||||
Future fetchData() async {
|
||||
setState(() => _state = AppState.FETCHING_DATA);
|
||||
|
||||
// define the types to get
|
||||
final types = [
|
||||
HealthDataType.STEPS,
|
||||
HealthDataType.WEIGHT,
|
||||
HealthDataType.HEIGHT,
|
||||
HealthDataType.BLOOD_GLUCOSE,
|
||||
HealthDataType.WORKOUT,
|
||||
// Uncomment these lines on iOS - only available on iOS
|
||||
// HealthDataType.AUDIOGRAM
|
||||
];
|
||||
|
||||
// with coresponsing permissions
|
||||
final permissions = [
|
||||
HealthDataAccess.READ,
|
||||
HealthDataAccess.READ,
|
||||
HealthDataAccess.READ,
|
||||
HealthDataAccess.READ,
|
||||
HealthDataAccess.READ,
|
||||
// HealthDataAccess.READ,
|
||||
];
|
||||
|
||||
// get data within the last 24 hours
|
||||
final now = DateTime.now();
|
||||
final yesterday = now.subtract(Duration(days: 5));
|
||||
// requesting access to the data types before reading them
|
||||
// note that strictly speaking, the [permissions] are not
|
||||
// needed, since we only want READ access.
|
||||
bool requested =
|
||||
await health.requestAuthorization(types, permissions: permissions);
|
||||
print('requested: $requested');
|
||||
|
||||
// If we are trying to read Step Count, Workout, Sleep or other data that requires
|
||||
// the ACTIVITY_RECOGNITION permission, we need to request the permission first.
|
||||
// This requires a special request authorization call.
|
||||
//
|
||||
// The location permission is requested for Workouts using the Distance information.
|
||||
await Permission.activityRecognition.request();
|
||||
await Permission.location.request();
|
||||
|
||||
if (requested) {
|
||||
try {
|
||||
// fetch health data
|
||||
List<HealthDataPoint> healthData =
|
||||
await health.getHealthDataFromTypes(yesterday, now, types);
|
||||
// save all the new data points (only the first 100)
|
||||
_healthDataList.addAll((healthData.length < 100)
|
||||
? healthData
|
||||
: healthData.sublist(0, 100));
|
||||
} catch (error) {
|
||||
print("Exception in getHealthDataFromTypes: $error");
|
||||
}
|
||||
|
||||
// filter out duplicates
|
||||
_healthDataList = HealthFactory.removeDuplicates(_healthDataList);
|
||||
|
||||
// print the results
|
||||
_healthDataList.forEach((x) => print(x));
|
||||
|
||||
// update the UI to display the results
|
||||
setState(() {
|
||||
_state =
|
||||
_healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY;
|
||||
});
|
||||
} else {
|
||||
print("Authorization not granted");
|
||||
setState(() => _state = AppState.DATA_NOT_FETCHED);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add some random health data.
|
||||
Future addData() async {
|
||||
final now = DateTime.now();
|
||||
final earlier = now.subtract(Duration(minutes: 20));
|
||||
|
||||
final types = [
|
||||
HealthDataType.STEPS,
|
||||
HealthDataType.HEIGHT,
|
||||
HealthDataType.BLOOD_GLUCOSE,
|
||||
HealthDataType.WORKOUT, // Requires Google Fit on Android
|
||||
// Uncomment these lines on iOS - only available on iOS
|
||||
// HealthDataType.AUDIOGRAM,
|
||||
];
|
||||
final rights = [
|
||||
HealthDataAccess.WRITE,
|
||||
HealthDataAccess.WRITE,
|
||||
HealthDataAccess.WRITE,
|
||||
HealthDataAccess.WRITE,
|
||||
// HealthDataAccess.WRITE
|
||||
];
|
||||
final permissions = [
|
||||
HealthDataAccess.READ_WRITE,
|
||||
HealthDataAccess.READ_WRITE,
|
||||
HealthDataAccess.READ_WRITE,
|
||||
HealthDataAccess.READ_WRITE,
|
||||
// HealthDataAccess.READ_WRITE,
|
||||
];
|
||||
late bool perm;
|
||||
bool? hasPermissions =
|
||||
await HealthFactory.hasPermissions(types, permissions: rights);
|
||||
if (hasPermissions == false) {
|
||||
perm = await health.requestAuthorization(types, permissions: permissions);
|
||||
}
|
||||
|
||||
// Store a count of steps taken
|
||||
_nofSteps = Random().nextInt(10);
|
||||
bool success = await health.writeHealthData(
|
||||
_nofSteps.toDouble(), HealthDataType.STEPS, earlier, now);
|
||||
|
||||
// Store a height
|
||||
success &=
|
||||
await health.writeHealthData(1.93, HealthDataType.HEIGHT, earlier, now);
|
||||
|
||||
// Store a Blood Glucose measurement
|
||||
_mgdl = Random().nextInt(10) * 1.0;
|
||||
success &= await health.writeHealthData(
|
||||
_mgdl, HealthDataType.BLOOD_GLUCOSE, now, now);
|
||||
|
||||
// Store a workout eg. running
|
||||
success &= await health.writeWorkoutData(
|
||||
HealthWorkoutActivityType.RUNNING, earlier, now,
|
||||
// The following are optional parameters
|
||||
// and the UNITS are functional on iOS ONLY!
|
||||
totalEnergyBurned: 230,
|
||||
totalEnergyBurnedUnit: HealthDataUnit.KILOCALORIE,
|
||||
totalDistance: 1234,
|
||||
totalDistanceUnit: HealthDataUnit.FOOT,
|
||||
);
|
||||
|
||||
// Store an Audiogram
|
||||
// Uncomment these on iOS - only available on iOS
|
||||
// const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0];
|
||||
// const leftEarSensitivities = [49.0, 54.0, 89.0, 52.0, 77.0, 35.0];
|
||||
// const rightEarSensitivities = [76.0, 66.0, 90.0, 22.0, 85.0, 44.5];
|
||||
|
||||
// success &= await health.writeAudiogram(
|
||||
// frequencies,
|
||||
// leftEarSensitivities,
|
||||
// rightEarSensitivities,
|
||||
// now,
|
||||
// now,
|
||||
// metadata: {
|
||||
// "HKExternalUUID": "uniqueID",
|
||||
// "HKDeviceName": "bluetooth headphone",
|
||||
// },
|
||||
// );
|
||||
|
||||
setState(() {
|
||||
_state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED;
|
||||
});
|
||||
}
|
||||
|
||||
/// Fetch steps from the health plugin and show them in the app.
|
||||
Future fetchStepData() async {
|
||||
int? steps;
|
||||
|
||||
// get steps for today (i.e., since midnight)
|
||||
final now = DateTime.now();
|
||||
final midnight = DateTime(now.year, now.month, now.day);
|
||||
|
||||
bool requested = await health.requestAuthorization([HealthDataType.STEPS]);
|
||||
|
||||
if (requested) {
|
||||
try {
|
||||
steps = await health.getTotalStepsInInterval(midnight, now);
|
||||
} catch (error) {
|
||||
print("Caught exception in getTotalStepsInInterval: $error");
|
||||
}
|
||||
|
||||
print('Total number of steps: $steps');
|
||||
|
||||
setState(() {
|
||||
_nofSteps = (steps == null) ? 0 : steps;
|
||||
_state = (steps == null) ? AppState.NO_DATA : AppState.STEPS_READY;
|
||||
});
|
||||
} else {
|
||||
print("Authorization not granted - error in authorization");
|
||||
setState(() => _state = AppState.DATA_NOT_FETCHED);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _contentFetchingData() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 10,
|
||||
)),
|
||||
Text('Fetching data...')
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _contentDataReady() {
|
||||
return ListView.builder(
|
||||
itemCount: _healthDataList.length,
|
||||
itemBuilder: (_, index) {
|
||||
HealthDataPoint p = _healthDataList[index];
|
||||
if (p.value is AudiogramHealthValue) {
|
||||
return ListTile(
|
||||
title: Text("${p.typeString}: ${p.value}"),
|
||||
trailing: Text('${p.unitString}'),
|
||||
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
|
||||
);
|
||||
}
|
||||
if (p.value is WorkoutHealthValue) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
"${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.typeToString()}"),
|
||||
trailing: Text(
|
||||
'${(p.value as WorkoutHealthValue).workoutActivityType.typeToString()}'),
|
||||
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
title: Text("${p.typeString}: ${p.value}"),
|
||||
trailing: Text('${p.unitString}'),
|
||||
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _contentNoData() {
|
||||
return Text('No Data to show');
|
||||
}
|
||||
|
||||
Widget _contentNotFetched() {
|
||||
return Column(
|
||||
children: [
|
||||
Text('No data available'),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _authorizationNotGranted() {
|
||||
return Text('Authorization not given. '
|
||||
'For Android please check your OAUTH2 client ID is correct in Google Developer Console. '
|
||||
'For iOS check your permissions in Apple Health.');
|
||||
}
|
||||
|
||||
Widget _dataAdded() {
|
||||
return Text('Data points inserted successfully!');
|
||||
}
|
||||
|
||||
Widget _stepsFetched() {
|
||||
return Text('Total number of steps: $_nofSteps');
|
||||
}
|
||||
|
||||
Widget _dataNotAdded() {
|
||||
return Text('Failed to add data');
|
||||
}
|
||||
|
||||
Widget _content() {
|
||||
if (_state == AppState.DATA_READY)
|
||||
return _contentDataReady();
|
||||
else if (_state == AppState.NO_DATA)
|
||||
return _contentNoData();
|
||||
else if (_state == AppState.FETCHING_DATA)
|
||||
return _contentFetchingData();
|
||||
else if (_state == AppState.AUTH_NOT_GRANTED)
|
||||
return _authorizationNotGranted();
|
||||
else if (_state == AppState.DATA_ADDED)
|
||||
return _dataAdded();
|
||||
else if (_state == AppState.STEPS_READY)
|
||||
return _stepsFetched();
|
||||
else if (_state == AppState.DATA_NOT_ADDED) return _dataNotAdded();
|
||||
|
||||
return _contentNotFetched();
|
||||
}
|
||||
@ -1,16 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:grow_me_app/components/card.dart';
|
||||
import 'package:grow_me_app/components/connection_sheet.dart';
|
||||
import 'package:grow_me_app/components/device_carousel.dart';
|
||||
import 'package:grow_me_app/components/device_model.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:health/health.dart';
|
||||
import 'package:flutter_blue/flutter_blue.dart';
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const GrowMeApp());
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
||||
|
||||
runApp(ChangeNotifierProvider(
|
||||
create: (context) => DeviceModel(), child: const GrowMeApp()));
|
||||
}
|
||||
|
||||
class GrowMeApp extends StatelessWidget {
|
||||
@ -68,310 +71,25 @@ enum AppState {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<GrowMeHomePage> {
|
||||
List<HealthDataPoint> _healthDataList = [];
|
||||
AppState _state = AppState.DATA_NOT_FETCHED;
|
||||
int _nofSteps = 10;
|
||||
double _mgdl = 10.0;
|
||||
|
||||
// create a HealthFactory for use in the app
|
||||
HealthFactory health = HealthFactory();
|
||||
|
||||
/// Fetch data points from the health plugin and show them in the app.
|
||||
Future fetchData() async {
|
||||
setState(() => _state = AppState.FETCHING_DATA);
|
||||
|
||||
// define the types to get
|
||||
final types = [
|
||||
HealthDataType.STEPS,
|
||||
HealthDataType.WEIGHT,
|
||||
HealthDataType.HEIGHT,
|
||||
HealthDataType.BLOOD_GLUCOSE,
|
||||
HealthDataType.WORKOUT,
|
||||
// Uncomment these lines on iOS - only available on iOS
|
||||
// HealthDataType.AUDIOGRAM
|
||||
];
|
||||
|
||||
// with coresponsing permissions
|
||||
final permissions = [
|
||||
HealthDataAccess.READ,
|
||||
HealthDataAccess.READ,
|
||||
HealthDataAccess.READ,
|
||||
HealthDataAccess.READ,
|
||||
HealthDataAccess.READ,
|
||||
// HealthDataAccess.READ,
|
||||
];
|
||||
|
||||
// get data within the last 24 hours
|
||||
final now = DateTime.now();
|
||||
final yesterday = now.subtract(Duration(days: 5));
|
||||
// requesting access to the data types before reading them
|
||||
// note that strictly speaking, the [permissions] are not
|
||||
// needed, since we only want READ access.
|
||||
bool requested =
|
||||
await health.requestAuthorization(types, permissions: permissions);
|
||||
print('requested: $requested');
|
||||
|
||||
// If we are trying to read Step Count, Workout, Sleep or other data that requires
|
||||
// the ACTIVITY_RECOGNITION permission, we need to request the permission first.
|
||||
// This requires a special request authorization call.
|
||||
//
|
||||
// The location permission is requested for Workouts using the Distance information.
|
||||
await Permission.activityRecognition.request();
|
||||
await Permission.location.request();
|
||||
|
||||
if (requested) {
|
||||
try {
|
||||
// fetch health data
|
||||
List<HealthDataPoint> healthData =
|
||||
await health.getHealthDataFromTypes(yesterday, now, types);
|
||||
// save all the new data points (only the first 100)
|
||||
_healthDataList.addAll((healthData.length < 100)
|
||||
? healthData
|
||||
: healthData.sublist(0, 100));
|
||||
} catch (error) {
|
||||
print("Exception in getHealthDataFromTypes: $error");
|
||||
}
|
||||
|
||||
// filter out duplicates
|
||||
_healthDataList = HealthFactory.removeDuplicates(_healthDataList);
|
||||
|
||||
// print the results
|
||||
_healthDataList.forEach((x) => print(x));
|
||||
|
||||
// update the UI to display the results
|
||||
setState(() {
|
||||
_state =
|
||||
_healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY;
|
||||
});
|
||||
} else {
|
||||
print("Authorization not granted");
|
||||
setState(() => _state = AppState.DATA_NOT_FETCHED);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add some random health data.
|
||||
Future addData() async {
|
||||
final now = DateTime.now();
|
||||
final earlier = now.subtract(Duration(minutes: 20));
|
||||
|
||||
final types = [
|
||||
HealthDataType.STEPS,
|
||||
HealthDataType.HEIGHT,
|
||||
HealthDataType.BLOOD_GLUCOSE,
|
||||
HealthDataType.WORKOUT, // Requires Google Fit on Android
|
||||
// Uncomment these lines on iOS - only available on iOS
|
||||
// HealthDataType.AUDIOGRAM,
|
||||
];
|
||||
final rights = [
|
||||
HealthDataAccess.WRITE,
|
||||
HealthDataAccess.WRITE,
|
||||
HealthDataAccess.WRITE,
|
||||
HealthDataAccess.WRITE,
|
||||
// HealthDataAccess.WRITE
|
||||
];
|
||||
final permissions = [
|
||||
HealthDataAccess.READ_WRITE,
|
||||
HealthDataAccess.READ_WRITE,
|
||||
HealthDataAccess.READ_WRITE,
|
||||
HealthDataAccess.READ_WRITE,
|
||||
// HealthDataAccess.READ_WRITE,
|
||||
];
|
||||
late bool perm;
|
||||
bool? hasPermissions =
|
||||
await HealthFactory.hasPermissions(types, permissions: rights);
|
||||
if (hasPermissions == false) {
|
||||
perm = await health.requestAuthorization(types, permissions: permissions);
|
||||
}
|
||||
|
||||
// Store a count of steps taken
|
||||
_nofSteps = Random().nextInt(10);
|
||||
bool success = await health.writeHealthData(
|
||||
_nofSteps.toDouble(), HealthDataType.STEPS, earlier, now);
|
||||
|
||||
// Store a height
|
||||
success &=
|
||||
await health.writeHealthData(1.93, HealthDataType.HEIGHT, earlier, now);
|
||||
|
||||
// Store a Blood Glucose measurement
|
||||
_mgdl = Random().nextInt(10) * 1.0;
|
||||
success &= await health.writeHealthData(
|
||||
_mgdl, HealthDataType.BLOOD_GLUCOSE, now, now);
|
||||
|
||||
// Store a workout eg. running
|
||||
success &= await health.writeWorkoutData(
|
||||
HealthWorkoutActivityType.RUNNING, earlier, now,
|
||||
// The following are optional parameters
|
||||
// and the UNITS are functional on iOS ONLY!
|
||||
totalEnergyBurned: 230,
|
||||
totalEnergyBurnedUnit: HealthDataUnit.KILOCALORIE,
|
||||
totalDistance: 1234,
|
||||
totalDistanceUnit: HealthDataUnit.FOOT,
|
||||
);
|
||||
|
||||
// Store an Audiogram
|
||||
// Uncomment these on iOS - only available on iOS
|
||||
// const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0];
|
||||
// const leftEarSensitivities = [49.0, 54.0, 89.0, 52.0, 77.0, 35.0];
|
||||
// const rightEarSensitivities = [76.0, 66.0, 90.0, 22.0, 85.0, 44.5];
|
||||
|
||||
// success &= await health.writeAudiogram(
|
||||
// frequencies,
|
||||
// leftEarSensitivities,
|
||||
// rightEarSensitivities,
|
||||
// now,
|
||||
// now,
|
||||
// metadata: {
|
||||
// "HKExternalUUID": "uniqueID",
|
||||
// "HKDeviceName": "bluetooth headphone",
|
||||
// },
|
||||
// );
|
||||
|
||||
setState(() {
|
||||
_state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED;
|
||||
});
|
||||
}
|
||||
|
||||
Future connectViaBluetooth() async {
|
||||
FlutterBlue flutterBlue = FlutterBlue.instance;
|
||||
|
||||
// Start scanning
|
||||
var resp = flutterBlue.startScan(timeout: Duration(seconds: 4));
|
||||
|
||||
// Listen to scan results
|
||||
var subscription = flutterBlue.scanResults.listen((results) {
|
||||
// do something with scan results
|
||||
for (ScanResult r in results) {
|
||||
print('${r.device.name} found! rssi: ${r.rssi}');
|
||||
}
|
||||
});
|
||||
|
||||
await resp;
|
||||
|
||||
// Stop scanning
|
||||
flutterBlue.stopScan();
|
||||
}
|
||||
|
||||
/// Fetch steps from the health plugin and show them in the app.
|
||||
Future fetchStepData() async {
|
||||
int? steps;
|
||||
|
||||
// get steps for today (i.e., since midnight)
|
||||
final now = DateTime.now();
|
||||
final midnight = DateTime(now.year, now.month, now.day);
|
||||
|
||||
bool requested = await health.requestAuthorization([HealthDataType.STEPS]);
|
||||
|
||||
if (requested) {
|
||||
try {
|
||||
steps = await health.getTotalStepsInInterval(midnight, now);
|
||||
} catch (error) {
|
||||
print("Caught exception in getTotalStepsInInterval: $error");
|
||||
}
|
||||
|
||||
print('Total number of steps: $steps');
|
||||
|
||||
setState(() {
|
||||
_nofSteps = (steps == null) ? 0 : steps;
|
||||
_state = (steps == null) ? AppState.NO_DATA : AppState.STEPS_READY;
|
||||
});
|
||||
} else {
|
||||
print("Authorization not granted - error in authorization");
|
||||
setState(() => _state = AppState.DATA_NOT_FETCHED);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _contentFetchingData() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 10,
|
||||
)),
|
||||
Text('Fetching data...')
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _contentDataReady() {
|
||||
return ListView.builder(
|
||||
itemCount: _healthDataList.length,
|
||||
itemBuilder: (_, index) {
|
||||
HealthDataPoint p = _healthDataList[index];
|
||||
if (p.value is AudiogramHealthValue) {
|
||||
return ListTile(
|
||||
title: Text("${p.typeString}: ${p.value}"),
|
||||
trailing: Text('${p.unitString}'),
|
||||
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
|
||||
);
|
||||
}
|
||||
if (p.value is WorkoutHealthValue) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
"${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.typeToString()}"),
|
||||
trailing: Text(
|
||||
'${(p.value as WorkoutHealthValue).workoutActivityType.typeToString()}'),
|
||||
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
title: Text("${p.typeString}: ${p.value}"),
|
||||
trailing: Text('${p.unitString}'),
|
||||
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _contentNoData() {
|
||||
return Text('No Data to show');
|
||||
}
|
||||
|
||||
Widget _contentNotFetched() {
|
||||
return Column(
|
||||
children: [
|
||||
Text('Press the download button to fetch data.'),
|
||||
Text('Press the plus button to insert some random data.'),
|
||||
Text('Press the walking button to get total step count.'),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _authorizationNotGranted() {
|
||||
return Text('Authorization not given. '
|
||||
'For Android please check your OAUTH2 client ID is correct in Google Developer Console. '
|
||||
'For iOS check your permissions in Apple Health.');
|
||||
}
|
||||
|
||||
Widget _dataAdded() {
|
||||
return Text('Data points inserted successfully!');
|
||||
}
|
||||
|
||||
Widget _stepsFetched() {
|
||||
return Text('Total number of steps: $_nofSteps');
|
||||
}
|
||||
|
||||
Widget _dataNotAdded() {
|
||||
return Text('Failed to add data');
|
||||
}
|
||||
|
||||
Widget _content() {
|
||||
if (_state == AppState.DATA_READY)
|
||||
return _contentDataReady();
|
||||
else if (_state == AppState.NO_DATA)
|
||||
return _contentNoData();
|
||||
else if (_state == AppState.FETCHING_DATA)
|
||||
return _contentFetchingData();
|
||||
else if (_state == AppState.AUTH_NOT_GRANTED)
|
||||
return _authorizationNotGranted();
|
||||
else if (_state == AppState.DATA_ADDED)
|
||||
return _dataAdded();
|
||||
else if (_state == AppState.STEPS_READY)
|
||||
return _stepsFetched();
|
||||
else if (_state == AppState.DATA_NOT_ADDED) return _dataNotAdded();
|
||||
|
||||
return _contentNotFetched();
|
||||
Widget _connectionCard() {
|
||||
return GrowMeCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
const Icon(Icons.bluetooth),
|
||||
const SizedBox(height: 15),
|
||||
const Text(
|
||||
"Grow your forest",
|
||||
textScaleFactor: 2,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
const Text("Add new GrowMe products"),
|
||||
const Spacer(),
|
||||
const ConnectionSheet(),
|
||||
].toList(),
|
||||
)));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -383,70 +101,21 @@ class _MyHomePageState extends State<GrowMeHomePage> {
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Here we take the value from the GrowMeHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
leadingWidth: 100,
|
||||
actions: [Icon(Icons.settings)],
|
||||
automaticallyImplyLeading: true,
|
||||
elevation: 0,
|
||||
),
|
||||
body: CarouselSlider(
|
||||
options: CarouselOptions(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
initialPage: 1,
|
||||
autoPlay: false,
|
||||
enlargeCenterPage: true,
|
||||
enableInfiniteScroll: false),
|
||||
items: [1, 2, 3, 4, 5].map((i) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
margin: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
margin: EdgeInsets.all(5),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(25),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 15),
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.lightGreen,
|
||||
backgroundImage:
|
||||
AssetImage('assets/prototype-render.png'),
|
||||
radius: 150.0,
|
||||
),
|
||||
SizedBox(height: 25),
|
||||
_content(),
|
||||
],
|
||||
))));
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
floatingActionButton:
|
||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Spacer(),
|
||||
FloatingActionButton(
|
||||
onPressed: () {
|
||||
fetchStepData();
|
||||
},
|
||||
tooltip: 'Load health data',
|
||||
child: const Icon(Icons.heart_broken),
|
||||
),
|
||||
SizedBox(
|
||||
width: 15,
|
||||
),
|
||||
FloatingActionButton(
|
||||
onPressed: () {
|
||||
connectViaBluetooth();
|
||||
},
|
||||
tooltip: 'Connect',
|
||||
child: const Icon(Icons.bluetooth_connected),
|
||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||
]));
|
||||
appBar: AppBar(
|
||||
// Here we take the value from the GrowMeHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
leadingWidth: 100,
|
||||
actions: [const Icon(Icons.settings)].toList(),
|
||||
automaticallyImplyLeading: true,
|
||||
elevation: 0,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
DeviceCarousel(leadingCarouselItems: [_connectionCard()]),
|
||||
const Spacer(),
|
||||
].toList()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
250
app/lib/message.pb.dart
Normal file
250
app/lib/message.pb.dart
Normal file
@ -0,0 +1,250 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: message.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
enum Command_Msg {
|
||||
progress,
|
||||
move,
|
||||
reset,
|
||||
notSet
|
||||
}
|
||||
|
||||
class Command extends $pb.GeneratedMessage {
|
||||
static const $core.Map<$core.int, Command_Msg> _Command_MsgByTag = {
|
||||
1 : Command_Msg.progress,
|
||||
2 : Command_Msg.move,
|
||||
3 : Command_Msg.reset,
|
||||
0 : Command_Msg.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Command', createEmptyInstance: create)
|
||||
..oo(0, [1, 2, 3])
|
||||
..aOM<ProgressCommand>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'progress', subBuilder: ProgressCommand.create)
|
||||
..aOM<MoveMotorCommand>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'move', subBuilder: MoveMotorCommand.create)
|
||||
..aOM<ResetMotorPositionCommand>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'reset', subBuilder: ResetMotorPositionCommand.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
Command._() : super();
|
||||
factory Command({
|
||||
ProgressCommand? progress,
|
||||
MoveMotorCommand? move,
|
||||
ResetMotorPositionCommand? reset,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (progress != null) {
|
||||
_result.progress = progress;
|
||||
}
|
||||
if (move != null) {
|
||||
_result.move = move;
|
||||
}
|
||||
if (reset != null) {
|
||||
_result.reset = reset;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory Command.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory Command.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
Command clone() => Command()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
Command copyWith(void Function(Command) updates) => super.copyWith((message) => updates(message as Command)) as Command; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Command create() => Command._();
|
||||
Command createEmptyInstance() => create();
|
||||
static $pb.PbList<Command> createRepeated() => $pb.PbList<Command>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Command getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Command>(create);
|
||||
static Command? _defaultInstance;
|
||||
|
||||
Command_Msg whichMsg() => _Command_MsgByTag[$_whichOneof(0)]!;
|
||||
void clearMsg() => clearField($_whichOneof(0));
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
ProgressCommand get progress => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set progress(ProgressCommand v) { setField(1, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasProgress() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearProgress() => clearField(1);
|
||||
@$pb.TagNumber(1)
|
||||
ProgressCommand ensureProgress() => $_ensure(0);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
MoveMotorCommand get move => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set move(MoveMotorCommand v) { setField(2, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasMove() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearMove() => clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
MoveMotorCommand ensureMove() => $_ensure(1);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
ResetMotorPositionCommand get reset => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set reset(ResetMotorPositionCommand v) { setField(3, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasReset() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearReset() => clearField(3);
|
||||
@$pb.TagNumber(3)
|
||||
ResetMotorPositionCommand ensureReset() => $_ensure(2);
|
||||
}
|
||||
|
||||
class ProgressCommand extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ProgressCommand', createEmptyInstance: create)
|
||||
..a<$core.double>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'progress', $pb.PbFieldType.OF)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
ProgressCommand._() : super();
|
||||
factory ProgressCommand({
|
||||
$core.double? progress,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (progress != null) {
|
||||
_result.progress = progress;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory ProgressCommand.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory ProgressCommand.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
ProgressCommand clone() => ProgressCommand()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
ProgressCommand copyWith(void Function(ProgressCommand) updates) => super.copyWith((message) => updates(message as ProgressCommand)) as ProgressCommand; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ProgressCommand create() => ProgressCommand._();
|
||||
ProgressCommand createEmptyInstance() => create();
|
||||
static $pb.PbList<ProgressCommand> createRepeated() => $pb.PbList<ProgressCommand>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ProgressCommand getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ProgressCommand>(create);
|
||||
static ProgressCommand? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.double get progress => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set progress($core.double v) { $_setFloat(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasProgress() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearProgress() => clearField(1);
|
||||
}
|
||||
|
||||
class MoveMotorCommand extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'MoveMotorCommand', createEmptyInstance: create)
|
||||
..a<$core.int>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'target', $pb.PbFieldType.O3)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
MoveMotorCommand._() : super();
|
||||
factory MoveMotorCommand({
|
||||
$core.int? target,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (target != null) {
|
||||
_result.target = target;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory MoveMotorCommand.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory MoveMotorCommand.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
MoveMotorCommand clone() => MoveMotorCommand()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
MoveMotorCommand copyWith(void Function(MoveMotorCommand) updates) => super.copyWith((message) => updates(message as MoveMotorCommand)) as MoveMotorCommand; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static MoveMotorCommand create() => MoveMotorCommand._();
|
||||
MoveMotorCommand createEmptyInstance() => create();
|
||||
static $pb.PbList<MoveMotorCommand> createRepeated() => $pb.PbList<MoveMotorCommand>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static MoveMotorCommand getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<MoveMotorCommand>(create);
|
||||
static MoveMotorCommand? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.int get target => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set target($core.int v) { $_setSignedInt32(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasTarget() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearTarget() => clearField(1);
|
||||
}
|
||||
|
||||
class ResetMotorPositionCommand extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ResetMotorPositionCommand', createEmptyInstance: create)
|
||||
..a<$core.int>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'motorIndex', $pb.PbFieldType.O3, protoName: 'motorIndex')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
ResetMotorPositionCommand._() : super();
|
||||
factory ResetMotorPositionCommand({
|
||||
$core.int? motorIndex,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (motorIndex != null) {
|
||||
_result.motorIndex = motorIndex;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory ResetMotorPositionCommand.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory ResetMotorPositionCommand.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
ResetMotorPositionCommand clone() => ResetMotorPositionCommand()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
ResetMotorPositionCommand copyWith(void Function(ResetMotorPositionCommand) updates) => super.copyWith((message) => updates(message as ResetMotorPositionCommand)) as ResetMotorPositionCommand; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ResetMotorPositionCommand create() => ResetMotorPositionCommand._();
|
||||
ResetMotorPositionCommand createEmptyInstance() => create();
|
||||
static $pb.PbList<ResetMotorPositionCommand> createRepeated() => $pb.PbList<ResetMotorPositionCommand>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ResetMotorPositionCommand getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ResetMotorPositionCommand>(create);
|
||||
static ResetMotorPositionCommand? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.int get motorIndex => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set motorIndex($core.int v) { $_setSignedInt32(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasMotorIndex() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearMotorIndex() => clearField(1);
|
||||
}
|
||||
|
||||
7
app/lib/message.pbenum.dart
Normal file
7
app/lib/message.pbenum.dart
Normal file
@ -0,0 +1,7 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: message.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
55
app/lib/message.pbjson.dart
Normal file
55
app/lib/message.pbjson.dart
Normal file
@ -0,0 +1,55 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: message.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
@$core.Deprecated('Use commandDescriptor instead')
|
||||
const Command$json = const {
|
||||
'1': 'Command',
|
||||
'2': const [
|
||||
const {'1': 'progress', '3': 1, '4': 1, '5': 11, '6': '.ProgressCommand', '9': 0, '10': 'progress'},
|
||||
const {'1': 'move', '3': 2, '4': 1, '5': 11, '6': '.MoveMotorCommand', '9': 0, '10': 'move'},
|
||||
const {'1': 'reset', '3': 3, '4': 1, '5': 11, '6': '.ResetMotorPositionCommand', '9': 0, '10': 'reset'},
|
||||
],
|
||||
'8': const [
|
||||
const {'1': 'msg'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Command`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List commandDescriptor = $convert.base64Decode('CgdDb21tYW5kEi4KCHByb2dyZXNzGAEgASgLMhAuUHJvZ3Jlc3NDb21tYW5kSABSCHByb2dyZXNzEicKBG1vdmUYAiABKAsyES5Nb3ZlTW90b3JDb21tYW5kSABSBG1vdmUSMgoFcmVzZXQYAyABKAsyGi5SZXNldE1vdG9yUG9zaXRpb25Db21tYW5kSABSBXJlc2V0QgUKA21zZw==');
|
||||
@$core.Deprecated('Use progressCommandDescriptor instead')
|
||||
const ProgressCommand$json = const {
|
||||
'1': 'ProgressCommand',
|
||||
'2': const [
|
||||
const {'1': 'progress', '3': 1, '4': 1, '5': 2, '10': 'progress'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ProgressCommand`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List progressCommandDescriptor = $convert.base64Decode('Cg9Qcm9ncmVzc0NvbW1hbmQSGgoIcHJvZ3Jlc3MYASABKAJSCHByb2dyZXNz');
|
||||
@$core.Deprecated('Use moveMotorCommandDescriptor instead')
|
||||
const MoveMotorCommand$json = const {
|
||||
'1': 'MoveMotorCommand',
|
||||
'2': const [
|
||||
const {'1': 'target', '3': 1, '4': 1, '5': 5, '10': 'target'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `MoveMotorCommand`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List moveMotorCommandDescriptor = $convert.base64Decode('ChBNb3ZlTW90b3JDb21tYW5kEhYKBnRhcmdldBgBIAEoBVIGdGFyZ2V0');
|
||||
@$core.Deprecated('Use resetMotorPositionCommandDescriptor instead')
|
||||
const ResetMotorPositionCommand$json = const {
|
||||
'1': 'ResetMotorPositionCommand',
|
||||
'2': const [
|
||||
const {'1': 'motorIndex', '3': 1, '4': 1, '5': 5, '10': 'motorIndex'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ResetMotorPositionCommand`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List resetMotorPositionCommandDescriptor = $convert.base64Decode('ChlSZXNldE1vdG9yUG9zaXRpb25Db21tYW5kEh4KCm1vdG9ySW5kZXgYASABKAVSCm1vdG9ySW5kZXg=');
|
||||
9
app/lib/message.pbserver.dart
Normal file
9
app/lib/message.pbserver.dart
Normal file
@ -0,0 +1,9 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: message.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
export 'message.pb.dart';
|
||||
|
||||
@ -38,8 +38,10 @@ dependencies:
|
||||
health: ^4.1.1
|
||||
permission_handler: ^10.0.1
|
||||
protobuf: ^2.1.0
|
||||
provider: ^6.0.0
|
||||
flutter_blue: ^0.8.0
|
||||
carousel_slider: ^4.1.1
|
||||
shared_preferences: ^2.0.15
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@ -13,7 +13,7 @@ import 'package:grow_me_app/main.dart';
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpWidget(const GrowMeApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
|
||||
61
growme/include/MotorControl.hpp
Normal file
61
growme/include/MotorControl.hpp
Normal file
@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
#include "A4988.h"
|
||||
#include "esp_log.h"
|
||||
#include "message.pb.h"
|
||||
#include "pb_decode.h"
|
||||
#include "pb_encode.h"
|
||||
|
||||
// using a 200-step motor (most common)
|
||||
#define MOTOR_STEPS 200
|
||||
|
||||
struct MotorControl {
|
||||
uint8_t index;
|
||||
A4988 *stepper;
|
||||
long stepTarget = 25;
|
||||
long currentPosition = 0;
|
||||
BLECharacteristic *bleCharacteristic = NULL;
|
||||
BLECharacteristic *bleInfoCharacteristic = NULL;
|
||||
|
||||
MotorControl(uint8_t index, short dir, short step);
|
||||
|
||||
void addControlCharacteristic(BLECharacteristic *bleCharac);
|
||||
void addInfoCharacteristic(BLECharacteristic *bleCharac);
|
||||
};
|
||||
|
||||
class CustomBLEMotorCallback : public BLECharacteristicCallbacks {
|
||||
int motorIndex;
|
||||
MotorControl *controller;
|
||||
|
||||
Command cmd = Command_init_zero;
|
||||
|
||||
void onWrite(BLECharacteristic *characteristic);
|
||||
void onRead(BLECharacteristic *ch);
|
||||
|
||||
public:
|
||||
CustomBLEMotorCallback(uint8_t motorIndex, MotorControl *controller) {
|
||||
this->motorIndex = motorIndex;
|
||||
this->controller = controller;
|
||||
};
|
||||
};
|
||||
|
||||
class CustomBLEMotorInfoCallback : public BLECharacteristicCallbacks {
|
||||
int motorIndex;
|
||||
MotorControl *controller;
|
||||
MotorStatus msg;
|
||||
|
||||
// set internal position value
|
||||
uint8_t buffer[256];
|
||||
|
||||
void onRead(BLECharacteristic *ch);
|
||||
|
||||
public:
|
||||
CustomBLEMotorInfoCallback(uint8_t motorIndex, MotorControl *controller) {
|
||||
this->motorIndex = motorIndex;
|
||||
this->controller = controller;
|
||||
};
|
||||
};
|
||||
@ -10,14 +10,28 @@
|
||||
#endif
|
||||
|
||||
/* Struct definitions */
|
||||
typedef struct _MsgSetProgress {
|
||||
typedef struct _MotorStatus {
|
||||
int32_t totalSteps;
|
||||
} MotorStatus;
|
||||
|
||||
typedef struct _MoveMotorCommand {
|
||||
int32_t target;
|
||||
} MoveMotorCommand;
|
||||
|
||||
typedef struct _ProgressCommand {
|
||||
float progress;
|
||||
} MsgSetProgress;
|
||||
} ProgressCommand;
|
||||
|
||||
typedef struct _ResetMotorPositionCommand {
|
||||
int32_t motorIndex;
|
||||
} ResetMotorPositionCommand;
|
||||
|
||||
typedef struct _Command {
|
||||
pb_size_t which_msg;
|
||||
union {
|
||||
MsgSetProgress progress;
|
||||
ProgressCommand progress;
|
||||
MoveMotorCommand move;
|
||||
ResetMotorPositionCommand reset;
|
||||
} msg;
|
||||
} Command;
|
||||
|
||||
@ -27,37 +41,76 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define MsgSetProgress_init_default {0}
|
||||
#define Command_init_default {0, {MsgSetProgress_init_default}}
|
||||
#define MsgSetProgress_init_zero {0}
|
||||
#define Command_init_zero {0, {MsgSetProgress_init_zero}}
|
||||
#define Command_init_default {0, {ProgressCommand_init_default}}
|
||||
#define ProgressCommand_init_default {0}
|
||||
#define MoveMotorCommand_init_default {0}
|
||||
#define ResetMotorPositionCommand_init_default {0}
|
||||
#define MotorStatus_init_default {0}
|
||||
#define Command_init_zero {0, {ProgressCommand_init_zero}}
|
||||
#define ProgressCommand_init_zero {0}
|
||||
#define MoveMotorCommand_init_zero {0}
|
||||
#define ResetMotorPositionCommand_init_zero {0}
|
||||
#define MotorStatus_init_zero {0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define MsgSetProgress_progress_tag 1
|
||||
#define MotorStatus_totalSteps_tag 1
|
||||
#define MoveMotorCommand_target_tag 1
|
||||
#define ProgressCommand_progress_tag 1
|
||||
#define ResetMotorPositionCommand_motorIndex_tag 1
|
||||
#define Command_progress_tag 1
|
||||
#define Command_move_tag 2
|
||||
#define Command_reset_tag 3
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define MsgSetProgress_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, progress, 1)
|
||||
#define MsgSetProgress_CALLBACK NULL
|
||||
#define MsgSetProgress_DEFAULT NULL
|
||||
|
||||
#define Command_FIELDLIST(X, a) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (msg,progress,msg.progress), 1)
|
||||
X(a, STATIC, ONEOF, MESSAGE, (msg,progress,msg.progress), 1) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (msg,move,msg.move), 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (msg,reset,msg.reset), 3)
|
||||
#define Command_CALLBACK NULL
|
||||
#define Command_DEFAULT NULL
|
||||
#define Command_msg_progress_MSGTYPE MsgSetProgress
|
||||
#define Command_msg_progress_MSGTYPE ProgressCommand
|
||||
#define Command_msg_move_MSGTYPE MoveMotorCommand
|
||||
#define Command_msg_reset_MSGTYPE ResetMotorPositionCommand
|
||||
|
||||
#define ProgressCommand_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, progress, 1)
|
||||
#define ProgressCommand_CALLBACK NULL
|
||||
#define ProgressCommand_DEFAULT NULL
|
||||
|
||||
#define MoveMotorCommand_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, INT32, target, 1)
|
||||
#define MoveMotorCommand_CALLBACK NULL
|
||||
#define MoveMotorCommand_DEFAULT NULL
|
||||
|
||||
#define ResetMotorPositionCommand_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, INT32, motorIndex, 1)
|
||||
#define ResetMotorPositionCommand_CALLBACK NULL
|
||||
#define ResetMotorPositionCommand_DEFAULT NULL
|
||||
|
||||
#define MotorStatus_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, INT32, totalSteps, 1)
|
||||
#define MotorStatus_CALLBACK NULL
|
||||
#define MotorStatus_DEFAULT NULL
|
||||
|
||||
extern const pb_msgdesc_t MsgSetProgress_msg;
|
||||
extern const pb_msgdesc_t Command_msg;
|
||||
extern const pb_msgdesc_t ProgressCommand_msg;
|
||||
extern const pb_msgdesc_t MoveMotorCommand_msg;
|
||||
extern const pb_msgdesc_t ResetMotorPositionCommand_msg;
|
||||
extern const pb_msgdesc_t MotorStatus_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define MsgSetProgress_fields &MsgSetProgress_msg
|
||||
#define Command_fields &Command_msg
|
||||
#define ProgressCommand_fields &ProgressCommand_msg
|
||||
#define MoveMotorCommand_fields &MoveMotorCommand_msg
|
||||
#define ResetMotorPositionCommand_fields &ResetMotorPositionCommand_msg
|
||||
#define MotorStatus_fields &MotorStatus_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define Command_size 7
|
||||
#define MsgSetProgress_size 5
|
||||
#define Command_size 13
|
||||
#define MotorStatus_size 11
|
||||
#define MoveMotorCommand_size 11
|
||||
#define ProgressCommand_size 5
|
||||
#define ResetMotorPositionCommand_size 11
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
11
growme/proto/app.proto
Normal file
11
growme/proto/app.proto
Normal file
@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message KnownDevicesState {
|
||||
repeated KnownDevice devices = 1;
|
||||
}
|
||||
|
||||
message KnownDevice {
|
||||
string name = 1;
|
||||
string remoteId = 2;
|
||||
string remoteName = 3;
|
||||
}
|
||||
@ -2,12 +2,27 @@ syntax = "proto3";
|
||||
|
||||
option go_package = "./proto";
|
||||
|
||||
message MsgSetProgress {
|
||||
message Command {
|
||||
oneof msg {
|
||||
ProgressCommand progress = 1;
|
||||
MoveMotorCommand move = 2;
|
||||
ResetMotorPositionCommand reset = 3;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
message ProgressCommand {
|
||||
float progress = 1;
|
||||
}
|
||||
|
||||
message Command {
|
||||
oneof msg {
|
||||
MsgSetProgress progress = 1;
|
||||
}
|
||||
message MoveMotorCommand {
|
||||
int32 target = 1;
|
||||
}
|
||||
|
||||
message ResetMotorPositionCommand {
|
||||
int32 motorIndex = 1;
|
||||
}
|
||||
|
||||
message MotorStatus {
|
||||
int32 totalSteps = 1;
|
||||
}
|
||||
@ -224,8 +224,9 @@ CONFIG_BT_ENABLED=y
|
||||
# Bluetooth controller
|
||||
#
|
||||
# CONFIG_BTDM_CTRL_MODE_BLE_ONLY is not set
|
||||
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y
|
||||
# CONFIG_BTDM_CTRL_MODE_BTDM is not set
|
||||
# CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY is not set
|
||||
CONFIG_BTDM_CTRL_MODE_BTDM=y
|
||||
CONFIG_BTDM_CTRL_BLE_MAX_CONN=3
|
||||
CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN=2
|
||||
CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN=0
|
||||
# CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_HCI is not set
|
||||
@ -238,9 +239,10 @@ CONFIG_BTDM_CTRL_PCM_POLAR_FALLING_EDGE=y
|
||||
# CONFIG_BTDM_CTRL_PCM_POLAR_RISING_EDGE is not set
|
||||
CONFIG_BTDM_CTRL_PCM_ROLE_EFF=0
|
||||
CONFIG_BTDM_CTRL_PCM_POLAR_EFF=0
|
||||
# CONFIG_BTDM_CTRL_AUTO_LATENCY is not set
|
||||
CONFIG_BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT=y
|
||||
CONFIG_BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT_EFF=y
|
||||
CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF=0
|
||||
CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF=3
|
||||
CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN_EFF=2
|
||||
CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN_EFF=0
|
||||
CONFIG_BTDM_CTRL_PINNED_TO_CORE_0=y
|
||||
@ -258,7 +260,19 @@ CONFIG_BTDM_CTRL_MODEM_SLEEP_MODE_ORIG=y
|
||||
CONFIG_BTDM_CTRL_LPCLK_SEL_MAIN_XTAL=y
|
||||
# end of MODEM SLEEP Options
|
||||
|
||||
CONFIG_BTDM_BLE_DEFAULT_SCA_250PPM=y
|
||||
CONFIG_BTDM_BLE_SLEEP_CLOCK_ACCURACY_INDEX_EFF=1
|
||||
CONFIG_BTDM_BLE_SCAN_DUPL=y
|
||||
CONFIG_BTDM_SCAN_DUPL_TYPE_DEVICE=y
|
||||
# CONFIG_BTDM_SCAN_DUPL_TYPE_DATA is not set
|
||||
# CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE is not set
|
||||
CONFIG_BTDM_SCAN_DUPL_TYPE=0
|
||||
CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE=200
|
||||
# CONFIG_BTDM_BLE_MESH_SCAN_DUPL_EN is not set
|
||||
CONFIG_BTDM_CTRL_FULL_SCAN_SUPPORTED=y
|
||||
CONFIG_BTDM_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y
|
||||
CONFIG_BTDM_BLE_ADV_REPORT_FLOW_CTRL_NUM=100
|
||||
CONFIG_BTDM_BLE_ADV_REPORT_DISCARD_THRSHOLD=20
|
||||
CONFIG_BTDM_RESERVE_DRAM=0xdb5c
|
||||
CONFIG_BTDM_CTRL_HLI=y
|
||||
# end of Bluetooth controller
|
||||
@ -276,12 +290,7 @@ CONFIG_BT_BLUEDROID_PINNED_TO_CORE_0=y
|
||||
CONFIG_BT_BLUEDROID_PINNED_TO_CORE=0
|
||||
CONFIG_BT_BTU_TASK_STACK_SIZE=4096
|
||||
# CONFIG_BT_BLUEDROID_MEM_DEBUG is not set
|
||||
CONFIG_BT_CLASSIC_ENABLED=y
|
||||
# CONFIG_BT_A2DP_ENABLE is not set
|
||||
CONFIG_BT_SPP_ENABLED=y
|
||||
# CONFIG_BT_HFP_ENABLE is not set
|
||||
# CONFIG_BT_HID_ENABLED is not set
|
||||
CONFIG_BT_SSP_ENABLED=y
|
||||
# CONFIG_BT_CLASSIC_ENABLED is not set
|
||||
CONFIG_BT_BLE_ENABLED=y
|
||||
CONFIG_BT_GATTS_ENABLE=y
|
||||
# CONFIG_BT_GATTS_PPCP_CHAR_GAP is not set
|
||||
@ -476,6 +485,7 @@ CONFIG_BT_MULTI_CONNECTION_ENBALE=y
|
||||
# CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY is not set
|
||||
# CONFIG_BT_BLE_HOST_QUEUE_CONG_CHECK is not set
|
||||
CONFIG_BT_SMP_ENABLE=y
|
||||
# CONFIG_BT_BLE_ACT_SCAN_REP_ADV_SCAN is not set
|
||||
CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT=30
|
||||
# CONFIG_BT_BLE_RPA_SUPPORTED is not set
|
||||
# end of Bluedroid Options
|
||||
@ -1194,6 +1204,7 @@ CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
|
||||
# mbedTLS v2.28.x related
|
||||
#
|
||||
# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH is not set
|
||||
CONFIG_MBEDTLS_ECDH_LEGACY_CONTEXT=y
|
||||
# CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set
|
||||
# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set
|
||||
CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y
|
||||
@ -1209,8 +1220,8 @@ CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y
|
||||
# CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE is not set
|
||||
# end of Certificate Bundle
|
||||
|
||||
# CONFIG_MBEDTLS_ECP_RESTARTABLE is not set
|
||||
# CONFIG_MBEDTLS_CMAC_C is not set
|
||||
CONFIG_MBEDTLS_ECP_RESTARTABLE=y
|
||||
CONFIG_MBEDTLS_CMAC_C=y
|
||||
CONFIG_MBEDTLS_HARDWARE_AES=y
|
||||
CONFIG_MBEDTLS_HARDWARE_MPI=y
|
||||
CONFIG_MBEDTLS_HARDWARE_SHA=y
|
||||
@ -1571,17 +1582,29 @@ CONFIG_STACK_CHECK_NONE=y
|
||||
CONFIG_ESP32_APPTRACE_DEST_NONE=y
|
||||
CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y
|
||||
# CONFIG_BTDM_CONTROLLER_MODE_BLE_ONLY is not set
|
||||
CONFIG_BTDM_CONTROLLER_MODE_BR_EDR_ONLY=y
|
||||
# CONFIG_BTDM_CONTROLLER_MODE_BTDM is not set
|
||||
# CONFIG_BTDM_CONTROLLER_MODE_BR_EDR_ONLY is not set
|
||||
CONFIG_BTDM_CONTROLLER_MODE_BTDM=y
|
||||
CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN=3
|
||||
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN=2
|
||||
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN=0
|
||||
CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN_EFF=0
|
||||
CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN_EFF=3
|
||||
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN_EFF=2
|
||||
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN_EFF=0
|
||||
CONFIG_BTDM_CONTROLLER_PINNED_TO_CORE=0
|
||||
CONFIG_BTDM_CONTROLLER_HCI_MODE_VHCI=y
|
||||
# CONFIG_BTDM_CONTROLLER_HCI_MODE_UART_H4 is not set
|
||||
CONFIG_BTDM_CONTROLLER_MODEM_SLEEP=y
|
||||
CONFIG_BLE_SCAN_DUPLICATE=y
|
||||
CONFIG_SCAN_DUPLICATE_BY_DEVICE_ADDR=y
|
||||
# CONFIG_SCAN_DUPLICATE_BY_ADV_DATA is not set
|
||||
# CONFIG_SCAN_DUPLICATE_BY_ADV_DATA_AND_DEVICE_ADDR is not set
|
||||
CONFIG_SCAN_DUPLICATE_TYPE=0
|
||||
CONFIG_DUPLICATE_SCAN_CACHE_SIZE=200
|
||||
# CONFIG_BLE_MESH_SCAN_DUPLICATE_EN is not set
|
||||
CONFIG_BTDM_CONTROLLER_FULL_SCAN_SUPPORTED=y
|
||||
CONFIG_BLE_ADV_REPORT_FLOW_CONTROL_SUPPORTED=y
|
||||
CONFIG_BLE_ADV_REPORT_FLOW_CONTROL_NUM=100
|
||||
CONFIG_BLE_ADV_REPORT_DISCARD_THRSHOLD=20
|
||||
CONFIG_BLUEDROID_ENABLED=y
|
||||
# CONFIG_NIMBLE_ENABLED is not set
|
||||
CONFIG_BTC_TASK_STACK_SIZE=3072
|
||||
@ -1590,9 +1613,7 @@ CONFIG_BLUEDROID_PINNED_TO_CORE_0=y
|
||||
CONFIG_BLUEDROID_PINNED_TO_CORE=0
|
||||
CONFIG_BTU_TASK_STACK_SIZE=4096
|
||||
# CONFIG_BLUEDROID_MEM_DEBUG is not set
|
||||
CONFIG_CLASSIC_BT_ENABLED=y
|
||||
# CONFIG_A2DP_ENABLE is not set
|
||||
# CONFIG_HFP_ENABLE is not set
|
||||
# CONFIG_CLASSIC_BT_ENABLED is not set
|
||||
CONFIG_GATTS_ENABLE=y
|
||||
# CONFIG_GATTS_SEND_SERVICE_CHANGE_MANUAL is not set
|
||||
CONFIG_GATTS_SEND_SERVICE_CHANGE_AUTO=y
|
||||
@ -1764,6 +1785,7 @@ CONFIG_BLUFI_TRACE_LEVEL_WARNING=y
|
||||
CONFIG_BLUFI_INITIAL_TRACE_LEVEL=2
|
||||
# CONFIG_BLE_HOST_QUEUE_CONGESTION_CHECK is not set
|
||||
CONFIG_SMP_ENABLE=y
|
||||
# CONFIG_BLE_ACTIVE_SCAN_REPORT_ADV_SCAN_RSP_INDIVIDUALLY is not set
|
||||
CONFIG_BLE_ESTABLISH_LINK_CONNECTION_TIMEOUT=30
|
||||
CONFIG_ADC2_DISABLE_DAC=y
|
||||
# CONFIG_SPIRAM_SUPPORT is not set
|
||||
|
||||
1372
growme/sdkconfig.esp32dev.old
Normal file
1372
growme/sdkconfig.esp32dev.old
Normal file
File diff suppressed because it is too large
Load Diff
71
growme/src/MotorControl.cpp
Normal file
71
growme/src/MotorControl.cpp
Normal file
@ -0,0 +1,71 @@
|
||||
#include "MotorControl.hpp"
|
||||
|
||||
void CustomBLEMotorCallback::onWrite(BLECharacteristic *characteristic) {
|
||||
const char *tag = "BLEMotor";
|
||||
|
||||
ESP_LOGI(tag, "onWrite for motor %d", this->motorIndex);
|
||||
// decode pb message from bluetooth characteristic
|
||||
pb_istream_t stream =
|
||||
pb_istream_from_buffer(characteristic->getData(), characteristic->getLength());
|
||||
if (pb_decode(&stream, Command_fields, &cmd)) {
|
||||
switch (cmd.which_msg) {
|
||||
case Command_reset_tag:
|
||||
ESP_LOGI(tag, "reset motor command received");
|
||||
this->controller->stepTarget = 0;
|
||||
this->controller->currentPosition = 0;
|
||||
break;
|
||||
case Command_move_tag:
|
||||
this->controller->stepTarget += cmd.msg.move.target;
|
||||
ESP_LOGI(tag,
|
||||
"received new sample %d, new target %ld",
|
||||
cmd.msg.move.target,
|
||||
this->controller->stepTarget);
|
||||
break;
|
||||
default: ESP_LOGW(tag, "received unhandled command type %d", cmd.which_msg);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const char *TAG = "MotorCallback";
|
||||
|
||||
void CustomBLEMotorCallback::onRead(BLECharacteristic *ch) {
|
||||
ESP_LOGI("MotorCallback", "onRead");
|
||||
};
|
||||
|
||||
void CustomBLEMotorInfoCallback::onRead(BLECharacteristic *characteristic) {
|
||||
// encode message in pb format
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(this->buffer, sizeof(this->buffer));
|
||||
|
||||
this->msg.totalSteps = this->controller->currentPosition;
|
||||
|
||||
if (!pb_encode(&stream, Command_fields, &this->msg)) {
|
||||
ESP_LOGE(TAG, "failed to encode: %s", PB_GET_ERROR(&stream));
|
||||
return;
|
||||
}
|
||||
|
||||
// encode latest value in characteristic
|
||||
characteristic->setValue(buffer, stream.bytes_written);
|
||||
};
|
||||
|
||||
MotorControl::MotorControl(uint8_t index, short dir, short step) {
|
||||
this->stepper = new A4988(MOTOR_STEPS, dir, step);
|
||||
this->index = index;
|
||||
};
|
||||
|
||||
void MotorControl::addControlCharacteristic(BLECharacteristic *bleCharac) {
|
||||
this->bleCharacteristic = bleCharac;
|
||||
|
||||
ESP_LOGI(TAG, "adding control characteristic for motor %d", this->index);
|
||||
|
||||
// add callback
|
||||
this->bleCharacteristic->setCallbacks(new CustomBLEMotorCallback(index, this));
|
||||
};
|
||||
|
||||
void MotorControl::addInfoCharacteristic(BLECharacteristic *bleCharac) {
|
||||
this->bleInfoCharacteristic = bleCharac;
|
||||
|
||||
ESP_LOGI(TAG, "adding info characteristic for motor %d", this->index);
|
||||
|
||||
// add callback
|
||||
this->bleInfoCharacteristic->setCallbacks(new CustomBLEMotorInfoCallback(index, this));
|
||||
};
|
||||
@ -1,19 +1,23 @@
|
||||
#include <Arduino.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <WiFi.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "A4988.h"
|
||||
#include "BluetoothCore.hpp"
|
||||
#include "MotorControl.hpp"
|
||||
#include "message.pb.h"
|
||||
#include "pb_decode.h"
|
||||
#include "pb_encode.h"
|
||||
// #include "BluetoothCore.hpp"
|
||||
// #include "BluetoothSerial.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#define UP_BTN_PIN 27
|
||||
#define DOWN_BTN_PIN 13
|
||||
|
||||
// using a 200-step motor (most common)
|
||||
#define MOTOR_STEPS 200
|
||||
|
||||
// define event bits for movement of individual motors
|
||||
#define DIR_REVERSE_BIT (1UL << 0UL)
|
||||
#define M1_BIT (1UL << 2UL)
|
||||
@ -23,16 +27,38 @@
|
||||
#define M5_BIT (1UL << 6UL)
|
||||
#define M6_BIT (1UL << 7UL)
|
||||
|
||||
// setup motors
|
||||
A4988 stepper1(MOTOR_STEPS, 22, 23);
|
||||
A4988 stepper2(MOTOR_STEPS, 18, 19);
|
||||
A4988 stepper3(MOTOR_STEPS, 16, 17);
|
||||
A4988 stepper4(MOTOR_STEPS, 32, 33);
|
||||
A4988 stepper5(MOTOR_STEPS, 25, 26);
|
||||
A4988 stepper6(MOTOR_STEPS, 14, 12);
|
||||
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
|
||||
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
|
||||
|
||||
MotorControl *motors[] = {
|
||||
new MotorControl(0, 22, 23),
|
||||
new MotorControl(1, 18, 19),
|
||||
new MotorControl(2, 16, 17),
|
||||
new MotorControl(3, 32, 33),
|
||||
new MotorControl(4, 25, 26),
|
||||
new MotorControl(5, 14, 12),
|
||||
};
|
||||
|
||||
// BLE characteristic IDs for each motor
|
||||
char *motorControlCharacteristicUUIDs[] = {
|
||||
"68023a4e-e253-46e4-a439-f716b3702ae1",
|
||||
"a9102165-7b40-4cce-9008-f4af28b5ac5e",
|
||||
"4e9cad83-71d8-40c1-8876-53c1cd5fe27e",
|
||||
"e0b49f4b-d7b0-4336-8562-41a16e16e8e6",
|
||||
"5f01e609-182f-45fe-aa23-45c12b82e2df",
|
||||
"09da8768-b6ba-4b4e-b91f-65d624581d48",
|
||||
};
|
||||
|
||||
// BLE characteristic IDs for each motor
|
||||
char *motorInfoCharacteristicUUIDs[] = {
|
||||
"9c05490f-cc74-4fd2-8d16-fb228e3f2270",
|
||||
"6b342a89-086b-4e6c-8c26-c46352e99709",
|
||||
"6729beea-61c3-4376-a8fb-9f0aab020ed0",
|
||||
"faf8581b-1085-4b45-980f-5039db39cfbe",
|
||||
"69999658-9997-4e57-8e07-e14e0faf9b32",
|
||||
"d02d9c65-df4d-42d5-abef-24f71b612aad",
|
||||
};
|
||||
|
||||
// declare a event grounp handler variable
|
||||
EventGroupHandle_t xMotorEventGroup;
|
||||
// motor movement direction
|
||||
// -1 = counter clockwise
|
||||
// 0 = nothing
|
||||
@ -42,10 +68,14 @@ int direction = 0;
|
||||
// TODO: for now use simple arduino based
|
||||
// task structure adjust as needed afterwards
|
||||
void motorTask(void *pvParameter) {
|
||||
A4988 stepper = *(A4988 *)pvParameter;
|
||||
MotorControl *m = (MotorControl *)pvParameter;
|
||||
|
||||
const char *tag = "MotorLoop";
|
||||
|
||||
long ongoingStepTarget = 0;
|
||||
|
||||
// configure motor
|
||||
stepper.begin(120, 1);
|
||||
m->stepper->begin(120, 1);
|
||||
|
||||
// track current movement of motor
|
||||
uint motorWaitTimeMicros = 0;
|
||||
@ -64,31 +94,65 @@ void motorTask(void *pvParameter) {
|
||||
// start spinning motor if:
|
||||
// - button pressed
|
||||
// - no spin command was already started
|
||||
if (direction == 0) {
|
||||
stepper.stop();
|
||||
} else {
|
||||
stepper.startRotate(direction * rotationAngle);
|
||||
if (direction != 0) {
|
||||
m->stepper->startRotate(direction * rotationAngle);
|
||||
|
||||
while (direction != 0) {
|
||||
// motor control loop - send pulse and return how long to wait until next pulse
|
||||
motorWaitTimeMicros = m->stepper->nextAction();
|
||||
if (motorWaitTimeMicros <= 0) {
|
||||
// reset execution task
|
||||
break;
|
||||
} else if (motorWaitTimeMicros >= 50) {
|
||||
vTaskDelay(pdMS_TO_TICKS(motorWaitTimeMicros / 1000));
|
||||
} else {
|
||||
// give tiny amount for RTOS scheduling
|
||||
vTaskDelay(20);
|
||||
}
|
||||
}
|
||||
m->stepper->stop();
|
||||
}
|
||||
|
||||
// motor control loop - send pulse and return how long to wait until next pulse
|
||||
motorWaitTimeMicros = stepper.nextAction();
|
||||
if (motorWaitTimeMicros <= 0) {
|
||||
vTaskDelay(noActionIdleTime);
|
||||
} else if (motorWaitTimeMicros >= 50) {
|
||||
vTaskDelay(pdMS_TO_TICKS(motorWaitTimeMicros / 1000));
|
||||
} else {
|
||||
// give tiny amount for RTOS scheduling
|
||||
vTaskDelay(20);
|
||||
// target based motor handling
|
||||
ongoingStepTarget = m->stepTarget;
|
||||
long diff = ongoingStepTarget - m->currentPosition;
|
||||
if (diff != 0) {
|
||||
ESP_LOGI(tag, "positional diff %ld", diff);
|
||||
// NOTE: maybe adjust speed here
|
||||
m->stepper->startMove(diff);
|
||||
|
||||
while (true) {
|
||||
// motor control loop - send pulse and return how long to wait until next pulse
|
||||
motorWaitTimeMicros = m->stepper->nextAction();
|
||||
if (motorWaitTimeMicros <= 0) {
|
||||
ESP_LOGI(tag, "move completed");
|
||||
// reset execution task
|
||||
break;
|
||||
} else if (motorWaitTimeMicros >= 50) {
|
||||
vTaskDelay(pdMS_TO_TICKS(motorWaitTimeMicros / 1000));
|
||||
} else {
|
||||
// give tiny amount for RTOS scheduling
|
||||
vTaskDelay(20);
|
||||
}
|
||||
}
|
||||
|
||||
m->currentPosition = ongoingStepTarget;
|
||||
}
|
||||
|
||||
vTaskDelay(noActionIdleTime);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void controlTask(void *pvParameter) {
|
||||
const TickType_t loopDelay = pdMS_TO_TICKS(50);
|
||||
|
||||
// configure button
|
||||
pinMode(UP_BTN_PIN, INPUT);
|
||||
pinMode(DOWN_BTN_PIN, INPUT);
|
||||
|
||||
// MoveMotorCommand init_cmd = MoveMotorCommand_init_zero;
|
||||
// pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
|
||||
// if (!pb_encode(&stream, MoveMotorCommand_fields, &init_cmd)) {
|
||||
// Serial.println("added initial value to BLE channel");
|
||||
// }
|
||||
|
||||
while (1) {
|
||||
// parse movement direction
|
||||
@ -104,28 +168,66 @@ void controlTask(void *pvParameter) {
|
||||
}
|
||||
}
|
||||
|
||||
bool deviceConnected = false;
|
||||
|
||||
class CustomBLEServerCallback : public BLEServerCallbacks {
|
||||
void onConnect(BLEServer *pServer) {
|
||||
deviceConnected = true;
|
||||
ESP_LOGI("MAIN", "connnected to remote");
|
||||
};
|
||||
void onDisconnect(BLEServer *pServer) {
|
||||
deviceConnected = false;
|
||||
ESP_LOGI("MAIN", "disconnected from remote");
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" void app_main() {
|
||||
// bootstrap bluetooth controller
|
||||
if (!BluetoothCore::setup()) {
|
||||
ESP_LOGI("MAIN", "BT setup failed");
|
||||
}
|
||||
// if (!BluetoothCore::setup()) {
|
||||
// ESP_LOGI("MAIN", "BT setup failed");
|
||||
// }
|
||||
|
||||
// initialize arduino library before we start the tasks
|
||||
initArduino();
|
||||
|
||||
BLEDevice::init("GrowMe-beta-1");
|
||||
BLEServer *pServer = BLEDevice::createServer();
|
||||
BLEService *pService = pServer->createService(SERVICE_UUID);
|
||||
|
||||
// create motor characteristics
|
||||
for (int i = 0; i < sizeof(motors) / sizeof(motors[0]); i++) {
|
||||
MotorControl *m = motors[i];
|
||||
BLECharacteristic *ctrl = pService->createCharacteristic(
|
||||
motorControlCharacteristicUUIDs[i],
|
||||
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
|
||||
BLECharacteristic *info = pService->createCharacteristic(motorInfoCharacteristicUUIDs[i],
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
|
||||
// register characteristic and callback methods
|
||||
m->addControlCharacteristic(ctrl);
|
||||
m->addInfoCharacteristic(info);
|
||||
}
|
||||
|
||||
pService->start();
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
pAdvertising->addServiceUUID(SERVICE_UUID);
|
||||
pAdvertising->setScanResponse(true);
|
||||
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
|
||||
pAdvertising->setMinPreferred(0x12);
|
||||
BLEDevice::startAdvertising();
|
||||
|
||||
pServer->setCallbacks(new CustomBLEServerCallback());
|
||||
|
||||
// setup serial COMs
|
||||
// Serial.begin(115200);
|
||||
|
||||
// setup event group
|
||||
xMotorEventGroup = xEventGroupCreate();
|
||||
|
||||
// xTaskCreate(&arduinoTask, "arduino_task", 8192, NULL, 5, NULL);
|
||||
xTaskCreate(&controlTask, "control_task", 8192, NULL, 3, NULL);
|
||||
xTaskCreate(&controlTask, "control_task", 4096, NULL, 3, NULL);
|
||||
|
||||
xTaskCreate(&motorTask, "motor_1_task", 4096, (void *)&stepper1, 2, NULL);
|
||||
// xTaskCreate(&motorTask, "motor_2_task", NULL, (void *)&stepper2, 3, NULL);
|
||||
// xTaskCreate(&motorTask, "motor_3_task", NULL, (void *)&stepper3, 3, NULL);
|
||||
// xTaskCreate(&motorTask, "motor_4_task", NULL, (void *)&stepper4, 3, NULL);
|
||||
// xTaskCreate(&motorTask, "motor_5_task", NULL, (void *)&stepper5, 3, NULL);
|
||||
// xTaskCreate(&motorTask, "motor_6_task", NULL, (void *)&stepper6, 3, NULL);
|
||||
xTaskCreate(&motorTask, "motor_1_task", 4096, (void *)motors[0], 2, NULL);
|
||||
xTaskCreate(&motorTask, "motor_1_task", 4096, (void *)motors[1], 2, NULL);
|
||||
xTaskCreate(&motorTask, "motor_1_task", 4096, (void *)motors[2], 2, NULL);
|
||||
xTaskCreate(&motorTask, "motor_1_task", 4096, (void *)motors[3], 2, NULL);
|
||||
xTaskCreate(&motorTask, "motor_1_task", 4096, (void *)motors[4], 2, NULL);
|
||||
xTaskCreate(&motorTask, "motor_1_task", 4096, (void *)motors[5], 2, NULL);
|
||||
}
|
||||
|
||||
@ -6,10 +6,19 @@
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
PB_BIND(MsgSetProgress, MsgSetProgress, AUTO)
|
||||
|
||||
|
||||
PB_BIND(Command, Command, AUTO)
|
||||
|
||||
|
||||
PB_BIND(ProgressCommand, ProgressCommand, AUTO)
|
||||
|
||||
|
||||
PB_BIND(MoveMotorCommand, MoveMotorCommand, AUTO)
|
||||
|
||||
|
||||
PB_BIND(ResetMotorPositionCommand, ResetMotorPositionCommand, AUTO)
|
||||
|
||||
|
||||
PB_BIND(MotorStatus, MotorStatus, AUTO)
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user