diff --git a/elasticlib/Elastic.java b/elasticlib/Elastic.java index 1c88070b..76c952d2 100644 --- a/elasticlib/Elastic.java +++ b/elasticlib/Elastic.java @@ -1,70 +1,72 @@ -package frc.robot; +package frc.robot.util; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.networktables.PubSubOption; import edu.wpi.first.networktables.StringPublisher; import edu.wpi.first.networktables.StringTopic; public final class Elastic { - private static final StringTopic topic = NetworkTableInstance.getDefault() - .getStringTopic("/Elastic/robotnotifications"); - private static final StringPublisher publisher = topic.publish(PubSubOption.sendAll(true)); - private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final StringTopic topic = + NetworkTableInstance.getDefault().getStringTopic("/Elastic/RobotNotifications"); + private static final StringPublisher publisher = + topic.publish(PubSubOption.sendAll(true), PubSubOption.keepDuplicates(true)); + private static final ObjectMapper objectMapper = new ObjectMapper(); - public static void sendAlert(ElasticNotification alert) { - try { - publisher.set(objectMapper.writeValueAsString(alert)); - } catch (JsonProcessingException e) { - e.printStackTrace(); - } + public static void sendAlert(ElasticNotification alert) { + try { + publisher.set(objectMapper.writeValueAsString(alert)); + } catch (JsonProcessingException e) { + e.printStackTrace(); } + } + + public static class ElasticNotification { + @JsonProperty("level") + private NotificationLevel level; - public static class ElasticNotification { - @JsonProperty("level") - private NotificationLevel level; - @JsonProperty("title") - private String title; - @JsonProperty("description") - private String description; + @JsonProperty("title") + private String title; - public ElasticNotification(NotificationLevel level, String title, String description) { - this.level = level; - this.title = title; - this.description = description; - } + @JsonProperty("description") + private String description; - public void setLevel(NotificationLevel level) { - this.level = level; - } + public ElasticNotification(NotificationLevel level, String title, String description) { + this.level = level; + this.title = title; + this.description = description; + } + + public void setLevel(NotificationLevel level) { + this.level = level; + } - public NotificationLevel getLevel() { - return level; - } + public NotificationLevel getLevel() { + return level; + } - public void setTitle(String title) { - this.title = title; - } + public void setTitle(String title) { + this.title = title; + } - public String getTitle() { - return title; - } + public String getTitle() { + return title; + } - public void setDescription(String description) { - this.description = description; - } + public void setDescription(String description) { + this.description = description; + } - public String getDescription() { - return description; - } + public String getDescription() { + return description; + } - public enum NotificationLevel { - INFO, - WARNING, - ERROR - } + public enum NotificationLevel { + INFO, + WARNING, + ERROR } -} \ No newline at end of file + } +} diff --git a/lib/services/nt4_client.dart b/lib/services/nt4_client.dart index 2189a502..fa3891ef 100644 --- a/lib/services/nt4_client.dart +++ b/lib/services/nt4_client.dart @@ -122,11 +122,11 @@ class NT4Subscription { } void updateValue(Object? value, int timestamp) { - currentValue = value; - this.timestamp = timestamp; for (var listener in _listeners) { - listener(currentValue, timestamp); + listener(value, timestamp); } + currentValue = value; + this.timestamp = timestamp; } Map _toSubscribeJson() { @@ -415,7 +415,7 @@ class NT4Client { } void addSample(NT4Topic topic, dynamic data, [int? timestamp]) { - timestamp ??= _getServerTimeUS(); + timestamp ??= getServerTimeUS(); _wsSendBinary( serialize([topic.pubUID, timestamp, topic.getTypeId(), data])); @@ -443,7 +443,7 @@ class NT4Client { return DateTime.now().microsecondsSinceEpoch; } - int _getServerTimeUS() { + int getServerTimeUS() { return _getClientTimeUS() + _serverTimeOffsetUS; } diff --git a/lib/services/nt_connection.dart b/lib/services/nt_connection.dart index f08dd861..1bdb8a69 100644 --- a/lib/services/nt_connection.dart +++ b/lib/services/nt_connection.dart @@ -23,6 +23,8 @@ class NTConnection { bool get isDSConnected => _dsConnected; DSInteropClient get dsClient => _dsClient; + int get serverTime => _ntClient.getServerTimeUS(); + @visibleForTesting List get subscriptions => subscriptionUseCount.keys.toList(); diff --git a/lib/services/robot_notifications_listener.dart b/lib/services/robot_notifications_listener.dart index 4de7adb7..49e57fc0 100644 --- a/lib/services/robot_notifications_listener.dart +++ b/lib/services/robot_notifications_listener.dart @@ -18,7 +18,7 @@ class RobotNotificationsListener { void listen() { var notifications = - ntConnection.subscribeAll('/Elastic/robotnotifications', 0.2); + ntConnection.subscribeAll('/Elastic/RobotNotifications', 0.2); notifications.listen((alertData, alertTimestamp) { if (alertData == null) { return; @@ -33,7 +33,20 @@ class RobotNotificationsListener { // prevent showing a notification when we connect to NT if (_alertFirstRun) { _alertFirstRun = false; - return; + + // If the alert existed 3 or more seconds before the client connected, ignore it + Duration serverTime = Duration(microseconds: ntConnection.serverTime); + Duration alertTime = Duration(microseconds: timestamp); + + // In theory if you had high enough latency and there was no existing data, + // this would not work as intended. However, if you find yourself with 3 + // seconds of latency you have a much more serious issue to deal with as you + // cannot control your robot with that much network latency, not to mention + // that this code wouldn't even be executing since the RTT timestamp delay + // would be so high that it would automatically disconnect from NT + if ((serverTime - alertTime).inSeconds > 3) { + return; + } } Map data; @@ -43,7 +56,9 @@ class RobotNotificationsListener { return; } - if (!data.containsKey('level')) {} + if (!data.containsKey('level')) { + return; + } Icon icon; diff --git a/test/pages/dashboard_page_test.dart b/test/pages/dashboard_page_test.dart index a4e1bc38..3f465c4e 100644 --- a/test/pages/dashboard_page_test.dart +++ b/test/pages/dashboard_page_test.dart @@ -1182,15 +1182,17 @@ void main() { 'level': 'INFO' }; - MockNTConnection connection = createMockOnlineNT4(virtualTopics: [ - NT4Topic( - name: '/Elastic/RobotNotifications', - type: NT4TypeStr.kString, - properties: {}, - ) - ], virtualValues: { - '/Elastic/RobotNotifications': jsonEncode(data) - }); + MockNTConnection connection = createMockOnlineNT4( + virtualTopics: [ + NT4Topic( + name: '/Elastic/RobotNotifications', + type: NT4TypeStr.kString, + properties: {}, + ) + ], + virtualValues: {'/Elastic/RobotNotifications': jsonEncode(data)}, + serverTime: 5000000, + ); MockNT4Subscription mockSub = MockNT4Subscription(); List listeners = []; @@ -1232,7 +1234,7 @@ void main() { await widgetTester.pumpAndSettle(); connection - .subscribeAll('/Elastic/robotnotifications', 0.2) + .subscribeAll('/Elastic/RobotNotifications', 0.2) .updateValue(jsonEncode(data), 1); await widgetTester.pump(); @@ -1244,7 +1246,7 @@ void main() { expect(notificationWidget, findsNothing); connection - .subscribeAll('/Elastic/robotnotifications', 0.2) + .subscribeAll('/Elastic/RobotNotifications', 0.2) .updateValue(jsonEncode(data), 1); }, ); diff --git a/test/services/robot_notifications_listener_test.dart b/test/services/robot_notifications_listener_test.dart index 0c0905bc..d5508539 100644 --- a/test/services/robot_notifications_listener_test.dart +++ b/test/services/robot_notifications_listener_test.dart @@ -28,7 +28,7 @@ void main() { notifications.listen(); // Verify that subscribeAll was called with the specific parameters - verify(mockConnection.subscribeAll('/Elastic/robotnotifications', 0.2)) + verify(mockConnection.subscribeAll('/Elastic/RobotNotifications', 0.2)) .called(1); verify(mockConnection.addDisconnectedListener(any)).called(1); @@ -39,8 +39,8 @@ void main() { verifyNever(mockOnNotification.call(any, any, any)); }); - test("Robot Notifications (Initial Connection | Existing Data) ", () { - MockNTConnection mockConnection = createMockOnlineNT4(); + test("Robot Notifications (Initial Connection | Old Existing Data) ", () { + MockNTConnection mockConnection = createMockOnlineNT4(serverTime: 5000000); MockNT4Subscription mockSub = MockNT4Subscription(); Map data = { @@ -84,13 +84,10 @@ void main() { notifications.listen(); // Verify that subscribeAll was called with the specific parameters - verify(mockConnection.subscribeAll('/Elastic/robotnotifications', 0.2)) + verify(mockConnection.subscribeAll('/Elastic/RobotNotifications', 0.2)) .called(1); verify(mockConnection.addDisconnectedListener(any)).called(1); - // Verify that no other interactions have been made with the mockConnection - verifyNoMoreInteractions(mockConnection); - // Verify that the onNotification callback was never called verifyNever(mockOnNotification(any, any, any)); @@ -110,5 +107,67 @@ void main() { mockSub.updateValue(jsonEncode(data), 3); reset(mockOnNotification); verifyNever(mockOnNotification(any, any, any)); + + // Try with missing data + data.remove('level'); + data['title'] = null; + data['description'] = null; + + mockSub.updateValue(jsonEncode(data), 4); + reset(mockOnNotification); + verifyNever(mockOnNotification(any, any, any)); + }); + + test("Robot Notifications (Initial Connection | Newer Existing Data) ", () { + MockNTConnection mockConnection = createMockOnlineNT4(serverTime: 5000000); + MockNT4Subscription mockSub = MockNT4Subscription(); + + Map data = { + 'title': 'Title1', + 'description': 'Description1', + 'level': 'Info' + }; + + List listeners = []; + when(mockSub.listen(any)).thenAnswer( + (realInvocation) { + listeners.add(realInvocation.positionalArguments[0]); + mockSub.updateValue(jsonEncode(data), 5000000); + }, + ); + + when(mockSub.updateValue(any, any)).thenAnswer( + (invoc) { + for (var value in listeners) { + value.call( + invoc.positionalArguments[0], invoc.positionalArguments[1]); + } + }, + ); + + when(mockConnection.subscribeAll(any, any)).thenAnswer( + (realInvocation) { + mockSub.updateValue(jsonEncode(data), 0); + return mockSub; + }, + ); + + // Create a mock for the onNotification callback + MockNotificationCallback mockOnNotification = MockNotificationCallback(); + + RobotNotificationsListener notifications = RobotNotificationsListener( + ntConnection: mockConnection, + onNotification: mockOnNotification.call, + ); + + notifications.listen(); + + // Verify that subscribeAll was called with the specific parameters + verify(mockConnection.subscribeAll('/Elastic/RobotNotifications', 0.2)) + .called(1); + verify(mockConnection.addDisconnectedListener(any)).called(1); + + // Verify that the onNotification callback was called + verify(mockOnNotification(any, any, any)); }); } diff --git a/test/test_util.dart b/test/test_util.dart index 891b39b8..226bd5d9 100644 --- a/test/test_util.dart +++ b/test/test_util.dart @@ -28,6 +28,8 @@ MockNTConnection createMockOfflineNT4() { when(mockNT4Connection.isNT4Connected).thenReturn(false); + when(mockNT4Connection.serverTime).thenReturn(0); + when(mockNT4Connection.connectionStatus()) .thenAnswer((_) => Stream.value(false)); @@ -52,6 +54,7 @@ MockNTConnection createMockOfflineNT4() { MockNTConnection createMockOnlineNT4({ List? virtualTopics, Map? virtualValues, + int serverTime = 0, }) { HttpOverrides.global = null; @@ -89,6 +92,8 @@ MockNTConnection createMockOnlineNT4({ when(mockNT4Connection.isNT4Connected).thenReturn(true); + when(mockNT4Connection.serverTime).thenReturn(serverTime); + when(mockNT4Connection.connectionStatus()) .thenAnswer((_) => Stream.value(true));