A Boat Instrument for displaying data in fully configurable Boxes. The data is received via a subscription to a SignalK server.
For first-time users see the installation instructions.
The Boat Instrument is built on the Flutter framework and therefore runs on all Flutter supported platforms. It has been mainly tested on an Android Tablet, but all the following screenshots show it functioning on other platforms.
The main functionality will work on all platforms, but functions depending upon platform plugins may not work as expected, e.g. updating the screen brightness.
The configuration is stored in JSON format and can be exported/imported between instruments.
Phone:
Tablet:
Tablet tiled:
Debian:
Raspberry Pi 4 - X-Windows:
Note: Screenshot taken via VNC on a tablet.
Raspberry PI 4 - Flutter-Pi:
Note: working on an HDMI monitor without X-Windows/Wayland.
Ventura:
Windows 11:
New Boxes can be added by sub-classing the BoxWidget. A simple example is:
class MyNewBox extends BoxWidget {
// The config includes access to the instrument controller, widget sizes and settings.
const MyNewBox(super.config, {super.key});
@override
State<MyNewBox> createState() => _MyNewBoxBoxState();
// This value must be unique across all Boxes.
static String sid = 'my-value';
@override
String get id => sid;
}
class _MyNewBoxState extends State<MyNewBox> {
double? _myValue;
@override
void initState() {
super.initState();
// Configure the controller to request updates to the Signalk path(s) you require.
// Note: this MUST be called even if not subscribing to any Signalk data
widget.config.controller.configure(onUpdate: _onUpdate, paths: {'signalk.path.for.data'});
}
@override
Widget build(BuildContext context) {
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text('My Value'),
Text('$_myValue')
]);
}
// This will be called with updates to the Signalk paths you've asked for.
// This simple case only asks for one path, so it assumes that's what's sent.
_onUpdate(List<Update>? updates) {
if(updates == null) {
_myValue = null;
} else {
try {
_myValue = (updates[0].value as num).toDouble();
} catch (e) {
widget.config.controller.l.e("Error converting $updates", error: e);
}
}
if(mounted) {
setState(() {});
}
}
}
Once defined, the new Box must be added to the configuration menu in the Edit Page:
List<BoxDetails> boxDetails = [
BoxDetails(BlankBox.sid, 'Blank', (config) {return BlankBox(config, key: UniqueKey());}), // This is the default Box.
BoxDetails(HelpBox.sid, 'Help', (config) {return HelpBox(config, key: UniqueKey());}), // This is the default Box.
BoxDetails(DepthBox.sid, 'Depth', (config) {return DepthBox(config, key: UniqueKey());}),
BoxDetails(MyNewBox.sid, 'My Value', (config) {return MyNewBox(config, GaugeOrientation.down, key: UniqueKey());}),
];
//<snip...>
_getWidgetMenus(_Box box) {
List<PopupMenuEntry<BoxDetails>> popupMenuEntries = [
_widgetMenuEntry(BlankBox.sid),
_widgetSubMenuEntry(box, 'Environment', [
_widgetMenuEntry(DepthBox.sid),
_widgetMenuEntry(WaterTemperatureBox.sid)]),
_widgetMenuEntry(MyNewBox.sid)
];
return popupMenuEntries;
}
Boxes can have Box type and/or Box instance settings. The DateTimeBox is a good example of a Box that has both Box Type and per-Box settings.
If the new Box is to display a single value, consider subclassing the DoubleValueBox. There are also base classes for Circular, Semi-Circular and Bar Gauges, see gauge_box.dart.
For Boxes with settings, the JSON serialising code should be generated with:
dart run build_runner build --delete-conflicting-outputs