summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/data.dart108
-rw-r--r--lib/main.dart9
-rw-r--r--lib/ui.dart153
3 files changed, 209 insertions, 61 deletions
diff --git a/lib/data.dart b/lib/data.dart
index 2583d1a..365f5bc 100644
--- a/lib/data.dart
+++ b/lib/data.dart
@@ -1,23 +1,82 @@
-import 'dart:async';
-
import 'package:flutter/foundation.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
-const String brokerHostname = 'sia.xengineering.eu';
const int brokerPort = 1883;
const String topicPrefix = 'sia';
+enum MachineState {
+ init, // user does not want connection MQTT not connected
+ disconnected, // user wants connection but MQTT not (yet) connected
+ unreachable, // connected to MQTT broker but Sia server not reachable
+ reachable, // connected to MQTT and Sia server reachable
+}
+
+enum MachineEvent {
+ connect, // user wants to connect
+ disconnect, // user does not want an connection anymore
+ connected, // connection to MQTT broker established
+ disconnected, // connection to MQTT broker lost
+ reachable, // Sia server is reachable via MQTT
+ unreachable, // Sia server not reachable via MQTT
+}
+
class AppState with ChangeNotifier {
+ static const Map<MachineState, Map<MachineEvent, MachineState>> machine = <MachineState, Map<MachineEvent, MachineState>> {
+ MachineState.init: <MachineEvent, MachineState> {
+ MachineEvent.connect: MachineState.disconnected,
+ },
+ MachineState.disconnected: <MachineEvent, MachineState> {
+ MachineEvent.disconnect: MachineState.init,
+ MachineEvent.connected: MachineState.unreachable,
+ },
+ MachineState.unreachable: <MachineEvent, MachineState> {
+ MachineEvent.disconnect: MachineState.init,
+ MachineEvent.disconnected: MachineState.disconnected,
+ MachineEvent.reachable: MachineState.reachable,
+ },
+ MachineState.reachable: <MachineEvent, MachineState> {
+ MachineEvent.disconnect: MachineState.init,
+ MachineEvent.disconnected: MachineState.disconnected,
+ MachineEvent.unreachable: MachineState.unreachable,
+ },
+ };
+ MachineState state = MachineState.init;
+
Map<String, bool> contacts = <String, bool>{};
- late final MqttServerClient _client;
- bool _brokerConnected = false;
- bool get brokerConnected => _brokerConnected;
- bool _serverConnected = false;
- bool get serverConnected => _serverConnected;
+ late MqttServerClient _client;
+
+ String fqdn = '';
+
+ AppState();
- AppState() {
- _initMqtt();
+ void process(MachineEvent event) {
+ MachineState lastState = state;
+
+ Map<MachineEvent, MachineState>? transitions = machine[lastState];
+ if (transitions == null) {
+ return;
+ }
+
+ MachineState? nextState = transitions[event];
+ if (nextState == null) {
+ return;
+ }
+
+ if (nextState == lastState) {
+ return;
+ }
+
+ if (lastState == MachineState.init) {
+ _initMqtt();
+ }
+
+ if (nextState == MachineState.init) {
+ _deinitMqtt();
+ }
+
+ state = nextState;
+ notifyListeners();
}
@override
@@ -26,9 +85,9 @@ class AppState with ChangeNotifier {
super.dispose();
}
- Future<void> _initMqtt() async {
+ void _initMqtt() {
_client = MqttServerClient(
- brokerHostname,
+ fqdn,
'sia_app_${DateTime.now().millisecondsSinceEpoch}',
);
@@ -44,7 +103,7 @@ class AppState with ChangeNotifier {
_client.onAutoReconnected = _onAutoReconnected;
try {
- await _client.connect();
+ _client.connect();
} catch (e) {
_client.disconnect();
return;
@@ -53,26 +112,27 @@ class AppState with ChangeNotifier {
_client.updates?.listen(_onMessage);
}
+ void _deinitMqtt() {
+ _client.disconnect();
+ }
+
void _onConnected() {
_client.subscribe('$topicPrefix/contact/+/state', MqttQos.exactlyOnce);
_client.subscribe('$topicPrefix/server/health', MqttQos.exactlyOnce);
- _brokerConnected = true;
- notifyListeners();
+
+ process(MachineEvent.connected);
}
void _onDisconnected() {
- _brokerConnected = false;
- notifyListeners();
+ process(MachineEvent.disconnected);
}
void _onAutoReconnect() {
- _brokerConnected = false;
- notifyListeners();
+ process(MachineEvent.disconnected);
}
void _onAutoReconnected() {
- _brokerConnected = true;
- notifyListeners();
+ process(MachineEvent.connected);
}
void _onMessage(List<MqttReceivedMessage<MqttMessage>> messages) {
@@ -86,13 +146,11 @@ class AppState with ChangeNotifier {
if (topic == '$topicPrefix/server/health') {
if (payload == 'good') {
- _serverConnected = true;
- notifyListeners();
+ process(MachineEvent.reachable);
}
if (payload == 'bad') {
- _serverConnected = false;
- notifyListeners();
+ process(MachineEvent.unreachable);
}
}
diff --git a/lib/main.dart b/lib/main.dart
index 1601965..b636f29 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,14 +1,7 @@
import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
import 'ui.dart';
-import 'data.dart';
void main() {
- runApp(
- ChangeNotifierProvider<AppState>(
- create: (BuildContext context) => AppState(),
- child: const UI(),
- ),
- );
+ runApp(const Sia());
}
diff --git a/lib/ui.dart b/lib/ui.dart
index b9f82d0..9603562 100644
--- a/lib/ui.dart
+++ b/lib/ui.dart
@@ -3,27 +3,96 @@ import 'package:provider/provider.dart';
import 'data.dart';
-class UI extends StatelessWidget {
- const UI({super.key});
+class Sia extends StatelessWidget {
+ const Sia({super.key});
@override
Widget build(BuildContext context) {
- return MaterialApp(
- home: Scaffold(
- appBar: AppBar(title: const Text("Contacts")),
- body: const Column(
- children: <Widget>[
- Expanded(child: ContactList()),
- ],
- ),
- bottomNavigationBar: const SafeArea(
- child: ConnectionStatus(),
+ return ChangeNotifierProvider<AppState>(
+ create: (BuildContext context) => AppState(),
+ child: MaterialApp(
+ home: Consumer<AppState>(
+ builder: (_, AppState provider, _) {
+ if (provider.state == MachineState.init) {
+ return const ConnectionPage();
+ }
+ return const DevicesPage();
+ }
),
),
);
}
}
+class ConnectionPage extends StatefulWidget {
+ const ConnectionPage({super.key});
+
+ @override
+ State<ConnectionPage> createState() => _ConnectionPageState();
+}
+
+class _ConnectionPageState extends State<ConnectionPage> {
+ late TextEditingController controller;
+
+ @override
+ void initState() {
+ super.initState();
+
+ final AppState provider = context.read<AppState>();
+ controller = TextEditingController(text: provider.fqdn);
+
+ controller.addListener(() {
+ provider.fqdn = controller.text;
+ });
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Consumer<AppState>(
+ builder: (_, AppState state, _) {
+ return Scaffold(
+ appBar: AppBar(title: const Text("Connection")),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: TextField(
+ controller: controller,
+ decoration: const InputDecoration(
+ labelText: "Server name",
+ hintText: "iot.example.org",
+ border: OutlineInputBorder(),
+ ),
+ ),
+ ),
+ bottomNavigationBar: const ConnectionStatus(),
+ );
+ }
+ );
+ }
+}
+
+class DevicesPage extends StatelessWidget {
+ const DevicesPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text("Contacts")),
+ body: const Column(
+ children: <Widget>[
+ Expanded(child: ContactList()),
+ ],
+ ),
+ bottomNavigationBar: const ConnectionStatus(),
+ );
+ }
+}
+
class ContactList extends StatelessWidget {
const ContactList({super.key});
@@ -56,24 +125,52 @@ class ConnectionStatus extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Consumer<AppState>(
- builder: (BuildContext context, AppState state, Widget? child) {
- Icon icon;
- Text text;
+ return SafeArea(
+ child: Consumer<AppState>(
+ builder: (BuildContext context, AppState state, Widget? child) {
+ Icon icon = const Icon(Icons.cloud_off, color: Colors.grey);
+ Text text = const Text('Disconnected');
- if (state.brokerConnected && state.serverConnected) {
- icon = const Icon(Icons.cloud, color: Colors.green);
- text = const Text('Connected');
- } else if (state.brokerConnected && !state.serverConnected) {
- icon = const Icon(Icons.cloud_off, color: Colors.orange);
- text = const Text('Connection issue');
- } else {
- icon = const Icon(Icons.cloud_off, color: Colors.red);
- text = const Text('Disconnected');
- }
+ switch (state.state) {
+ case MachineState.init:
+ icon = const Icon(Icons.cloud_off, color: Colors.grey);
+ text = const Text('Off');
+ case MachineState.disconnected:
+ icon = const Icon(Icons.cloud_off, color: Colors.red);
+ text = const Text('Disconnected');
+ case MachineState.unreachable:
+ icon = const Icon(Icons.cloud_off, color: Colors.orange);
+ text = const Text('Unreachable');
+ case MachineState.reachable:
+ icon = const Icon(Icons.cloud, color: Colors.green);
+ text = const Text('Connected');
+ }
- return ListTile(leading: icon, title: text);
- },
+ MachineEvent event = MachineEvent.disconnect;
+ String action = 'disconnect';
+ if (state.state == MachineState.init) {
+ event = MachineEvent.connect;
+ action = 'connect';
+ }
+
+ return Row(
+ children: <Widget>[
+ Expanded(
+ child: ListTile(
+ leading: icon,
+ title: text,
+ trailing: ElevatedButton(
+ onPressed: () {
+ state.process(event);
+ },
+ child: Text(action),
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ ),
);
}
}