Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class DashboardCore {
/*
* Telemetry packets are batched for transmission and sent at this interval.
*/
private static final int DEFAULT_TELEMETRY_TRANSMISSION_INTERVAL = 100; // ms
public static final int DEFAULT_TELEMETRY_TRANSMISSION_INTERVAL = 100; // ms

public boolean enabled;

Expand Down Expand Up @@ -182,9 +182,9 @@ public void accept(CustomVariable configRoot) {

/**
* Queues a telemetry packet to be sent to all clients. Packets are sent in batches of
* approximate period {@link #getTelemetryTransmissionInterval()}. Clients display the most
* recent value received for each key, and the data is cleared upon op mode init or a call to
* {@link #clearTelemetry()}.
* approximate period {@link #getTelemetryTransmissionInterval()}. The Telemetry view rebuilds
* its display from each batch, keeps it through a batch that only draws on the field, and is
* cleared by {@link #clearTelemetry()}.
*
* @param telemetryPacket packet to send
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,66 @@

import com.acmerobotics.dashboard.canvas.Canvas;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;

/**
* Container for telemetry information. This class can be extended to support additional, custom
* telemetry data.
*
* <p>Items and lines share one ordered list; log entries render below, as on the Driver Station.
*/
public class TelemetryPacket {
/** Mirrors the SDK's Telemetry.DisplayFormat. */
public enum DisplayFormat {
CLASSIC,
MONOSPACE,
HTML
}

/** A displayed entry. A null caption is a bare line, rendered without a separator. */
public static final class Item {
private final String caption;
private String value;

Item(String caption, String value) {
this.caption = caption;
this.value = value;
}

public String getCaption() {
return caption;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value == null ? "null" : value;
}

public void appendValue(String suffix) {
value += suffix;
}
}

private long timestamp;
private SortedMap<String, String> data;
private List<String> log;
private long[] logRange;
private Canvas field;
private Canvas fieldOverlay;
private List<Item> items;
private DisplayFormat displayFormat;
private String captionValueSeparator;
private boolean telemetryFrame;

private static final String DEFAULT_CAPTION_VALUE_SEPARATOR = ": ";

private static final Canvas DEFAULT_FIELD = new Canvas();

Expand All @@ -31,6 +76,9 @@ public class TelemetryPacket {
public TelemetryPacket(boolean drawDefaultField) {
data = new TreeMap<>();
log = new ArrayList<>();
items = new ArrayList<>();
displayFormat = DisplayFormat.HTML;
captionValueSeparator = DEFAULT_CAPTION_VALUE_SEPARATOR;
field = new Canvas();
fieldOverlay = new Canvas();

Expand All @@ -44,13 +92,48 @@ public TelemetryPacket() {
}

/**
* Stores a single key-value pair.
* Stores a single key-value pair. A key already present is overwritten in place, unlike the
* SDK's {@code addData()}.
*
* @param key entry key
* @param value entry value
*/
public void put(String key, Object value) {
data.put(key, value == null ? "null" : value.toString());
String caption = captionOf(key);
String stringValue = value == null ? "null" : value.toString();

data.put(caption, stringValue);

for (Item item : items) {
if (Objects.equals(item.getCaption(), caption)) {
item.setValue(stringValue);
return;
}
}

items.add(new Item(caption, stringValue));
}

/**
* Adds a key-value pair as the SDK's addData() does: a repeated key is displayed again rather
* than overwritten. The keyed data keeps the latest value.
*/
protected void append(String key, Object value) {
String caption = captionOf(key);
String stringValue = value == null ? "null" : value.toString();

data.put(caption, stringValue);
items.add(new Item(caption, stringValue));
}

/** Stores a key-value pair for the graph and logging views without displaying it. */
public void putData(String key, Object value) {
data.put(captionOf(key), value == null ? "null" : value.toString());
}

// A null caption means "bare line", so a null key renders as the SDK would format it.
private static String captionOf(String key) {
return key == null ? "null" : key;
}

/**
Expand All @@ -65,19 +148,77 @@ public void putAll(Map<String, Object> map) {
}

/**
* Adds a line to the telemetry log.
* Adds a line in place, between the items added before and after it.
*
* @param line text to append
*/
public void addLine(String line) {
log.add(line);
addItem(line);
}

/** Clears the telemetry log. */
/** Like addLine, but returns the line so values can be appended to it. */
public Item addItem(String value) {
Item item = new Item(null, value == null ? "" : value);
items.add(item);
return item;
}

/** Clears bare lines only; keyed items and log entries are left alone. */
public void clearLines() {
Iterator<Item> it = items.iterator();
while (it.hasNext()) {
if (it.next().getCaption() == null) {
it.remove();
}
}
}

public void addLogEntry(String entry) {
log.add(entry);
}

public void clearLog() {
log.clear();
}

/** For a log resent whole each packet: its entries number consecutively from first to last. */
protected void setLogRange(long first, long last) {
logRange = new long[] {first, last};
}

public long[] getLogRange() {
return logRange == null ? null : logRange.clone();
}

/** Direct packets default to HTML; FtcDashboard.getTelemetry() defaults to CLASSIC. */
public void setDisplayFormat(DisplayFormat displayFormat) {
this.displayFormat = displayFormat == null ? DisplayFormat.HTML : displayFormat;
}

public DisplayFormat getDisplayFormat() {
return displayFormat;
}

/** Lets an empty packet clear the display; a pruned drawing-only packet looks the same. */
public void markTelemetryFrame() {
telemetryFrame = true;
}

public boolean isTelemetryFrame() {
return telemetryFrame;
}

public void setCaptionValueSeparator(String captionValueSeparator) {
this.captionValueSeparator =
captionValueSeparator == null
? DEFAULT_CAPTION_VALUE_SEPARATOR
: captionValueSeparator;
}

public String getCaptionValueSeparator() {
return captionValueSeparator;
}

/**
* Adds and returns the current timestamp to the packet. This is called automatically when the
* packet is sent (and any previous timestamp will be overwritten).
Expand All @@ -87,6 +228,18 @@ public long addTimestamp() {
return timestamp;
}

public List<Item> getItems() {
return items;
}

public SortedMap<String, String> getData() {
return data;
}

public List<String> getLog() {
return log;
}

/** Returns the field overlay canvas. */
public Canvas fieldOverlay() {
return fieldOverlay;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.acmerobotics.dashboard;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.acmerobotics.dashboard.telemetry.TelemetryPacket;
import com.google.gson.JsonObject;
import org.junit.jupiter.api.Test;

public class TelemetryPacketTests {
// The real serializer, so these pin what actually goes on the wire. Notably it serializes
// nulls, so a bare line is sent as "caption":null rather than omitting the key.
private static JsonObject serialize(TelemetryPacket packet) {
return DashboardCore.GSON.toJsonTree(packet).getAsJsonObject();
}

@Test
public void itemsKeepInsertionOrderWithLinesInPlace() {
TelemetryPacket packet = new TelemetryPacket(false);
packet.put("zebra", 1);
packet.addLine("--- drive ---");
packet.put("apple", 2);
packet.addItem("sticks").appendValue(" | x: 0.5");
packet.put("zebra", 3);

JsonObject json = serialize(packet);
assertEquals(
"[{\"caption\":\"zebra\",\"value\":\"3\"},"
+ "{\"caption\":null,\"value\":\"--- drive ---\"},"
+ "{\"caption\":\"apple\",\"value\":\"2\"},"
+ "{\"caption\":null,\"value\":\"sticks | x: 0.5\"}]",
json.get("items").toString());
assertEquals("{\"apple\":\"2\",\"zebra\":\"3\"}", json.get("data").toString());
}

// The SDK renders a null caption as the text "null", which must not match a bare line.
@Test
public void nullKeyIsCaptionedNullAndLeavesBareLinesAlone() {
TelemetryPacket packet = new TelemetryPacket(false);
packet.addLine("a bare line");
packet.put(null, 5);

assertEquals(
"[{\"caption\":null,\"value\":\"a bare line\"},"
+ "{\"caption\":\"null\",\"value\":\"5\"}]",
serialize(packet).get("items").toString());
}

@Test
public void clearLinesLeavesKeyedItemsAndLogAlone() {
TelemetryPacket packet = new TelemetryPacket(false);
packet.put("a", 1);
packet.addLine("gone");
packet.addLogEntry("kept");
packet.clearLines();

JsonObject json = serialize(packet);
assertEquals("[{\"caption\":\"a\",\"value\":\"1\"}]", json.get("items").toString());
assertEquals("[\"kept\"]", json.get("log").toString());
}

// A packet built directly has no Telemetry to call setDisplayFormat on, and never reaches a
// Driver Station, so it keeps rendering markup the way the dashboard always has.
@Test
public void handBuiltPacketDefaultsToHtmlAndAnUnnumberedLog() {
TelemetryPacket packet = new TelemetryPacket(false);
packet.addLogEntry("tick");

JsonObject json = serialize(packet);
assertEquals("\"HTML\"", json.get("displayFormat").toString());
assertEquals("null", json.get("logRange").toString());
}
}
9 changes: 9 additions & 0 deletions FtcDashboard/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ android {
lintOptions {
abortOnError false
}

// The telemetry logic resolves no android.* class, so it runs as a plain JVM test.
testOptions {
unitTests.all {
useJUnitPlatform()
}
}
}

node {
Expand Down Expand Up @@ -122,6 +129,8 @@ dependencies {
implementation("org.nanohttpd:nanohttpd-websocket:2.3.1") {
exclude module: "nanohttpd"
}

testImplementation "org.junit.jupiter:junit-jupiter:5.9.1"
}

def addPomDependencies(root) {
Expand Down
Loading
Loading