feat: enable full app motor controlls

This commit is contained in:
Wlad Meixner 2022-09-29 00:08:55 +02:00
parent 8f1779fe38
commit 14ee41e4c2
30 changed files with 3267 additions and 469 deletions

View File

@ -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
View 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
View 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
View 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');

View 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';

View 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);
});
}

View 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)),
),
);
}
}

View 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,
);
}
}

View 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),
],
));
},
));
}
}

View 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),
);
});
}
}

View 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),
);
}

View 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()));
}
}

View 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,
));
}

View 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();
}

View File

@ -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
View 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);
}

View 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

View 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=');

View 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';

View File

@ -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:

View File

@ -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);

View 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;
};
};

View File

@ -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
View File

@ -0,0 +1,11 @@
syntax = "proto3";
message KnownDevicesState {
repeated KnownDevice devices = 1;
}
message KnownDevice {
string name = 1;
string remoteId = 2;
string remoteName = 3;
}

View File

@ -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;
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View 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));
};

View File

@ -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);
}

View File

@ -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)