Skip to content

Commit

Permalink
Fix first notification not showing (#77)
Browse files Browse the repository at this point in the history
The first notification data received will only be ignored if the message
is from more than 3 seconds in the past
  • Loading branch information
Gold872 authored Jul 20, 2024
1 parent 0166c29 commit 189d21f
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 74 deletions.
98 changes: 50 additions & 48 deletions elasticlib/Elastic.java
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
10 changes: 5 additions & 5 deletions lib/services/nt4_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> _toSubscribeJson() {
Expand Down Expand Up @@ -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]));
Expand Down Expand Up @@ -443,7 +443,7 @@ class NT4Client {
return DateTime.now().microsecondsSinceEpoch;
}

int _getServerTimeUS() {
int getServerTimeUS() {
return _getClientTimeUS() + _serverTimeOffsetUS;
}

Expand Down
2 changes: 2 additions & 0 deletions lib/services/nt_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class NTConnection {
bool get isDSConnected => _dsConnected;
DSInteropClient get dsClient => _dsClient;

int get serverTime => _ntClient.getServerTimeUS();

@visibleForTesting
List<NT4Subscription> get subscriptions => subscriptionUseCount.keys.toList();

Expand Down
21 changes: 18 additions & 3 deletions lib/services/robot_notifications_listener.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, dynamic> data;
Expand All @@ -43,7 +56,9 @@ class RobotNotificationsListener {
return;
}

if (!data.containsKey('level')) {}
if (!data.containsKey('level')) {
return;
}

Icon icon;

Expand Down
24 changes: 13 additions & 11 deletions test/pages/dashboard_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Function(Object?, int)> listeners = [];
Expand Down Expand Up @@ -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();
Expand All @@ -1244,7 +1246,7 @@ void main() {
expect(notificationWidget, findsNothing);

connection
.subscribeAll('/Elastic/robotnotifications', 0.2)
.subscribeAll('/Elastic/RobotNotifications', 0.2)
.updateValue(jsonEncode(data), 1);
},
);
Expand Down
73 changes: 66 additions & 7 deletions test/services/robot_notifications_listener_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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<String, dynamic> data = {
Expand Down Expand Up @@ -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));

Expand All @@ -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<String, dynamic> data = {
'title': 'Title1',
'description': 'Description1',
'level': 'Info'
};

List<Function(Object?, int)> 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));
});
}
5 changes: 5 additions & 0 deletions test/test_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ MockNTConnection createMockOfflineNT4() {

when(mockNT4Connection.isNT4Connected).thenReturn(false);

when(mockNT4Connection.serverTime).thenReturn(0);

when(mockNT4Connection.connectionStatus())
.thenAnswer((_) => Stream.value(false));

Expand All @@ -52,6 +54,7 @@ MockNTConnection createMockOfflineNT4() {
MockNTConnection createMockOnlineNT4({
List<NT4Topic>? virtualTopics,
Map<String, dynamic>? virtualValues,
int serverTime = 0,
}) {
HttpOverrides.global = null;

Expand Down Expand Up @@ -89,6 +92,8 @@ MockNTConnection createMockOnlineNT4({

when(mockNT4Connection.isNT4Connected).thenReturn(true);

when(mockNT4Connection.serverTime).thenReturn(serverTime);

when(mockNT4Connection.connectionStatus())
.thenAnswer((_) => Stream.value(true));

Expand Down

0 comments on commit 189d21f

Please sign in to comment.