Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix first notification not showing #77

Merged
merged 4 commits into from
Jul 20, 2024
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
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
Loading