Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ Arduino library for the Open Acidification pH Stat Controller

* Use [`extras/scripts/install.sh`](extras/scripts/install.sh) to do the initial install.
* After that use [`extras/scripts/testAndBuild.sh`](extras/scripts/testAndBuild.sh) to test.
* * See [this video](https://youtu.be/ZYNnVE4LnCg) for an install and test example.
* See [this video](https://youtu.be/ZYNnVE4LnCg) for an install and test example.
* Push a tag with 'v' in order to build a release
* * Use Ansible to install on [oap.cs.wallawalla.edu](oap.cs.wallawalla.edu).
* Use Ansible to install on [oap.cs.wallawalla.edu](oap.cs.wallawalla.edu).
* To build the GUI simulator, see [GUI/build.sh](GUI/build.sh).
* Use the [`extras/scripts/tasks.json`](extras/scripts/tasks.json) file to easily start the development servers in Visual Studio Code.
* Copy the JSON file to your [`.vscode`](.vscode) directory
* Open the Command Palette (Ctrl+Shift+P), select "Tasks: Run Task", and choose "Start All Servers". Alternatively, you can set up a keyboard shortcut to run this task.
* Ensure you choose a device in the Flutter client terminal.
* Navigate to http://localhost:8080/ to view the app.


### Install

Expand Down
128 changes: 88 additions & 40 deletions extras/log_file_client/lib/components/csv_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ class CsvView extends StatelessWidget {

final String csvPath;
final HttpClient httpClient;
late final Future<List<List<dynamic>>> csvTable = getCsvTable();
late final Future<List<LogDataLine>> logData = getLogData();

Future<List<List<dynamic>>> getCsvTable() async {
final table = await httpClient.getCsvTable(csvPath);
return table;
Future<List<LogDataLine>> getLogData() async {
final table = await httpClient.getLogData(csvPath);
return table!;
}

@override
Widget build(BuildContext context) {
return FutureBuilder<List>(
future: csvTable,
return FutureBuilder<List<LogDataLine>>(
future: logData, // Assuming this fetches a List<LogDataLine>
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
Expand All @@ -27,67 +27,115 @@ class CsvView extends StatelessWidget {
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No data found'));
} else {
final csvTable = snapshot.data!;
final logData = snapshot.data!;
return Column(
children: [
// Headers
Container(
padding: EdgeInsets.all(5),
padding: const EdgeInsets.all(5),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey)),
),
child: Row(
children: csvTable[0].asMap().entries.map<Widget>((entry) {
final int idx = entry.key;
final header = entry.value;
return Expanded(
flex: idx == 0
? 2
: 1, // Allow more space for the first column for the datetimes
children: const [
Expanded(
flex: 2,
child: Text(
header.toString(),
style: const TextStyle(fontStyle: FontStyle.italic),
'time',
style: TextStyle(fontStyle: FontStyle.italic),
),
);
}).toList(),
),
Expanded(
child: Text(
'tankid',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
Expanded(
child: Text(
'temp',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
Expanded(
child: Text(
'temp setpoint',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
Expanded(
child: Text(
'pH',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
Expanded(
child: Text(
'pH setpoint',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
Expanded(
child: Text(
'onTime',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
Expanded(
child: Text(
'Kp',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
Expanded(
child: Text(
'Ki',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
Expanded(
child: Text(
'Kd',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
],
),
),

// Data
Expanded(
child: ListView.builder(
// Cutoff at 5000 lines to avoid long wait times
itemCount: csvTable.length > 5000
? csvTable.sublist(1, 5000).length
: csvTable.length - 1,
itemCount: logData.length > 5000 ? 5000 : logData.length,
itemBuilder: (context, index) {
final row = logData[index];
return Container(
padding: EdgeInsets.all(5),
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey.shade400),
),
),
child: Row(
children: csvTable[index + 1]
.asMap()
.entries
.map<Widget>((entry) {
final int idx = entry.key;
final cell = entry.value;
return Expanded(
flex: idx == 0
? 2
: 1, // Allow more space for the first column
children: [
Expanded(
flex: 2,
child: Text(
cell is num
? (idx >= 2 && idx <= 5
? cell.toStringAsFixed(3)
: cell.toStringAsFixed(0))
: cell.toString(),
row.time.toString(),
style: const TextStyle(fontSize: 16),
),
);
}).toList(),
),
Expanded(child: Text(row.tankid.toString())),
Expanded(child: Text(row.temp.toString())),
Expanded(child: Text(row.tempSetpoint.toString())),
Expanded(child: Text(row.pH.toString())),
Expanded(child: Text(row.pHSetpoint.toString())),
Expanded(child: Text(row.onTime.toString())),
Expanded(child: Text(row.kp.toString())),
Expanded(child: Text(row.ki.toString())),
Expanded(child: Text(row.kd.toString())),
],
),
);
},
Expand Down
130 changes: 130 additions & 0 deletions extras/log_file_client/lib/components/graph_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:log_file_client/utils/http_client.dart';
import 'package:syncfusion_flutter_charts/charts.dart';

class GraphView extends StatefulWidget {
const GraphView({required this.csvPath, required this.httpClient, super.key});

final String csvPath;
final HttpClient httpClient;

@override
State<GraphView> createState() => _GraphViewState();
}

class _GraphViewState extends State<GraphView> {
late TrackballBehavior _trackballBehavior;
late final Future<List<LogDataLine>> logData = getLogData();

Future<List<LogDataLine>> getLogData() async {
final table = await widget.httpClient.getLogData(widget.csvPath);
return table!;
}

@override
void initState() {
_trackballBehavior = TrackballBehavior(
enable: true,
tooltipDisplayMode: TrackballDisplayMode.groupAllPoints,
markerSettings: TrackballMarkerSettings(
markerVisibility: TrackballVisibilityMode.visible,
),
tooltipSettings: InteractiveTooltip(
enable: true,
format: 'series.name : point.y',
),
activationMode: ActivationMode.singleTap,
);
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FutureBuilder<List<LogDataLine>>(
future: logData,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No data found'));
} else {
final logData = snapshot.data!;
return Container(
padding: const EdgeInsets.all(16.0),
child: SfCartesianChart(
title: ChartTitle(text: 'Tank ID: ${logData.first.tankid}'),
backgroundColor: Colors.white,
primaryXAxis: DateTimeAxis(
title: AxisTitle(text: 'Time'),
intervalType: DateTimeIntervalType.hours,
interval: 1,
),
primaryYAxis: NumericAxis(
name: 'pHAxis',
title: AxisTitle(text: 'pH Value'),
),
axes: <ChartAxis>[
NumericAxis(
name: 'TemperatureAxis',
title: AxisTitle(text: 'Temperature Value'),
opposedPosition: true,
),
],
legend: Legend(
isVisible: true,
position: LegendPosition.bottom,
),
trackballBehavior: _trackballBehavior,
series: <CartesianSeries>[
LineSeries<LogDataLine, DateTime>(
legendItemText: 'temp',
name: 'temp',
dataSource: logData,
xValueMapper: (LogDataLine log, _) => log.time,
yValueMapper: (LogDataLine log, _) => log.temp,
color: Colors.blue,
yAxisName: 'TemperatureAxis',
),
LineSeries<LogDataLine, DateTime>(
legendItemText: 'temp setpoint',
name: 'temp setpoint',
dataSource: logData,
xValueMapper: (LogDataLine log, _) => log.time,
yValueMapper: (LogDataLine log, _) => log.tempSetpoint,
dashArray: <double>[5, 5],
color: Colors.blue,
yAxisName: 'TemperatureAxis',
),
LineSeries<LogDataLine, DateTime>(
legendItemText: 'pH',
name: 'pH',
dataSource: logData,
xValueMapper: (LogDataLine log, _) => log.time,
yValueMapper: (LogDataLine log, _) => log.pH,
color: Colors.green,
yAxisName: 'pHAxis',
),
LineSeries<LogDataLine, DateTime>(
legendItemText: 'pH setpoint',
name: 'pH setpoint',
dataSource: logData,
xValueMapper: (LogDataLine log, _) => log.time,
yValueMapper: (LogDataLine log, _) => log.pHSetpoint,
dashArray: <double>[5, 5],
color: Colors.green,
yAxisName: 'pHAxis',
),
],
),
);
}
},
),
),
);
}
}
4 changes: 2 additions & 2 deletions extras/log_file_client/lib/pages/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:async';

import 'package:flutter/material.dart';
import 'package:log_file_client/components/app_drawer.dart';
import 'package:log_file_client/components/csv_view.dart';
import 'package:log_file_client/components/graph_view.dart';
import 'package:log_file_client/utils/http_client.dart';

class HomePage extends StatefulWidget {
Expand Down Expand Up @@ -60,7 +60,7 @@ class _HomePageState extends State<HomePage> {
child: _isLoading
? const CircularProgressIndicator()
: _openedLogIndex >= 0
? CsvView(
? GraphView(
csvPath: _logList![_openedLogIndex].uri,
httpClient: httpClient,
)
Expand Down
Loading
Loading