diff --git a/.github/workflows/device-client.yaml b/.github/workflows/device-client.yaml index b27d118d7..bb4fc4f9d 100644 --- a/.github/workflows/device-client.yaml +++ b/.github/workflows/device-client.yaml @@ -12,7 +12,7 @@ jobs: - name: install flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.2' + flutter-version: '3.27.3' channel: 'stable' - name: use cache uses: actions/cache@v3 @@ -43,7 +43,7 @@ jobs: - name: install flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.2' + flutter-version: '3.27.3' channel: 'stable' - name: use cache uses: actions/cache@v3 diff --git a/.github/workflows/log_file_client.yaml b/.github/workflows/log_file_client.yaml index 25c60dbad..8f2f08d1d 100644 --- a/.github/workflows/log_file_client.yaml +++ b/.github/workflows/log_file_client.yaml @@ -12,7 +12,7 @@ - name: install flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.2' + flutter-version: '3.27.3' channel: 'stable' - name: use cache uses: actions/cache@v3 @@ -43,7 +43,7 @@ - name: install flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.2' + flutter-version: '3.27.3' channel: 'stable' - name: use cache uses: actions/cache@v3 diff --git a/.github/workflows/log_file_server.yaml b/.github/workflows/log_file_server.yaml index a9e091180..75a346b86 100644 --- a/.github/workflows/log_file_server.yaml +++ b/.github/workflows/log_file_server.yaml @@ -12,7 +12,7 @@ - name: install flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.2' + flutter-version: '3.27.3' channel: 'stable' - name: use cache uses: actions/cache@v3 @@ -42,7 +42,7 @@ - name: install flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.2' + flutter-version: '3.27.3' channel: 'stable' - name: use cache uses: actions/cache@v3 @@ -60,7 +60,6 @@ mkdir -p test/logs touch test/logs/empty.log echo "123456789" > test/logs/ten.log - echo "This is data for line 1\nThis is data for line 2" > test/logs/deleteMe.log dart run "bin/log_file_server.dart" "test/logs" & dart test --concurrency=1 jobs diff --git a/Makefile b/Makefile index fc02c98f0..d9965622b 100644 --- a/Makefile +++ b/Makefile @@ -109,9 +109,6 @@ GPP_TEST=g++ $(FLAGS) -L$(BIN) $(INCLUDE) $(BIN)/PHCalibrationWarningTest.cpp.bin: $(BIN)/libarduino.so $(TEST)/PHCalibrationWarningTest.cpp $(HEADERS) $(GPP_TEST) -o $(BIN)/PHCalibrationWarningTest.cpp.bin $(TEST)/PHCalibrationWarningTest.cpp -larduino -$(BIN)/RemoteLogPusherTest.cpp.bin: $(BIN)/libarduino.so $(TEST)/RemoteLogPusherTest.cpp $(HEADERS) - $(GPP_TEST) -o $(BIN)/RemoteLogPusherTest.cpp.bin $(TEST)/RemoteLogPusherTest.cpp -larduino - $(BIN)/BlinkTest.cpp.bin: $(BIN)/libarduino.so $(TEST)/BlinkTest.cpp $(HEADERS) $(GPP_TEST) -o $(BIN)/BlinkTest.cpp.bin $(TEST)/BlinkTest.cpp -larduino @@ -431,9 +428,6 @@ $(BIN)/TC_util.o: $(SRC)/model/TC_util.cpp $(HEADERS) $(BIN)/TankController.o: $(SRC)/TankController.cpp $(HEADERS) g++ -c $(FLAGS) $(INCLUDE) -o $(BIN)/TankController.o $(SRC)/TankController.cpp -$(BIN)/RemoteLogPusher.o: $(SRC)/model/RemoteLogPusher.cpp $(HEADERS) - g++ -c $(FLAGS) $(INCLUDE) -o $(BIN)/RemoteLogPusher.o $(SRC)/model/RemoteLogPusher.cpp - $(BIN)/DataLogger.o: $(SRC)/model/DataLogger.cpp $(HEADERS) g++ -c $(FLAGS) $(INCLUDE) -o $(BIN)/DataLogger.o $(SRC)/model/DataLogger.cpp diff --git a/examples/TankController/TankController.ino b/examples/TankController/TankController.ino index fe71c8481..224dc83be 100644 --- a/examples/TankController/TankController.ino +++ b/examples/TankController/TankController.ino @@ -14,6 +14,7 @@ const char pushingBoxID[] = ""; // If it remains empty, the MAC address will be used. Keep in mind that // the name should be unique across all devices, not just your devices. // So "tank-1" is not a good name, but "Onthank-tank-1" is. +// The name will have ".log" added to it (so don't include it!). const char remoteLogName[] = ""; // We query a web server for GMT time and then adjust for local time diff --git a/extras/device_client/lib/model/version.dart b/extras/device_client/lib/model/version.dart index d0555af25..ddec32c9a 100644 --- a/extras/device_client/lib/model/version.dart +++ b/extras/device_client/lib/model/version.dart @@ -1 +1 @@ -const String gitVersion = 'v24.10.2-14-gf3+'; +const String gitVersion = 'v24.10.2-105-g8+'; diff --git a/extras/device_client/pubspec.lock b/extras/device_client/pubspec.lock index 0f6049428..13a0d8af9 100644 --- a/extras/device_client/pubspec.lock +++ b/extras/device_client/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "88399e291da5f7e889359681a8f64b18c5123e03576b01f32a6a276611e511c3" + sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c" url: "https://pub.dev" source: hosted - version: "78.0.0" + version: "79.0.0" _macros: dependency: transitive description: dart @@ -18,10 +18,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "62899ef43d0b962b056ed2ebac6b47ec76ffd003d5f7c4e4dc870afe63188e33" + sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643 url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" args: dependency: transitive description: @@ -313,10 +313,10 @@ packages: dependency: "direct main" description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" io: dependency: transitive description: @@ -545,26 +545,26 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93" + sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" + sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" shared_preferences_foundation: dependency: transitive description: @@ -774,18 +774,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" vector_math: dependency: transitive description: @@ -860,4 +860,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0" diff --git a/extras/device_client/pubspec.yaml b/extras/device_client/pubspec.yaml index 21f0ee35e..0ac7ea629 100644 --- a/extras/device_client/pubspec.yaml +++ b/extras/device_client/pubspec.yaml @@ -1,7 +1,7 @@ name: device_client description: A web UI for the Open Acidification Tank Controller. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 24.10.2 +version: 25.1.1 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/extras/docs/Network.drawio b/extras/docs/Network.drawio new file mode 100644 index 000000000..2ee82a56a --- /dev/null +++ b/extras/docs/Network.drawio @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extras/docs/Network.drawio.svg b/extras/docs/Network.drawio.svg new file mode 100644 index 000000000..e7a0ad4f3 --- /dev/null +++ b/extras/docs/Network.drawio.svg @@ -0,0 +1,4 @@ + + + +
oap-vm.cs
oap.cs (Traefik)
Tank Controller
User Agent (Web)
A
B
C
D
\ No newline at end of file diff --git a/extras/docs/Network.md b/extras/docs/Network.md new file mode 100644 index 000000000..c004c7106 --- /dev/null +++ b/extras/docs/Network.md @@ -0,0 +1,55 @@ +# Architecture + +## Machines + +* **traefik** is a virtual machine that acts as the public-facing server for [oap.cs.wallawalla.edu](oap.cs.wallawalla.edu). It acts as a firewall and reverse-proxy, forwarding requests to oap-vm. +* **oap-vm** is a virtual machine in our CS Lab that supports various services for the Open Acidification Project (OAP). +* A **Tank Controller** is an Arduino-based device controller that monitors and manages temperature and pH for a tank (aquarium) used in ocean acidification research. It has a 4x4 keypad for input and a 16x2 LCD display for output. It has a serial port used to flash the program memory and communicate with a computer. It also has an Ethernet connection and can make and respond to HTTP requests (the system does not have adequate resources to support HTTPS). +* A **User Agent (Web Browser)** running on a separate machine that can connect to oap-vm or the tank controller. + +## Applications + +* The original application, **TankController**, is Arduino program that runs on the Tank Controller. It consists of a trivial "sketch" ([TankController.ino](../../examples/TankController/TankController.ino)), a primary library (TankController), and a number of other device-related libraries (see [libraries.md](libraries.md)). +* The second application, **device_client**, is a Flutter application that mimics the Tank Controller's keypad and LCD. It is a single-page application (SPA) served from http://oap.cs.wallawalla.edu. After the static files are loaded, it presents a GUI in which the user can enter the IP address of a Tank Controller and interact—using the web browser—with the Tank Controller as if the device were physically present. The fact that we use one browser to interact with multiple servers presents a couple complications. + * First, most browsers prevent "Cross-Origin Resource Sharing (CORS)" unless explicitly enabled. In particular, CORS is designed to prevent a malicious (primary) site from scraping content from a secondary site (say, a bank), and sending it to the user (making the user think it is interacting with the bank when it is actually interacting with the malicious site). A secondary site may, however, authorize a primary site to collect and display its data by adding `Access-Control-Allow-Origin: *` to a response header. The TankController does this, and thus solves the CORS issue. + * Second, if a page is served with HTTPS (typically indicated by a lock in the address bar), most browsers do not let JavaScript on the page make HTTP requests because this would violate the user's expectation of security in the content. This means that if any data is obtained using HTTP, the entire page must be loaded as HTTP (to adequately communicate "insecure" to the user). As mentioned above, the Tank Controller's Arduino processor does not have adequate resources to handle HTTPS, so the Flutter-generated files served from oap.cs.wallawalla.edu must be served using HTTP before making the HTTP requests to the Tank Controller. + * While the browser sees the device_client app as HTTP, it is actually served by oap-vm as HTTPS and sent on by traefik as HTTP. +* The third application, **log_file_server**, is a Dart server application that runs on oap-vm and accepts data from a Tank Controller and stores it on disk where it can be served by an Nginx web server on oap-vm. Because the Tank Controller does not support HTTPS (either as client or server), the data upload must be done using HTTP to oap.cs.wallawalla.edu (traefik) where it is converted to HTTPS and forwarded to oap-vm. This application also generates an abridged copy of the data to be used as thumbnails showing summary information for a group of Tank Controllers. +* The fourth application, **log_file_client**, is a Flutter SPA that takes the data saved by log_file_server (from the Tank Controllers), and presents it in a graphical format. Because it does not interact directly with a Tank Controller, it can be served from https://oap.cs.wallawalla.edu. In fact, the distinction between device_client and log_file_client is http vs. https. + +## Communication + +![Network Architecture Diagram](Network.drawio.svg) + +* **A**: traefik communicates with oap-vm on an internal network using HTTPS using a self-signed certificate generated by oap-vm and recognized by traefik. +* **B**: The user requests device_client (http://oap.cs.wallawalla.edu) or log_file_client (https://oap.cs.wallawalla.edu) from traefik which forwards the requests to oap-vm. +* **C**: A Tank Controller can send data to log_file_server (using HTTP) to be saved on oap-vm. +* **D**: Using device_client, a user can interact directly with a live Tank Controller (using HTTP). + +## Traefik and Nginx Configuration + +Traefik receives several types of requests (on links B and C) that it passes on to oap-vm (on link A). Each is sent to oap-vm using HTTPS, so the request needs to be mapped to something that Nginx on oap-vm can recognize as distinct. + +### HTTPS + +1. GET https://oap.cs.wallawalla.edu/api/ requests are for recent lines from files saved by the log_file_server app (link B). + * Traefik forwards to https://oap.vm.cs.wallawalla.edu:443/ + * Nginx forwards to http://localhost:8080 (log_file_server) +1. GET https://oap.cs.wallawalla.edu/logs/ requests are for files saved by the log_file_server app (link B). + * Traefik forwards to https://oap.vm.cs.wallawalla.edu:443/ + * Nginx serves `/var/www/html` with `autoindex on` +1. GET https://oap.cs.wallawalla.edu/ requests are for the log_file_client app (link B). + * Traefik forwards to https://oap.vm.cs.wallawalla.edu:443/ + * Nginx serves `/var/www/html/log_file_client/` + +### HTTP + +1. GET http://oap.cs.wallawalla.edu/ requests are for the device_client app (link B). + * Traefik forwards to https://oap.vm.cs.wallawalla.edu:444/ + * Nginx serves `/var/www/html/device_client/` +1. HEAD http://oap.cs.wallawalla.edu/logs/ requests are for the size of log files (link C). + * Traefik forwards to https://oap.vm.cs.wallawalla.edu:444/logs/ + * Nginx serves `/var/www/html/` +1. POST http://oap.cs.wallawalla.edu/api/ requests are to the log_file_server app (link C). + * Traefik forwards to https://oap.vm.cs.wallawalla.edu:444/api/ + * Nginx forwards to http://localhost:8080 (log_file_server) diff --git a/extras/log_file_client/pubspec.lock b/extras/log_file_client/pubspec.lock index 0aab11460..fb390914e 100644 --- a/extras/log_file_client/pubspec.lock +++ b/extras/log_file_client/pubspec.lock @@ -244,10 +244,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" intl: dependency: "direct main" description: diff --git a/extras/log_file_client/pubspec.yaml b/extras/log_file_client/pubspec.yaml index 5b9833c55..140c61a8f 100644 --- a/extras/log_file_client/pubspec.yaml +++ b/extras/log_file_client/pubspec.yaml @@ -1,7 +1,7 @@ name: log_file_client description: "A new Flutter project." publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 24.10.2 +version: 25.1.1 environment: sdk: '>=3.3.1 <4.0.0' diff --git a/extras/log_file_server/bin/log_file_server.dart b/extras/log_file_server/bin/log_file_server.dart index e607190c2..04c77df6a 100644 --- a/extras/log_file_server/bin/log_file_server.dart +++ b/extras/log_file_server/bin/log_file_server.dart @@ -1,4 +1,4 @@ -import 'dart:async' show Future, Timer; +import 'dart:async' show Future; import 'dart:io'; import 'package:csv/csv.dart'; @@ -12,7 +12,7 @@ String rootDir = // Configure routes. final _router = Router() ..get('/api/', _get) - ..post('/logs/', _post); + ..post('/api/', _post); // Get snapshots of log files Future _get(Request req, String path) async { @@ -22,7 +22,6 @@ Future _get(Request req, String path) async { final snapshotLength = uri.queryParameters['length'] == null ? 720 : int.parse(uri.queryParameters['length']!); - if (!file.existsSync()) { return Response.notFound(null); } @@ -62,12 +61,6 @@ Future _post(Request req, String path) async { ); } - // // get remote address - // var connectionInfo = - // req.context['shelf.io.connection_info'] as HttpConnectionInfo; - // var remoteAddress = connectionInfo.remoteAddress.address; - // print('remoteAddress = "$remoteAddress" (${remoteAddress.runtimeType})'); - final file = File('$rootDir/$path'); file.createSync(exclusive: false); file.writeAsStringSync( @@ -77,33 +70,13 @@ Future _post(Request req, String path) async { return Response.ok(null); } -/* - * read the root directory and create an index.html file - */ -Future _createIndex() async { - final sink = File('$rootDir/index.html').openWrite(); - sink.write('
    '); - await for (final file in Directory(rootDir).list()) { - if (file is File) { - final path = file.path.substring(rootDir.length + 1); - sink.write('
  • /logs/$path
  • '); - } - } - sink.write('
