From 6cb5a471a1bb18de802006c1017839c00d1871ad Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 6 Mar 2026 21:45:43 +0100 Subject: Add class ConnectionPage This is a second page to offer a form to set the Sia server name. --- lib/data.dart | 1 + 1 file changed, 1 insertion(+) (limited to 'lib/data.dart') diff --git a/lib/data.dart b/lib/data.dart index 2583d1a..31e48b1 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -9,6 +9,7 @@ const int brokerPort = 1883; const String topicPrefix = 'sia'; class AppState with ChangeNotifier { + bool onConnectionPage = false; Map contacts = {}; late final MqttServerClient _client; bool _brokerConnected = false; -- cgit v1.3 From e75d23db9960c6060fd2b530f5878727e0caaf37 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 7 Mar 2026 11:45:34 +0100 Subject: Allow switching between pages This prepares manually starting the connection after server name insertion. --- lib/data.dart | 5 +++++ lib/ui.dart | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) (limited to 'lib/data.dart') diff --git a/lib/data.dart b/lib/data.dart index 31e48b1..5e90b0e 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -123,4 +123,9 @@ class AppState with ChangeNotifier { return null; } } + + void togglePage() { + onConnectionPage = !onConnectionPage; + notifyListeners(); + } } diff --git a/lib/ui.dart b/lib/ui.dart index 20df8f5..73e532a 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -112,7 +112,20 @@ class ConnectionStatus extends StatelessWidget { text = const Text('Disconnected'); } - return ListTile(leading: icon, title: text); + return Row( + children: [ + Expanded( + child: ListTile( + leading: icon, + title: text, + trailing: ElevatedButton( + onPressed: state.togglePage, + child: const Text('switch view'), + ), + ), + ), + ], + ); }, ), ); -- cgit v1.3 From a46cb6e94688a65a31248bc4585bccfbc22242f0 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 7 Mar 2026 11:53:18 +0100 Subject: Start by default on ConnectionPage This prepares starting connection attempts on manual action only. --- lib/data.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/data.dart') diff --git a/lib/data.dart b/lib/data.dart index 5e90b0e..e660689 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -9,7 +9,7 @@ const int brokerPort = 1883; const String topicPrefix = 'sia'; class AppState with ChangeNotifier { - bool onConnectionPage = false; + bool onConnectionPage = true; Map contacts = {}; late final MqttServerClient _client; bool _brokerConnected = false; -- cgit v1.3 From 2e15dd323b7c3198e2af10f35f1186d91cc6cfaf Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 7 Mar 2026 13:03:24 +0100 Subject: Label button with '(dis)connect' This does not yet work but the correct text based on the page is displayed. This is based on a state machine based implementation suitable to implement the state handling cleanly. --- lib/data.dart | 41 +++++++++++++++++++++++++++++++++++------ lib/ui.dart | 15 ++++++++++++--- 2 files changed, 47 insertions(+), 9 deletions(-) (limited to 'lib/data.dart') diff --git a/lib/data.dart b/lib/data.dart index e660689..53c3d01 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -8,8 +8,27 @@ 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 +} + +enum MachineEvent { + connect, // user wants to connect + disconnect, // user does not want an connection anymore +} + class AppState with ChangeNotifier { - bool onConnectionPage = true; + static const Map> machine = > { + MachineState.init: { + MachineEvent.connect: MachineState.disconnected, + }, + MachineState.disconnected: { + MachineEvent.disconnect: MachineState.init, + }, + }; + MachineState state = MachineState.init; + Map contacts = {}; late final MqttServerClient _client; bool _brokerConnected = false; @@ -21,6 +40,21 @@ class AppState with ChangeNotifier { _initMqtt(); } + void process(MachineEvent event) { + Map? ruleset = machine[state]; + if (ruleset == null) { + return; + } + + MachineState? nextMachineState = ruleset[event]; + if (nextMachineState == null) { + return; + } + + state = nextMachineState; + notifyListeners(); + } + @override void dispose() { _client.disconnect(); @@ -123,9 +157,4 @@ class AppState with ChangeNotifier { return null; } } - - void togglePage() { - onConnectionPage = !onConnectionPage; - notifyListeners(); - } } diff --git a/lib/ui.dart b/lib/ui.dart index 73e532a..e14895b 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -13,7 +13,7 @@ class Sia extends StatelessWidget { child: MaterialApp( home: Consumer( builder: (_, AppState provider, _) { - if (provider.onConnectionPage) { + if (provider.state == MachineState.init) { return const ConnectionPage(); } return const DevicesPage(); @@ -112,6 +112,13 @@ class ConnectionStatus extends StatelessWidget { text = const Text('Disconnected'); } + MachineEvent event = MachineEvent.disconnect; + String action = 'disconnect'; + if (state.state == MachineState.init) { + event = MachineEvent.connect; + action = 'connect'; + } + return Row( children: [ Expanded( @@ -119,8 +126,10 @@ class ConnectionStatus extends StatelessWidget { leading: icon, title: text, trailing: ElevatedButton( - onPressed: state.togglePage, - child: const Text('switch view'), + onPressed: () { + state.process(event); + }, + child: Text(action), ), ), ), -- cgit v1.3 From 796a2b21e8b5989187bfc7d4ddb7ba648038cbca Mon Sep 17 00:00:00 2001 From: xengineering Date: Sat, 7 Mar 2026 16:35:13 +0100 Subject: Remove automatic initial connection The user should provide the fully qualified domain name (FQDN) of the MQTT broker manually and then press connect to actually connect including automated re-connects. As a first step the initial connect is bound to the manual button in the bottom bar. To reduce the scope the disconnect button is labeled "(disabled)" to make clear this is not expected to work. --- lib/data.dart | 33 ++++++++++++++++++--------------- lib/ui.dart | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) (limited to 'lib/data.dart') diff --git a/lib/data.dart b/lib/data.dart index 53c3d01..89d8760 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_server_client.dart'; @@ -23,9 +21,6 @@ class AppState with ChangeNotifier { MachineState.init: { MachineEvent.connect: MachineState.disconnected, }, - MachineState.disconnected: { - MachineEvent.disconnect: MachineState.init, - }, }; MachineState state = MachineState.init; @@ -36,22 +31,30 @@ class AppState with ChangeNotifier { bool _serverConnected = false; bool get serverConnected => _serverConnected; - AppState() { - _initMqtt(); - } + AppState(); void process(MachineEvent event) { - Map? ruleset = machine[state]; - if (ruleset == null) { + MachineState lastState = state; + + Map? transitions = machine[lastState]; + if (transitions == null) { return; } - MachineState? nextMachineState = ruleset[event]; - if (nextMachineState == null) { + MachineState? nextState = transitions[event]; + if (nextState == null) { return; } - state = nextMachineState; + if (nextState == lastState) { + return; + } + + if (lastState == MachineState.init) { + _initMqtt(); + } + + state = nextState; notifyListeners(); } @@ -61,7 +64,7 @@ class AppState with ChangeNotifier { super.dispose(); } - Future _initMqtt() async { + void _initMqtt() { _client = MqttServerClient( brokerHostname, 'sia_app_${DateTime.now().millisecondsSinceEpoch}', @@ -79,7 +82,7 @@ class AppState with ChangeNotifier { _client.onAutoReconnected = _onAutoReconnected; try { - await _client.connect(); + _client.connect(); } catch (e) { _client.disconnect(); return; diff --git a/lib/ui.dart b/lib/ui.dart index e14895b..1d2ad53 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -113,7 +113,7 @@ class ConnectionStatus extends StatelessWidget { } MachineEvent event = MachineEvent.disconnect; - String action = 'disconnect'; + String action = '(disabled)'; if (state.state == MachineState.init) { event = MachineEvent.connect; action = 'connect'; -- cgit v1.3 From ef8877ccffe0ebcc8f9f1c0bb6db372fa0886c2a Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 8 Mar 2026 16:49:16 +0100 Subject: Add manual disconnect This allows going back to the connection page and to choose a different server. Furthermore it might be useful to force a new connection attempt while auto-reconnect might be in a delay state. --- lib/data.dart | 13 ++++++++++++- lib/ui.dart | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'lib/data.dart') diff --git a/lib/data.dart b/lib/data.dart index 89d8760..415e61e 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -21,11 +21,14 @@ class AppState with ChangeNotifier { MachineState.init: { MachineEvent.connect: MachineState.disconnected, }, + MachineState.disconnected: { + MachineEvent.disconnect: MachineState.init, + }, }; MachineState state = MachineState.init; Map contacts = {}; - late final MqttServerClient _client; + late MqttServerClient _client; bool _brokerConnected = false; bool get brokerConnected => _brokerConnected; bool _serverConnected = false; @@ -54,6 +57,10 @@ class AppState with ChangeNotifier { _initMqtt(); } + if (nextState == MachineState.init) { + _deinitMqtt(); + } + state = nextState; notifyListeners(); } @@ -91,6 +98,10 @@ 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); diff --git a/lib/ui.dart b/lib/ui.dart index 1d2ad53..e14895b 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -113,7 +113,7 @@ class ConnectionStatus extends StatelessWidget { } MachineEvent event = MachineEvent.disconnect; - String action = '(disabled)'; + String action = 'disconnect'; if (state.state == MachineState.init) { event = MachineEvent.connect; action = 'connect'; -- cgit v1.3 From 4bc034c7ab627c50aba7e2ce4da360c14465c150 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 8 Mar 2026 17:27:52 +0100 Subject: Switch completely to connection state machine This makes use of the state machine in the UI and implements all states and transitions planned so far. --- lib/data.dart | 40 ++++++++++++++++++++++++---------------- lib/ui.dart | 26 +++++++++++++++----------- 2 files changed, 39 insertions(+), 27 deletions(-) (limited to 'lib/data.dart') diff --git a/lib/data.dart b/lib/data.dart index 415e61e..eaf301a 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -9,11 +9,17 @@ 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 { @@ -23,16 +29,23 @@ class AppState with ChangeNotifier { }, MachineState.disconnected: { MachineEvent.disconnect: MachineState.init, + MachineEvent.connected: MachineState.unreachable, + }, + MachineState.unreachable: { + MachineEvent.disconnect: MachineState.init, + MachineEvent.disconnected: MachineState.disconnected, + MachineEvent.reachable: MachineState.reachable, + }, + MachineState.reachable: { + MachineEvent.disconnect: MachineState.init, + MachineEvent.disconnected: MachineState.disconnected, + MachineEvent.unreachable: MachineState.unreachable, }, }; MachineState state = MachineState.init; Map contacts = {}; late MqttServerClient _client; - bool _brokerConnected = false; - bool get brokerConnected => _brokerConnected; - bool _serverConnected = false; - bool get serverConnected => _serverConnected; AppState(); @@ -105,23 +118,20 @@ class AppState with ChangeNotifier { 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> messages) { @@ -135,13 +145,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/ui.dart b/lib/ui.dart index e14895b..7a97f32 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -98,18 +98,22 @@ class ConnectionStatus extends StatelessWidget { return SafeArea( child: Consumer( builder: (BuildContext context, AppState state, Widget? child) { - Icon icon; - Text text; + 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'); } MachineEvent event = MachineEvent.disconnect; -- cgit v1.3 From 6ee59e8c2aaf69951f6f80003c23f9cd44976ce1 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 8 Mar 2026 17:53:53 +0100 Subject: Implement server selection This lets the user select the fully qualified domain name (FQDN) of the MQTT broker. The FQDN is not yet preserved after a manual disconnect. --- lib/data.dart | 5 +++-- lib/ui.dart | 31 +++++++++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) (limited to 'lib/data.dart') diff --git a/lib/data.dart b/lib/data.dart index eaf301a..365f5bc 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -2,7 +2,6 @@ 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'; @@ -47,6 +46,8 @@ class AppState with ChangeNotifier { Map contacts = {}; late MqttServerClient _client; + String fqdn = ''; + AppState(); void process(MachineEvent event) { @@ -86,7 +87,7 @@ class AppState with ChangeNotifier { void _initMqtt() { _client = MqttServerClient( - brokerHostname, + fqdn, 'sia_app_${DateTime.now().millisecondsSinceEpoch}', ); diff --git a/lib/ui.dart b/lib/ui.dart index 7a97f32..a191f5f 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -29,19 +29,26 @@ class ConnectionPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("Connection")), - body: const Padding( - padding: EdgeInsets.all(16.0), - child: TextField( - decoration: InputDecoration( - labelText: "Server name", - hintText: "iot.example.org", - border: OutlineInputBorder(), + return Consumer( + builder: (_, AppState state, _) { + return Scaffold( + appBar: AppBar(title: const Text("Connection")), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + decoration: const InputDecoration( + labelText: "Server name", + hintText: "iot.example.org", + border: OutlineInputBorder(), + ), + onChanged: (String value) { + state.fqdn = value; + }, + ), ), - ), - ), - bottomNavigationBar: const ConnectionStatus(), + bottomNavigationBar: const ConnectionStatus(), + ); + } ); } } -- cgit v1.3