bobaflu - programming accessories on flutter
This article will focus on the implementation of the Flutter mobile client.
Which mobile client?
The previous publication described the system of software accessories:
bobaoskit - accessories, dnssd and WebSocket .
An analogue of a software accessory is a real object. Light bulb, switch, cd / cassette player, radio player, thermostat, temperature sensor, motion sensor, etc. ... A set of accessories is determined by imagination and program code. You can implement at least a chessboard. For such a board, you need to have a control field ( control
) move
that takes an object { from: "e2", to: "e4" }
for example and service fields for resetting figures, etc. ... The accessory script will process the request for managing the field move
, decide whether it is possible to move the figure, and return (or not) the status with the position of the figures all over the field.
Currently supported types of accessories with minimal functionality are as follows: "switch", "temperature sensor", "thermostat", "radio player".
About chess, there will be no further discussion. If it is interesting and in that case, welcome to cat.
So up and bobaoskit.worker
running. Accessory objects exist in the computer’s memory, you can read information about them, you can manually send a JSON
request to the WebSocket
port, see incoming events.
For management, I made the simplest mobile application.
Why flutter?
For the last couple of years, an idea has been actively living in my head to study programming for mobile devices. Since I write in JavaScript
, I studied solutions that allow you not to learn a new programming language.
Appcelerator
. He began the study with him. If the memory does not change, the SDK is open, but the IDE with various tariffs. NativeScript
. Here I have already created a simple application showing a list with pictures. It didn’t go further. ReactNative
. The longest assault of the frameworks listed so far. The biggest challenge is getting started. I looked at the course. At first it’s clear, interesting, it turns out. But redux
after that it was not possible to overpower. Then he regularly tried to start writing, but redux
stubbornly did not allow himself to overpower.
As a result, I was not attached to any decision then (end of 2016). Perhaps because there was no specific task, perhaps for other reasons.
Closer to the fall of the past (2018), work was already underway on the sdk for software accessories. Naturally, you need a mobile application. It all started with mdns. Once in my spare time I updated ReactNative, found the react-native-zeroconf plugin, created the application. According to the instructions installed, made link
, launched. The Expo debugging application launched, which does not support native modules, and, accordingly, the mdns plugin did not work. At this point, there was not enough free time to create a clean (without expo) react-native application and test with it. The work was postponed for a couple of months.
At the same time, more and more materials appeared about flutter
on the network. I installed myself. Installation is simple: git clone
and add in PATH
. The rest is already setting up the Android SDK / Xcode (in my case, the Android SDK has been configured for a long time. I can’t develop for iOS, because I am not a macOS user) and the Dart SDK (you can install it separately, but not necessarily, since it is part of flutter).
Principle / scheme of work
- When launched, the application searches for services
_bobaoskit._tcp
on the local network using the flutter_mdns plugin . There are several versions of this plugin, all take their roots from the published one , but it is not compatible with new versions of the Dart SDK, respectively, many forked and added compatibility. I chose this version because the others did not resolve the hosts of several discovered services at once.
Upon detection and determination (onResolve), the host is added to the list.
A page with a list of detected services -StatefulWidget
accordingly, when a service is discovered / lost, it is calledsetState() {...}
- When you select a host from the list, a new page (also
StatefulWidget
), which is transmittedhost
andport
the selected service.
The objectBobaosKit
responsible for communication is created. Responses are processed through callbacks, as while I did not study much asynchronous dart. But judging by the scanned documentation, itFutures
’s an analoguePromise
in JS.
Functions are recorded for incoming events (no responses). Looked hereEventEmitter
for Dart. I wrote my very simple one.
void registerListener(String name, Function cb) {
this._events.add(new BobaosKitCallback(name, cb));
}
void removeAllListeners() {
this._events = [];
}
void emitEvent(String name, dynamic params) {
// call all listeners
List foundCallbacks =
this._events.where((t) => t.name == name).toList();
foundCallbacks.forEach((f) => f.cb(params));
}
...
...
void listen() {
this.ws.listen((text) {
var json = jsonDecode(text);
if (json.containsKey('response_id')) {
....
} else {
// без поля response_id - событие
this.emitEvent(json['method'], json['payload']);
}
});
}
Incoming events - if the accessory has been removed, added, updated status. Or if all accessories are removed ( clear accessories
).
Registered functions - for updating lists, widgets for these events.
- A request is sent for information about all the accessories.
An object is created for each accessory AccessoryInfo
:
import 'package:scoped_model/scoped_model.dart';
// AccessoryInfo extends Model
// so, when accessory value is updated it descends down to
// all widgets inside ScopedModelDescendant
class AccessoryInfo extends Model {
dynamic id;
dynamic type;
String name;
String job_channel;
List control;
List status;
bool selected;
Map currentState;
AccessoryInfo(Map obj) {
this.id = obj['id'];
this.type = obj['type'];
this.name = obj['name'];
this.job_channel = obj['job_channel'];
this.control = obj['control'];
this.status = obj['status'];
this.currentState = {};
}
void updateCurrentState(key, value) {
currentState[key] = value;
notifyListeners();
}
void notify() {
notifyListeners();
}
}
this object is already a model. Initially, I wrote everywhere StatefulWidget
and setState() {}
, but setState() {}
it only works for a widget inside which listeners are registered. But for detailed accessory management, I created initially new Stateful
pages, and noticed that the status is not updated. As a solution, I used it ScopedModel
.
After the list of accessories is received, for each of them we send a request for status and add it to the list . We call , thus adding a supported accessory to the interface. Supported accessory types are defined in and in . While supported . The main work ahead is to add new ones and improve existing widgets.List
setState() {}
ListView.builder
./lib/widgets/*.dart
switch/temperature sensor/radio player/thermostat
- Now about how to create separate elements for each accessory. For example, consider a switch.
@override
Widget build(BuildContext context) {
return new ScopedModel(
model: info,
child: ScopedModelDescendant(
builder: (context, child, model) {
var cardColor = Theme.of(context).cardColor;
dynamic switchState = model.currentState['state'];
if (switchState is bool) {
if (switchState) {
cardColor = Colors.deepPurple;
} else {
cardColor = Theme.of(context).cardColor;
}
}
return Card(
color: cardColor,
child: ListTile(
selected: false,
leading: new Icon(Icons.lightbulb_outline),
title: new Text("${model.name}"),
onTap: () {
// to control accessory value
// get status value at first
bobaos.getStatusValue(
model.id, "state", (bool err, Object payload) {
if (err) {
return print('error ocurred $payload');
}
if (payload is Map) {
dynamic currentValue = payload['status']['value'];
bool newValue;
if (currentValue is bool) {
// invert
newValue = !currentValue;
} else {
newValue = false;
}
// then send new value
bobaos.controlAccessoryValue(
model.id, {"state": newValue}, (bool err, Object payload) {
if (err) {
return print('error ocurred $payload');
}
});
}
});
},
onLongPress: () {
// TODO: dialog with additional funcs
},
));
}));
}
For an accessory of type switch
, an element is created in the general list of accessories, when interacting with which (onTap) a request is sent to obtain the current value, then to switch this value. ScopedModel
allows you to redraw the widget with incoming status updates.
A long click handler is not implemented for this accessory.
For a radio player, it looks like this:
onLongPress: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => AccRadioPlayerControl(
info: info,
bobaos: bobaos,
)));
},
A page opens AccRadioPlayerControl
that also uses ScopedModel
state management.
On this, the entire description of the program operation algorithm is exhausted. No additional features, such as remembering the last host, sorting accessories into categories / rooms are not implemented. At the moment, everything is simple.
Problems
I will describe the main problem that is now. I did not understand how to detect a broken WebSocket
connection.
I use: WebSocket class .
When the application / device is in sleep mode for a long time, the connection is disconnected. You have to go back to the very first page and reopen the discovered service.
Afterword
On the one hand, Flutter is pretty fast at learning and developing. ScopedModel turned out to be more understandable to me than redux.
Dart turned out to be similar to familiar JavaScript. Typing + dynamic types will allow everyone to write as conveniently.
Difficulties in writing code: large nesting of widgets. The well-known callback hell after flutter looks differently. Vim-mode and %
will be useful.
Now a few thoughts about IoT. Recently, more and more smart devices / services that require registration in the cloud. Chinese outlets, for the use of which you need to install the application, create an account, and only after that you can use it.
Voice assistants. Alice from Yandex requires her cloud into which the recognized text is sent. Amazon's Alexa works in a similar fashion.
The most successful, in my opinion, is made by Apple HomeKit in conjunction with Siri. The cloud is used for text recognition. Interaction with devices - in the local network.
My opinion is that the cloud must exist for its purpose: remote control, updating, etc. ... If the device can be controlled on a local network, then you need to do this.
References
- Application repository
- Bobaoskit documentation - describes how to install bobaoskit.worker and run the accessory
radio player
.