'); - // close the file - await sink.close(); -} - void main(List args) async { // assign rootDir from args if (args.isNotEmpty) { rootDir = args[0]; } + // Should not be needed in production, but it is useful for testing. await Directory(rootDir).create(recursive: true); - await _createIndex(); - Timer.periodic(Duration(hours: 1), (timer) async { - await _createIndex(); - }); // Use any available host or container IP (usually `0.0.0.0`). final ip = InternetAddress.anyIPv4; diff --git a/extras/log_file_server/pubspec.lock b/extras/log_file_server/pubspec.lock index f2b53316c..27f5273fb 100644 --- a/extras/log_file_server/pubspec.lock +++ b/extras/log_file_server/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "88399e291da5f7e889359681a8f64b18c5123e03576b01f32a6a276611e511c3" + sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c" url: "https://pub.dev" source: hosted - version: "78.0.0" + version: "79.0.0" _macros: dependency: transitive description: dart @@ -18,10 +18,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "62899ef43d0b962b056ed2ebac6b47ec76ffd003d5f7c4e4dc870afe63188e33" + sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643 url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" args: dependency: "direct main" description: @@ -138,10 +138,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" io: dependency: transitive description: @@ -322,10 +322,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "4ac0537115a24d772c408a2520ecd0abb99bca2ea9c4e634ccbdbfae64fe17ec" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" string_scanner: dependency: transitive description: diff --git a/extras/log_file_server/pubspec.yaml b/extras/log_file_server/pubspec.yaml index 7fb1a9213..f3ec80942 100644 --- a/extras/log_file_server/pubspec.yaml +++ b/extras/log_file_server/pubspec.yaml @@ -1,6 +1,6 @@ name: log_file_server description: A server app using the shelf package to manage TankController log files. -version: 24.10.2 +version: 25.1.1 repository: https://github.com/Open-Acidification/TankController environment: diff --git a/extras/reverse_proxy/pubspec.lock b/extras/reverse_proxy/pubspec.lock index 8782ef1b3..54b7c5477 100644 --- a/extras/reverse_proxy/pubspec.lock +++ b/extras/reverse_proxy/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "88399e291da5f7e889359681a8f64b18c5123e03576b01f32a6a276611e511c3" + sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c" url: "https://pub.dev" source: hosted - version: "78.0.0" + version: "79.0.0" _macros: dependency: transitive description: dart @@ -18,10 +18,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "62899ef43d0b962b056ed2ebac6b47ec76ffd003d5f7c4e4dc870afe63188e33" + sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643 url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" args: dependency: transitive description: @@ -122,10 +122,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" io: dependency: transitive description: @@ -306,10 +306,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "4ac0537115a24d772c408a2520ecd0abb99bca2ea9c4e634ccbdbfae64fe17ec" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" string_scanner: dependency: transitive description: diff --git a/extras/scripts/post-commit b/extras/scripts/post-commit deleted file mode 100755 index f3c96e6b0..000000000 --- a/extras/scripts/post-commit +++ /dev/null @@ -1,5 +0,0 @@ -version=`git describe --tags` -version="$version " -version="${version:0:15}+" -echo "#define VERSION \"$version\"" > src/Version.h -echo "const String gitVersion = '$version';" > extras/device_client/lib/model/version.dart diff --git a/library.properties b/library.properties index 3311de1e8..16bb9dfab 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=TankController -version=24.10.2 +version=25.1.1 author=Kirt Onthank , James Foster , Preston Carman maintainer=James Foster sentence=Software for the Arduino that controls pH and temperature in the Open-Acidification project. diff --git a/src/TankController.cpp b/src/TankController.cpp index e4c937e86..ffae5c72d 100644 --- a/src/TankController.cpp +++ b/src/TankController.cpp @@ -40,7 +40,6 @@ TankController *TankController::instance(const char *remoteLogName, const char * if (!_instance) { serial(F("\r\n##############\r\nTankController %s"), TANK_CONTROLLER_VERSION); _instance = new TankController(); - unsigned long start = millis(); SD_TC::instance()->setRemoteLogName(remoteLogName); EEPROM_TC::instance(); Keypad_TC::instance(); @@ -60,7 +59,6 @@ TankController *TankController::instance(const char *remoteLogName, const char * GetTime::instance(tzOffsetHrs); serial(F("Free memory = %i"), _instance->freeMemory()); wdt_enable(WDTO_8S); - serial(F("TankController::instance() - took %lu ms"), millis() - start); } return _instance; } @@ -169,8 +167,8 @@ void TankController::handleUI() { void TankController::loop(bool report_loop_delay) { static unsigned long previousLoopStart = 0; unsigned long currentLoopStart = millis(); - if (report_loop_delay && previousLoopStart && currentLoopStart - previousLoopStart > 300) { - serial(F("unexpected overall delay of %i ms (at %lu sec uptime)"), currentLoopStart - previousLoopStart, + if (report_loop_delay && previousLoopStart && currentLoopStart - previousLoopStart > 500) { + serial(F("unexpected overall delay of %lu ms (at %lu sec uptime)"), currentLoopStart - previousLoopStart, millis() / 1000); } wdt_reset(); @@ -186,7 +184,7 @@ void TankController::loop(bool report_loop_delay) { if (report_loop_delay) { static long int count = 0; unsigned long currentLoopTime = millis() - currentLoopStart; - if (++count % 10000 == 1 || currentLoopTime > 200) { // first time through and periodically thereafter + if (++count % 100000 == 1 || currentLoopTime > 500) { // first time through and periodically thereafter serial(F("TankController::loop() - took %lu ms (at %lu sec uptime)"), currentLoopTime, millis() / 1000); } } diff --git a/src/Version.h b/src/Version.h index 68975925a..be24c5e11 100644 --- a/src/Version.h +++ b/src/Version.h @@ -1 +1 @@ -#define VERSION "v24.10.2-14-gf3+" +#define VERSION "v24.10.2-105-g8+" diff --git a/src/model/DataLogger.cpp b/src/model/DataLogger.cpp index 9238d51ca..e1ea186aa 100644 --- a/src/model/DataLogger.cpp +++ b/src/model/DataLogger.cpp @@ -188,7 +188,6 @@ void DataLogger::writeDataToRemoteLog() { serial(F("WARNING! String was truncated to \"%s\""), buffer); } SD_TC::instance()->writeToRemoteLog(buffer); - serial(F("New info written to remote log")); } /** diff --git a/src/model/GetTime.cpp b/src/model/GetTime.cpp index 99da618ba..62fd26b93 100644 --- a/src/model/GetTime.cpp +++ b/src/model/GetTime.cpp @@ -102,7 +102,7 @@ void GetTime::loop() { void GetTime::sendRequest() { static const char format[] PROGMEM = - "GET /Date HTTP/1.1\r\n" + "GET /index.html HTTP/1.1\r\n" "Host: %s\r\n" "Connection: close\r\n" "\r\n"; diff --git a/src/model/RemoteLogPusher.cpp b/src/model/RemoteLogPusher.cpp index 9dfb48dad..498824a5d 100644 --- a/src/model/RemoteLogPusher.cpp +++ b/src/model/RemoteLogPusher.cpp @@ -1,7 +1,23 @@ #include "model/RemoteLogPusher.h" +#include "Version.h" +#include "model/PHControl.h" +#include "model/TC_util.h" +#include "wrappers/Ethernet_TC.h" +#include "wrappers/SD_TC.h" +#include "wrappers/Serial_TC.h" + +const uint16_t PORT = 80; + +// class variables RemoteLogPusher* RemoteLogPusher::_instance = nullptr; +// class methods +/** + * @brief accessor for singleton + * + * @return RemoteLogPusher* + */ RemoteLogPusher* RemoteLogPusher::instance() { if (!_instance) { _instance = new RemoteLogPusher(); @@ -9,10 +25,183 @@ RemoteLogPusher* RemoteLogPusher::instance() { return _instance; } +// instance methods +RemoteLogPusher::RemoteLogPusher() { + this->buffer[0] = '\0'; +} + +bool RemoteLogPusher::isReadyToPost() { + return _isReadyToPost && millis() > delayRequestsUntilTime && !(PHControl::instance()->isOn()); +} + void RemoteLogPusher::loop() { - // This function does nothing in the current codebase + if (!(Ethernet_TC::instance()->isConnectedToNetwork())) { + return; + } + switch (state) { + case CLIENT_NOT_CONNECTED: + if (isReadyToPost()) { + _isReadyToPost = false; + sendPostRequest(); + } else if (shouldSendHeadRequest()) { + _shouldSendHeadRequest = false; + sendHeadRequest(); + } else { + // nothing to do + } + break; + case PROCESS_HEAD_RESPONSE: + loopHead(); + break; + case PROCESS_POST_RESPONSE: + loopPost(); + break; + default: + break; + } +} + +void RemoteLogPusher::loopHead() { + if (client.connected()) { + if (client.available()) { // bytes are remaining in the current packet + int next; + while ((next = client.read()) != -1) { // Flawfinder: ignore + if (next) { + if (next == '\r') { + buffer[index] = '\0'; + bool isDone = false; + if (index >= 22 && memcmp_P(buffer, F("http/1.1 404 not found"), 22) == 0) { + // File has not yet been created on server + serverFileSize = (uint32_t)0; + _isReadyToPost = true; + isDone = true; + } else if (index > 16 && memcmp_P(buffer, F("content-length: "), 16) == 0) { + serverFileSize = strtoul(buffer + 16, nullptr, 10); + uint32_t localFileSize = SD_TC::instance()->getRemoteFileSize(); + _isReadyToPost = serverFileSize < localFileSize; + isDone = true; + } + if (isDone) { + buffer[0] = '\0'; + index = 0; + client.stop(); + state = CLIENT_NOT_CONNECTED; + delayRequestsUntilTime = millis() + (_isReadyToPost ? 3000 : 30000); + return; + } + } else if (next == '\n' || index == sizeof(buffer)) { + buffer[0] = '\0'; + index = 0; + } else { + buffer[index++] = tolower(next); + } + } + } + } + } else { + state = CLIENT_NOT_CONNECTED; + client.stop(); + delayRequestsUntilTime = millis() + 3000; + } +} + +void RemoteLogPusher::loopPost() { + if (client.connected()) { + if (client.available()) { // bytes are remaining in the current packet + int next; + while ((next = client.read()) != -1) { // Flawfinder: ignore + if (next) { + if (next == '\r') { + buffer[index] = '\0'; + if (index >= 15 && memcmp_P(buffer, F("http/1.1 200 ok"), 15) == 0) { + buffer[0] = '\0'; + index = 0; + state = CLIENT_NOT_CONNECTED; + client.stop(); + _shouldSendHeadRequest = true; + delayRequestsUntilTime = millis() + 3000; + return; + } + } else if (next == '\n' || index == sizeof(buffer)) { + buffer[0] = '\0'; + index = 0; + } else { + buffer[index++] = tolower(next); + } + } + } + } + } else { + state = CLIENT_NOT_CONNECTED; + client.stop(); + _shouldSendHeadRequest = true; + delayRequestsUntilTime = millis() + 3000; + } } +/** + * @brief attempt to push at the next opportunity + * + * This method is called when the remote log file is written to, and when the + * bubbler is turned off. + */ void RemoteLogPusher::pushSoon() { - // This function does nothing in the current codebase + _shouldSendHeadRequest = true; +} + +/** + * @brief send request to server for size of remote log file + * + */ +void RemoteLogPusher::sendHeadRequest() { + const char* logName = SD_TC::instance()->getRemoteLogName(); + if (logName[0] == '\0') { + return; + } + static const char format[] PROGMEM = + "HEAD /logs/%s HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: TankController/%s\r\n" + "Accept: text/plain\r\n" + "Connection: Close\r\n" + "\r\n"; + snprintf_P(buffer, sizeof(buffer), (PGM_P)format, logName, serverDomain, VERSION); + if (client.connected() || client.connect(serverDomain, PORT) == 1) { // this is a blocking step + client.write(buffer, strnlen(buffer, sizeof(buffer))); + } else { + serial(F("RemoteLogPusher: connection to %s failed"), serverDomain); + // "_shouldSendHeadRequest = true;" would retry next loop but we'll try within one minute anyway + } + buffer[0] = '\0'; + state = PROCESS_HEAD_RESPONSE; +} + +void RemoteLogPusher::sendPostRequest() { + const char* logName = SD_TC::instance()->getRemoteLogName(); + if (logName[0] == '\0') { + return; + } + char data[300]; + SD_TC::instance()->getRemoteLogContents(data, sizeof(data), serverFileSize); + static const char format[] PROGMEM = + "POST /api/%s HTTP/1.1\r\n" + "Host: %s\r\n" + "Content-Type: text/plain\r\n" + "User-Agent: TankController/%s\r\n" + "Content-Length: %i\r\n" + "Connection: Close\r\n" + "\r\n"; + snprintf_P(buffer, sizeof(buffer), (PGM_P)format, logName, serverDomain, VERSION, strnlen(data, sizeof(data))); + if (client.connected() || client.connect(serverDomain, PORT) == 1) { // this is a blocking step + client.write(buffer, strnlen(buffer, sizeof(buffer))); + client.write(data, strnlen(data, sizeof(data))); + } else { + serial(F("RemoteLogPusher: connection to %s failed"), serverDomain); + } + buffer[0] = '\0'; + state = PROCESS_POST_RESPONSE; +} + +bool RemoteLogPusher::shouldSendHeadRequest() { + return _shouldSendHeadRequest && millis() > delayRequestsUntilTime && !(PHControl::instance()->isOn()); } diff --git a/src/model/RemoteLogPusher.h b/src/model/RemoteLogPusher.h index 34b40c8af..986d04268 100644 --- a/src/model/RemoteLogPusher.h +++ b/src/model/RemoteLogPusher.h @@ -1,16 +1,98 @@ #pragma once #include +#include "wrappers/Ethernet_TC.h" + +/* + * @brief RemoteLogPusher is a singleton that sends data records and remote logs to the server. + * + * The server is expected to be running extras/log_file_server. + * The ideas is that while the device records data to the SD card's 1-second log and + * 1-minute log, it also sends the 1-minute log records to the server with configuration + * changes and certain warning and error conditions. + * + * Eventually, the server will send remote logs to a user's phone. + * + * The device tries to keep the server up-to-date on the latest data by comparing the + * file size reported by the server (in a HEAD request) with the file size on the SD card. + * If the server has a smaller file, the device sends data from the indicated point to the + * end of the file. + * + * The instance is called each time through the TankController's loop() and operates + * with a state machine. + * + * If the state is CLIENT_NOT_CONNECTED and there is new data (indicated by the + * _shouldSendHeadRequest flag), it sends a HEAD request and changes the state to + * PROCESS_HEAD_RESPONSE. + * + * If the state is PROCESS_HEAD_RESPONSE, it reads the response from the server and if it + * is a 404, it transitions to the CLIENT_NOT_CONNECTED state and sets a _isReadyToPost flag. + * If the response is a 200, it transitions to the CLIENT_NOT_CONNECTED state and sets a + * _isReadyToPost flag if the reported file size is less than the file size on the SD card. + * + * If the state is CLIENT_NOT_CONNECTED and there is a _isReadyToPost flag, reads from the + * SD card and sends a POST request and changes the state to PROCESS_POST_RESPONSE. + * + * If the state is PROCESS_POST_RESPONSE, it reads the response from the server and if it + * a 200, it transitions to the CLIENT_NOT_CONNECTED state and sets a _shouldSendHeadRequest + * flag to see if there is more that should be sent. + * + */ + +enum clientState_t { CLIENT_NOT_CONNECTED, PROCESS_HEAD_RESPONSE, PROCESS_POST_RESPONSE }; + class RemoteLogPusher { public: // class methods static RemoteLogPusher *instance(); // instance methods + RemoteLogPusher(); + bool isReadyToPost(); void loop(); void pushSoon(); + bool shouldSendHeadRequest(); + +#if defined(ARDUINO_CI_COMPILATION_MOCKS) + EthernetClient *getClient() { + return &client; + } + const char *getServerDomain() { + return serverDomain; + } + bool basicShouldSendHeadRequest() { + return _shouldSendHeadRequest; + } + + void setShouldSentHeadRequest(bool value) { + _shouldSendHeadRequest = value; + } + clientState_t getState() { + return state; + } + void reset() { + _instance = nullptr; + } +#endif private: // class variables static RemoteLogPusher *_instance; + + // instance variables + EthernetClient client; + clientState_t state = CLIENT_NOT_CONNECTED; + uint32_t delayRequestsUntilTime = 40000; // wait a bit before first request + const char *serverDomain = "oap.cs.wallawalla.edu"; + char buffer[300]; + unsigned int index = 0; + bool _isReadyToPost = false; + uint32_t serverFileSize = 0; + bool _shouldSendHeadRequest = false; + + // instance methods + void loopHead(); + void loopPost(); + void sendPostRequest(); + void sendHeadRequest(); }; diff --git a/src/wrappers/SD_TC.cpp b/src/wrappers/SD_TC.cpp index 2ac10c90a..919f64b11 100644 --- a/src/wrappers/SD_TC.cpp +++ b/src/wrappers/SD_TC.cpp @@ -42,7 +42,6 @@ SD_TC::SD_TC() { if (!sd.begin(SD_SELECT_PIN)) { Serial.println(F("SD_TC failed to initialize!")); } - setRemoteLogName(); remoteLogName[0] = '\0'; } @@ -129,13 +128,18 @@ bool SD_TC::format() { void SD_TC::getRemoteLogContents(char* buffer, int size, uint32_t index) { buffer[0] = '\0'; - File file = open(getRemoteLogName(), O_RDONLY); + const char* logName = getRemoteLogName(); + if (logName[0] == '\0') { + return; + } + File file = open(logName, O_RDONLY); if (file) { file.seek(index); int remaining = file.available(); if (remaining > 0) { - int readSize = file.read(buffer, min(size - 1, remaining)); // Flawfinder: ignore - if (readSize == min(size - 1, remaining)) { + int toRead = min(size - 1, remaining); + int readSize = file.read(buffer, toRead); // Flawfinder: ignore + if (readSize == toRead) { buffer[readSize] = '\0'; } else { buffer[0] = '\0'; @@ -145,15 +149,6 @@ void SD_TC::getRemoteLogContents(char* buffer, int size, uint32_t index) { } } -const char* SD_TC::getRemoteLogName() { - if (remoteLogName[0] == '\0') { - byte* mac = Ethernet_TC::instance()->getMac(); - snprintf_P(remoteLogName, sizeof(remoteLogName), PSTR("%02X%02X%02X%02X%02X%02X.log"), mac[0], mac[1], mac[2], - mac[3], mac[4], mac[5]); - } - return remoteLogName; -} - bool SD_TC::iterateOnFiles(doOnFile functionName, void* userData) { #if defined(ARDUINO_CI_COMPILATION_MOCKS) return false; // no more files @@ -263,9 +258,12 @@ bool SD_TC::remove(const char* path) { } void SD_TC::setRemoteLogName(const char* newFileName) { + if (newFileName == nullptr || newFileName[0] == '\0') { + remoteLogName[0] = '\0'; + return; + } // See TankController.ino for the definition of remoteLogName - if (newFileName != nullptr && strnlen(newFileName, MAX_FILE_NAME_LENGTH + 1) > 0 && - strnlen(newFileName, MAX_FILE_NAME_LENGTH + 1) <= MAX_FILE_NAME_LENGTH) { + if (strnlen(newFileName, MAX_FILE_NAME_LENGTH + 1) <= MAX_FILE_NAME_LENGTH) { // valid file name has been provided (See TankController.ino) snprintf_P(remoteLogName, MAX_FILE_NAME_LENGTH + 5, PSTR("%s.log"), newFileName); } @@ -278,6 +276,10 @@ void SD_TC::todaysDataFileName(char* path, int size) { } void SD_TC::updateRemoteFileSize() { + if (remoteLogName[0] == '\0') { + remoteFileSize = 0; + return; + } File file = open(remoteLogName, O_RDONLY); if (file) { remoteFileSize = file.size(); @@ -297,7 +299,11 @@ void SD_TC::writeToRemoteLog(const char* line) { strncpy(mostRecentRemoteLogEntry, line, sizeof(mostRecentRemoteLogEntry)); // Flawfinder: ignore mostRecentRemoteLogEntry[sizeof(mostRecentRemoteLogEntry) - 1] = '\0'; // Ensure null-terminated string #endif - if (!sd.exists(getRemoteLogName())) { + const char* logName = getRemoteLogName(); + if (logName[0] == '\0') { + return; + } + if (!sd.exists(logName)) { // rather than write an entire header line in one buffer, we break it into chunks to save memory char buffer[200]; DataLogger::instance()->writeRemoteFileHeader(buffer, sizeof(buffer), 0); @@ -310,5 +316,6 @@ void SD_TC::writeToRemoteLog(const char* line) { appendStringToPath(buffer, remoteLogName); } appendStringToPath(line, remoteLogName); + updateRemoteFileSize(); RemoteLogPusher::instance()->pushSoon(); } diff --git a/src/wrappers/SD_TC.h b/src/wrappers/SD_TC.h index 0165fda28..7dfbe5e7e 100644 --- a/src/wrappers/SD_TC.h +++ b/src/wrappers/SD_TC.h @@ -31,7 +31,9 @@ class SD_TC { uint32_t getRemoteFileSize() { return remoteFileSize; } - const char* getRemoteLogName(); + const char* getRemoteLogName() { + return remoteLogName; + } bool listRootToBuffer(void (*callWhenFull)(const char*, bool)); bool mkdir(const char* path); File open(const char* path, oflag_t oflag = 0x00); @@ -58,7 +60,7 @@ class SD_TC { const uint8_t SD_SELECT_PIN = SS; bool hasHadError = false; SdFat sd; - char remoteLogName[MAX_FILE_NAME_LENGTH + 5]; // add ".log" with null-terminator + char remoteLogName[MAX_FILE_NAME_LENGTH + 5] = ""; // add ".log" with null-terminator uint32_t remoteFileSize = 0; // Max depth of file system search for rootdir() diff --git a/test/DataLoggerTest.cpp b/test/DataLoggerTest.cpp index b90e8da62..b81b8ad53 100644 --- a/test/DataLoggerTest.cpp +++ b/test/DataLoggerTest.cpp @@ -72,7 +72,6 @@ unittest(loop) { snprintf(infoString, sizeof(infoString), "%s\t%s", VERSION, "0\tI\t2023-08-15 00:01:00\t\t20.00\t-242.02\t0.000\t8.100\t0.000\t60"); assertEqual(infoString, sd->mostRecentRemoteLogEntry); - assertEqual("New info written to remote log", serialPort->getBuffer()); } unittest(writeToSD) { diff --git a/test/PHProbeTest.cpp b/test/PHProbeTest.cpp index f4d6f9900..6ea087b3f 100644 --- a/test/PHProbeTest.cpp +++ b/test/PHProbeTest.cpp @@ -155,12 +155,18 @@ unittest(setLowpointCalibration) { unittest(setMidpointCalibration) { GodmodeState *state = GODMODE(); state->reset(); + DataLogger::instance()->reset(); + assertFalse(DataLogger::instance()->getShouldWriteWarning()); eeprom->setIgnoreBadPHSlope(true); assertTrue(eeprom->getIgnoreBadPHSlope()); + assertTrue(DataLogger::instance()->getShouldWriteWarning()); + DataLogger::instance()->reset(); + assertFalse(DataLogger::instance()->getShouldWriteWarning()); assertEqual("", state->serialPort[0].dataOut); assertEqual("", state->serialPort[1].dataOut); state->serialPort[0].dataOut = ""; pHProbe->setMidpointCalibration(11.875); + assertTrue(DataLogger::instance()->getShouldWriteWarning()); assertEqual("PHProbe::setMidpointCalibration(11.875)\r\n", state->serialPort[0].dataOut); assertEqual("Cal,mid,11.875\r", state->serialPort[1].dataOut); assertFalse(eeprom->getIgnoreBadPHSlope()); diff --git a/test/PushingBoxTest.cpp b/test/PushingBoxTest.cpp index 6839c87ff..69c022471 100644 --- a/test/PushingBoxTest.cpp +++ b/test/PushingBoxTest.cpp @@ -47,17 +47,29 @@ unittest_teardown() { } unittest(NoTankID) { + SD_TC::instance()->setRemoteLogName("90A2DA807B76"); + auto expected1 = "heater turned on at 1813 after 1813 ms"; + assertEqual(expected1, Serial_TC::instance()->getBuffer()); + Serial_TC::instance()->clearBuffer(); // set tank id to 0, set time interval to 1 minute EEPROM_TC::instance()->setTankID(0); delay(30 * 1000); // allow 30 seconds for time update tc->loop(); + auto expected2 = "GetTime: connected to oap.cs.wallawalla.edu"; + assertEqual(expected2, Serial_TC::instance()->getBuffer()); + Serial_TC::instance()->clearBuffer(); tc->loop(); - state->serialPort[0].dataOut = ""; - delay(40 * 1000); // allow 70 seconds (30 + 40) for PushingBox update - tc->loop(); // Trigger SD logging and Serial (DataLogger) and PushingBox - char expected[] = "Set Tank ID in order to send data to PushingBox"; - assertEqual(expected, Serial_TC::instance()->getBuffer()); + delay(20 * 1000); // allow 50 seconds (30 + 40) for RemoteLogPusher update + tc->loop(); // Trigger SD logging and Serial (DataLogger) + auto expected3 = "RemoteLogPusher: connection to oap.cs.wallawalla.edu failed"; + assertEqual(expected3, Serial_TC::instance()->getBuffer()); + Serial_TC::instance()->clearBuffer(); + delay(20 * 1000); // allow 70 seconds (30 + 20 + 20) for PushingBox update + tc->loop(); // Trigger PushingBox + auto expected4 = "Set Tank ID in order to send data to PushingBox"; + assertEqual(expected4, Serial_TC::instance()->getBuffer()); + Serial_TC::instance()->clearBuffer(); } unittest(SendData) { @@ -74,7 +86,6 @@ unittest(SendData) { delay(10 * 1000); // allow for PushingBox update tc->loop(); char expected[] = - "New info written to remote log\r\n" "GET /pushingbox?devid=PushingBoxIdentifier&tankid=99&tempData=20.25&pHdata=7.125 HTTP/1.1\r\n" "attempting to connect to PushingBox...\r\n" "connected\r\n" diff --git a/test/RemoteLogPusherTest.cpp b/test/RemoteLogPusherTest.cpp index 4e6e8d6ae..9f7f07a0d 100644 --- a/test/RemoteLogPusherTest.cpp +++ b/test/RemoteLogPusherTest.cpp @@ -1,8 +1,208 @@ #include #include -unittest(shouldBeEqual) { - assertEqual(0, 0); +#include "DataLogger.h" +#include "DateTime_TC.h" +#include "PHControl.h" +#include "PHProbe.h" +#include "RemoteLogPusher.h" +#include "SD_TC.h" +#include "TC_util.h" +#include "TankController.h" + +unittest_setup() { + GODMODE()->reset(); + Ethernet.mockDHCP(IPAddress(192, 168, 1, 42)); + SD_TC::instance()->format(); // reset the remote log file + DataLogger::instance()->reset(); + RemoteLogPusher::instance()->reset(); + TankController::instance(); +} + +unittest_teardown() { +} + +unittest(singleton) { + RemoteLogPusher* thing1 = RemoteLogPusher::instance(); + RemoteLogPusher* thing2 = RemoteLogPusher::instance(); + assertTrue(thing1 != nullptr); + assertEqual(thing1, thing2); +} + +unittest(loopSendsRequests) { + TankController* tc = TankController::instance(); + RemoteLogPusher* pusher = RemoteLogPusher::instance(); + EthernetClient* pClient = pusher->getClient(); + SD_TC::instance()->setRemoteLogName("90A2DA807B76"); + + // Set up the server to respond to the HEAD request + assertTrue(Ethernet_TC::instance(true)->isConnectedToNetwork()); + EthernetClient::startMockServer( + pusher->getServerDomain(), + (uint32_t)0, + 80, + (const uint8_t *)"HTTP/1.1 404 Not Found\r\n" + "Date: Mon, 21 Aug 2023 16:33:52 GMT\r\n" + "Content-Type: text/html\r\n" + "\r\n" + ); + + // We start the test with a fresh remote log file + assertFalse(pusher->shouldSendHeadRequest()); + assertFalse(pusher->isReadyToPost()); + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + assertFalse(pClient->connected()); + char buffer[100]; + SD_TC::instance()->getRemoteLogContents(buffer, sizeof(buffer), 0); + assertEqual("", buffer); + + DataLogger::instance()->writeWarningSoon(); + tc->loop(false); // write to data log + assertTrue(pusher->basicShouldSendHeadRequest()); + assertFalse(pusher->shouldSendHeadRequest()); + tc->loop(false); // write to remote log + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + SD_TC::instance()->getRemoteLogContents(buffer, sizeof(buffer), 0); + buffer[7] = '\0'; // truncate the message + assertEqual("Version", buffer); + + // After a start-up delay we send a HEAD request + tc->loop(false); + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + assertTrue(pusher->basicShouldSendHeadRequest()); + assertFalse(pusher->shouldSendHeadRequest()); + assertFalse(pusher->isReadyToPost()); + delay(40000); + assertTrue(pusher->shouldSendHeadRequest()); + tc->loop(false); // Send HEAD request to server + assertTrue(pClient->connected()); + assertEqual(PROCESS_HEAD_RESPONSE, pusher->getState()); + assertFalse(pusher->shouldSendHeadRequest()); + + // The server responds with a 404 (file not found) + tc->loop(false); // handle PROCESS_HEAD_RESPONSE + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + assertFalse(pusher->isReadyToPost()); + + pClient->stop(); // clears the readBuffer (but not the write buffer!?) + assertFalse(pClient->connected()); + EthernetClient::stopMockServer(pusher->getServerDomain(), (uint32_t)0, 80); + EthernetClient::startMockServer( + pusher->getServerDomain(), + (uint32_t)0, + 80, + (const uint8_t *)"HTTP/1.1 200 OK\r\n" + "Date: Tue, 12 Mar 2024 16:33:52 GMT\r\n" + "Content-Type: text/html\r\n" + "\r\n" + ); + + // After a brief delay we send the post request + delay(4000); + assertTrue(pusher->isReadyToPost()); + tc->loop(false); // send POST request to server + assertFalse(pusher->isReadyToPost()); + assertEqual(PROCESS_POST_RESPONSE, pusher->getState()); + + // The server should respond with a 200 OK + tc->loop(false); // get response to POST + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + assertFalse(pusher->isReadyToPost()); + assertFalse(pusher->shouldSendHeadRequest()); + assertFalse(pClient->connected()); + + // After a brief delay we could send another HEAD request + delay(4000); + assertTrue(pusher->shouldSendHeadRequest()); + + // set up server for next HEAD request + (pusher->getClient())->stop(); + EthernetClient::stopMockServer(pusher->getServerDomain(), (uint32_t)0, 80); + EthernetClient::startMockServer( + pusher->getServerDomain(), + (uint32_t)0, + 80, + (const uint8_t*)"HTTP/1.1 200 OK\r\n" + "Date: Mon, 21 Aug 2023 16:33:54 GMT\r\n" + "Content-Length: 664\r\n" + "Content-Type: text/html\r\n" + "\r\n" + ); + tc->loop(false); // HEAD request is sent + assertFalse(pusher->isReadyToPost()); + uint32_t localFileSize = SD_TC::instance()->getRemoteFileSize(); + SD_TC::instance()->updateRemoteLogFileSizeForTest(); // size to zero + // SD_TC::instance()->writeRemoteLog("some data here"); // and '\n' is added for 15 bytes + tc->loop(false); // "200 OK" is received + assertFalse(pusher->shouldSendHeadRequest()); // + assertFalse(pusher->isReadyToPost()); // because server has all 15 bytes + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + (pusher->getClient())->stop(); + EthernetClient::stopMockServer(pusher->getServerDomain(), (uint32_t)0, 80); +} + +unittest(noInternetConnectionWhenBubblerIsOn) { + TankController* tc = TankController::instance(); + RemoteLogPusher* pusher = RemoteLogPusher::instance(); + assertFalse(pusher->basicShouldSendHeadRequest()); + EthernetClient* pClient = pusher->getClient(); + + // Turn on the bubbler + PHControl* controlSolenoid = PHControl::instance(); + PHProbe* pHProbe = PHProbe::instance(); + controlSolenoid->setBaseTargetPh(7.50); + pHProbe->setPh(8.5); + tc->loop(false); // update the controls based on the current readings + assertTrue(controlSolenoid->isOn()); + assertTrue(pusher->basicShouldSendHeadRequest()); + + // Set up the server to respond to the HEAD request + assertTrue(Ethernet_TC::instance(true)->isConnectedToNetwork()); + EthernetClient::startMockServer( + pusher->getServerDomain(), + (uint32_t)0, + 80, + (const uint8_t *)"HTTP/1.1 404 Not Found\r\n" + "Date: Mon, 21 Aug 2023 16:33:52 GMT\r\n" + "Content-Type: text/html\r\n" + "\r\n" + ); + + // We start the test with a fresh remote log file + DataLogger::instance()->writeWarningSoon(); + assertTrue(pusher->basicShouldSendHeadRequest()); + assertFalse(pusher->shouldSendHeadRequest()); + assertFalse(pusher->isReadyToPost()); + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + assertFalse(pClient->connected()); + char buffer[100]; + SD_TC::instance()->getRemoteLogContents(buffer, sizeof(buffer), 0); + buffer[7] = '\0'; // truncate the message + assertEqual("Version", buffer); // We have data to send to the server + + // Allow start-up delay to pass; we should not send a HEAD request + tc->loop(false); + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + assertTrue(pusher->basicShouldSendHeadRequest()); + assertFalse(pusher->shouldSendHeadRequest()); + assertFalse(pusher->isReadyToPost()); + delay(40000); + assertTrue(pusher->basicShouldSendHeadRequest()); + assertFalse(pusher->shouldSendHeadRequest()); // because bubbler is on + tc->loop(false); + assertEqual(CLIENT_NOT_CONNECTED, pusher->getState()); + assertTrue(pusher->basicShouldSendHeadRequest()); + assertFalse(pClient->connected()); + + // Allow time to pass to turn off bubbler + delay(7500); + assertFalse(pClient->connected()); + assertTrue(pusher->basicShouldSendHeadRequest()); + pHProbe->setPh(7.25); // this also does a loop() call + assertFalse(pusher->basicShouldSendHeadRequest()); + assertTrue(pClient->connected()); + assertEqual(PROCESS_HEAD_RESPONSE, pusher->getState()); + // from here on we are testing the normal case } unittest_main() diff --git a/test/SDTest.cpp b/test/SDTest.cpp index 6cc1cc3b0..823098cdc 100644 --- a/test/SDTest.cpp +++ b/test/SDTest.cpp @@ -3,10 +3,10 @@ #include "DateTime_TC.h" #include "PHCalibrationMid.h" +#include "RemoteLogPusher.h" #include "SD_TC.h" #include "TC_util.h" #include "TankController.h" -#include "UIState/PHCalibrationMid.h" unittest_setup() { GODMODE()->reset(); @@ -41,11 +41,11 @@ unittest(tankControllerLoop) { if (file.size() < sizeof(data)) { file.read(data, file.size()); data[file.size()] = '\0'; - assertEqual( + auto expected = "time,tankid,temp,temp setpoint,pH,pH setpoint,upTime,Kp,Ki,Kd\n" "04/15/2021 00:00:00, 0, 0.00, 20.00, 0.000, 8.100, 1, 100000.0, 0.0, 0.0\n" - "04/15/2021 00:00:01, 0, 0.00, 20.00, 0.000, 8.100, 2, 100000.0, 0.0, 0.0\n", - data); + "04/15/2021 00:00:01, 0, 0.00, 20.00, 0.000, 8.100, 2, 100000.0, 0.0, 0.0\n"; + assertEqual(expected, data); } file.close(); } @@ -70,10 +70,10 @@ unittest(loopInCalibration) { if (file.size() < sizeof(data)) { file.read(data, file.size()); data[file.size()] = '\0'; - assertEqual( + auto expected = "time,tankid,temp,temp setpoint,pH,pH setpoint,upTime,Kp,Ki,Kd\n" - "04/15/2021 00:00:03, 0, C, 20.00, C, 8.100, 3, 100000.0, 0.0, 0.0\n", - data); + "04/15/2021 00:00:03, 0, C, 20.00, C, 8.100, 3, 100000.0, 0.0, 0.0\n"; + assertEqual(expected, data); } file.close(); } @@ -103,14 +103,16 @@ unittest(appendData) { File file = SD_TC::instance()->open("20210415.csv"); file.read(data, file.size()); data[file.size()] = '\0'; - assertEqual("time,tankid,temp,temp setpoint,pH,pH setpoint,upTime,Kp,Ki,Kd\nline 1\nline 2\n", data); + auto expected1 = "time,tankid,temp,temp setpoint,pH,pH setpoint,upTime,Kp,Ki,Kd\nline 1\nline 2\n"; + assertEqual(expected1, data); file.close(); // verify contents of 16.csv file = SD_TC::instance()->open("20210416.csv"); file.read(data, file.size()); data[file.size()] = '\0'; - assertEqual("time,tankid,temp,temp setpoint,pH,pH setpoint,upTime,Kp,Ki,Kd\nline 3\n", data); + auto expected2 = "time,tankid,temp,temp setpoint,pH,pH setpoint,upTime,Kp,Ki,Kd\nline 3\n"; + assertEqual(expected2, data); file.close(); } @@ -188,19 +190,70 @@ unittest(removeFile) { assertFalse(SD_TC::instance()->exists("20220706.log")); } -unittest(noAlertFileName) { +unittest(writeRemoteLog) { SD_TC* sd = SD_TC::instance(); - sd->setRemoteLogName(""); + sd->setRemoteLogName("Tank1"); + delay(60000); // remote logs don't get written immediately + char data[20]; + sd->setRemoteLogName("90A2DA807B76"); + RemoteLogPusher* pusher = RemoteLogPusher::instance(); + assertEqual("90A2DA807B76.log", sd->getRemoteLogName()); + sd->updateRemoteLogFileSizeForTest(); + assertFalse(sd->exists("90A2DA807B76.log")); + assertEqual(0, sd->getRemoteFileSize()); + pusher->setShouldSentHeadRequest(false); + assertFalse(pusher->shouldSendHeadRequest()); + + // write data + sd->writeToRemoteLog("line 1"); // also writes header row + sd->updateRemoteLogFileSizeForTest(); + assertTrue(pusher->basicShouldSendHeadRequest()); + assertTrue(sd->exists("90A2DA807B76.log")); + int size = sd->getRemoteFileSize(); + sd->writeToRemoteLog("line 2"); + sd->updateRemoteLogFileSizeForTest(); + assertEqual(size + strlen("line 2\n"), sd->getRemoteFileSize()); // Flawfinder: ignore + + // verify contents of remote log + File file = sd->open("90A2DA807B76.log"); + file.seek(size); + file.read(data, 7); // Flawfinder: ignore + file.close(); + data[7] = '\0'; + assertEqual("line 2\n", data); +} + +unittest(getRemoteLogContents) { + SD_TC* sd = SD_TC::instance(); + + // write data + sd->setRemoteLogName("Tank1"); + sd->writeToRemoteLog("line 1"); + sd->updateRemoteLogFileSizeForTest(); + int size = sd->getRemoteFileSize(); + sd->writeToRemoteLog("and 2\nline 3"); + sd->updateRemoteLogFileSizeForTest(); + char buffer[20]; + // get remaining remote log + sd->getRemoteLogContents(buffer, sizeof(buffer), size); + assertEqual("and 2\nline 3\n", buffer); +} + +unittest(noRemoteLogFileName) { + SD_TC* sd = SD_TC::instance(); + sd->setRemoteLogName("Tank1"); + sd->setRemoteLogName(""); + assertEqual("", sd->getRemoteLogName()); } -unittest(validAlertFileName) { +unittest(validRemoteLogFileName) { SD_TC* sd = SD_TC::instance(); sd->setRemoteLogName("Tank1"); assertEqual("Tank1.log", sd->getRemoteLogName()); } -unittest(longAlertFileName) { +unittest(longRemoteLogFileName) { SD_TC* sd = SD_TC::instance(); sd->setRemoteLogName("1234567890123456789012345678"); // maximum length assertEqual("1234567890123456789012345678.log", sd->getRemoteLogName()); @@ -220,7 +273,7 @@ unittest(remoteLogName) { tc = TankController::instance(); sd = SD_TC::instance(); name = sd->getRemoteLogName(); - assertEqual("90A2DA807B76.log", name); + assertEqual("", name); sd->setRemoteLogName("newName"); name = sd->getRemoteLogName(); diff --git a/test/SerialTest.cpp b/test/SerialTest.cpp index 09e3755da..0bd52ed01 100644 --- a/test/SerialTest.cpp +++ b/test/SerialTest.cpp @@ -55,8 +55,6 @@ unittest(report_loop_delay) { assertEqual("00:01 pH=0.000 temp= 0.00\r\n", state->serialPort[0].dataOut); state->serialPort[0].dataOut = ""; // clear serial output tc->loop(); // for remote log - assertEqual("New info written to remote log\r\n", state->serialPort[0].dataOut); - state->serialPort[0].dataOut = ""; // clear serial output tc->loop(true); // to get the first loop delay message assertEqual("TankController::loop() - took 0 ms (at 60 sec uptime)\r\n", state->serialPort[0].dataOut);