diff options
| -rw-r--r-- | lib/data.dart | 108 | ||||
| -rw-r--r-- | lib/main.dart | 9 | ||||
| -rw-r--r-- | lib/ui.dart | 153 |
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), + ), + ), + ), + ], + ); + }, + ), ); } } |
