Add FTA variant of field monitor with ability to save notes (closes #58).

This commit is contained in:
Patrick Fairbank
2020-04-02 20:06:56 -07:00
parent 30b0679b6e
commit eb64939b20
9 changed files with 161 additions and 14 deletions

View File

@@ -11,7 +11,8 @@ CREATE TABLE teams (
accomplishments VARCHAR(1000),
wpakey VARCHAR(16),
yellowcard bool,
hasconnected bool
hasconnected bool,
ftanotes VARCHAR(1000)
);
-- +goose Down

View File

@@ -18,6 +18,7 @@ type Team struct {
WpaKey string
YellowCard bool
HasConnected bool
FtaNotes string
}
func (database *Database) CreateTeam(team *Team) error {

View File

@@ -44,25 +44,29 @@ body {
width: 42%;
height: 100%;
background-color: #333;
font-size: 13vw;
display: flex;
flex-direction: column;
}
.team-id {
width: 100%;
height: 80%;
font-size: 13vw;
}
.team-id[data-status=no-link] {
.team-id[data-fta="true"] {
height: 40%;
font-size: 6vw;
}
.team-id[data-status=no-link], .team-notes[data-status=no-link] {
background-color: #963;
}
.team-id[data-status=ds-linked] {
.team-id[data-status=ds-linked], .team-notes[data-status=ds-linked] {
background-color: #ff0;
color: #333;
}
.team-id[data-status=robot-linked] {
.team-id[data-status=robot-linked], .team-notes[data-status=robot-linked] {
background-color: #0a3;
}
.team-id[data-status=radio-linked] {
.team-id[data-status=radio-linked], .team-notes[data-status=radio-linked] {
background-color: #ff00ff;
}
.team-box-row {
@@ -85,3 +89,24 @@ body {
.team-box i {
margin-right: 0.5vw;
}
.team-notes[data-fta="true"] {
height: 40%;
display: flex;
justify-content: space-between;
padding: 0.5vw;
font-size: 1vw;
}
.team-notes[data-fta="false"] {
display: none;
}
.team-notes div {
width: 96%;
height: 96%;
white-space: pre;
}
textarea {
width: 96%;
height: 96%;
background-color: #ccc;
color: #000;
}

View File

@@ -19,6 +19,8 @@ var handleArenaStatus = function(data) {
teamElementPrefix = "#" + blueSide + "Team" + station[1];
}
var teamIdElement = $(teamElementPrefix + "Id");
var teamNotesElement = $(teamElementPrefix + "Notes");
var teamNotesTextElement = $(teamElementPrefix + "Notes div");
var teamEthernetElement = $(teamElementPrefix + "Ethernet");
var teamDsElement = $(teamElementPrefix + "Ds");
var teamRadioElement = $(teamElementPrefix + "Radio");
@@ -26,6 +28,8 @@ var handleArenaStatus = function(data) {
var teamRobotElement = $(teamElementPrefix + "Robot");
var teamBypassElement = $(teamElementPrefix + "Bypass");
teamNotesTextElement.attr("data-station", station);
if (stationStatus.Team) {
// Set the team number and status.
teamIdElement.text(stationStatus.Team.Id);
@@ -42,10 +46,13 @@ var handleArenaStatus = function(data) {
}
}
teamIdElement.attr("data-status", status);
teamNotesTextElement.text(stationStatus.Team.FtaNotes);
teamNotesElement.attr("data-status", status);
} else {
// No team is present in this position for this match; blank out the status.
teamIdElement.text("");
teamIdElement.attr("data-status", "");
teamNotesTextElement.text("");
teamNotesElement.attr("data-status", "");
}
// Format the Ethernet status box.
@@ -119,6 +126,19 @@ var handleEventStatus = function(data) {
$("#earlyLateMessage").text(data.EarlyLateMessage);
};
// Makes the team notes section editable and handles saving edits to the server.
var editFtaNotes = function(element) {
var teamNotesElement = $(element);
var textArea = $("<textarea />");
textArea.val(teamNotesElement.text());
teamNotesElement.replaceWith(textArea);
textArea.focus();
textArea.blur(function() {
textArea.replaceWith(teamNotesElement);
websocket.send("updateTeamNotes", { station: teamNotesElement.attr("data-station"), notes: textArea.val()});
});
};
$(function() {
// Read the configuration for this display from the URL query string.
var urlParams = new URLSearchParams(window.location.search);
@@ -132,6 +152,7 @@ $(function() {
}
$(".reversible-left").attr("data-reversed", reversed);
$(".reversible-right").attr("data-reversed", reversed);
$(".fta-dependent").attr("data-fta", urlParams.get("fta"));
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/field_monitor/websocket", {

View File

@@ -86,6 +86,7 @@
<li><a href="/displays/announcer">Announcer</a></li>
<li><a href="/displays/audience">Audience</a></li>
<li><a href="/displays/field_monitor">Field Monitor</a></li>
<li><a href="/displays/field_monitor?fta=true">Field Monitor (FTA)</a></li>
<li><a href="/displays/pit">Pit</a></li>
<li><a href="/displays/queueing">Queueing</a></li>
<li class="divider"></li>

View File

@@ -8,6 +8,9 @@
<html>
<head>
<title>Field Monitor - {{.EventSettings.Name}} - Cheesy Arena</title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="width=device-width, maximum-scale=1.0, user-scalable=no" />
<link rel="shortcut icon" href="/static/img/favicon.ico">
<link rel="stylesheet" href="/static/css/lib/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/cheesy-arena.css" />
@@ -42,7 +45,11 @@
{{define "team"}}
<div id="{{.side}}Team{{.position}}" class="team">
<div id="{{.side}}Team{{.position}}Id" class="team-id center"></div>
<div id="{{.side}}Team{{.position}}Id" class="team-id center fta-dependent"></div>
<div id="{{.side}}Team{{.position}}Notes" class="team-notes fta-dependent" title="FTA Notes">
<i class="glyphicon glyphicon-comment"></i>
<div onclick="editFtaNotes(this);"></div>
</div>
<div class="team-box-row">
<div id="{{.side}}Team{{.position}}Ethernet" class="team-box center"
title="Driver Station Ethernet Connected&#10;Trip Time (ms)">ETH</div>

View File

@@ -8,12 +8,19 @@ package web
import (
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/websocket"
"github.com/mitchellh/mapstructure"
"io"
"log"
"net/http"
)
// Renders the field monitor display.
func (web *Web) fieldMonitorDisplayHandler(w http.ResponseWriter, r *http.Request) {
if !web.enforceDisplayConfiguration(w, r, map[string]string{"reversed": "false"}) {
if r.URL.Query().Get("fta") == "true" && !web.userIsAdmin(w, r) {
return
}
if !web.enforceDisplayConfiguration(w, r, map[string]string{"reversed": "false", "fta": "false"}) {
return
}
@@ -34,6 +41,11 @@ func (web *Web) fieldMonitorDisplayHandler(w http.ResponseWriter, r *http.Reques
// The websocket endpoint for the field monitor display client to receive status updates.
func (web *Web) fieldMonitorDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
isFta := r.URL.Query().Get("fta") == "true"
if isFta && !web.userIsAdmin(w, r) {
return
}
display, err := web.registerDisplay(r)
if err != nil {
handleWebErr(w, err)
@@ -48,7 +60,50 @@ func (web *Web) fieldMonitorDisplayWebsocketHandler(w http.ResponseWriter, r *ht
}
defer ws.Close()
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(display.Notifier, web.arena.ArenaStatusNotifier, web.arena.EventStatusNotifier,
// Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine.
go ws.HandleNotifiers(display.Notifier, web.arena.ArenaStatusNotifier, web.arena.EventStatusNotifier,
web.arena.ReloadDisplaysNotifier)
// Loop, waiting for commands and responding to them, until the client closes the connection.
for {
command, data, err := ws.Read()
if err != nil {
if err == io.EOF {
// Client has closed the connection; nothing to do here.
return
}
log.Println(err)
return
}
if command == "updateTeamNotes" {
if isFta {
args := struct {
Station string
Notes string
}{}
err = mapstructure.Decode(data, &args)
if err != nil {
ws.WriteError(err.Error())
continue
}
if allianceStation, ok := web.arena.AllianceStations[args.Station]; ok {
if allianceStation.Team != nil {
allianceStation.Team.FtaNotes = args.Notes
if err := web.arena.Database.SaveTeam(allianceStation.Team); err != nil {
ws.WriteError(err.Error())
}
web.arena.ArenaStatusNotifier.Notify()
} else {
ws.WriteError("No team present")
}
} else {
ws.WriteError("Invalid alliance station")
}
} else {
ws.WriteError("Must be in FTA mode to update team notes")
}
}
}
}

View File

@@ -13,17 +13,19 @@ import (
func TestFieldMonitorDisplay(t *testing.T) {
web := setupTestWeb(t)
recorder := web.getHttpResponse("/displays/field_monitor?displayId=1&reversed=false")
recorder := web.getHttpResponse("/displays/field_monitor?displayId=1&fta=true&reversed=false")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Field Monitor - Untitled Event - Cheesy Arena")
}
func TestFieldMonitorDisplayWebsocket(t *testing.T) {
web := setupTestWeb(t)
assert.Nil(t, web.arena.SubstituteTeam(254, "B1"))
server, wsUrl := web.startTestServer()
defer server.Close()
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/field_monitor/websocket?displayId=1", nil)
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/field_monitor/websocket?displayId=1&fta=false",
nil)
assert.Nil(t, err)
defer conn.Close()
ws := websocket.NewTestWebsocket(conn)
@@ -32,4 +34,38 @@ func TestFieldMonitorDisplayWebsocket(t *testing.T) {
readWebsocketType(t, ws, "displayConfiguration")
readWebsocketType(t, ws, "arenaStatus")
readWebsocketType(t, ws, "eventStatus")
// Should not be able to update team notes.
ws.Write("updateTeamNotes", map[string]interface{}{"station": "B1", "notes": "Bypassed in M1"})
assert.Contains(t, readWebsocketError(t, ws), "Must be in FTA mode to update team notes")
assert.Equal(t, "", web.arena.AllianceStations["B1"].Team.FtaNotes)
}
func TestFieldMonitorFtaDisplayWebsocket(t *testing.T) {
web := setupTestWeb(t)
assert.Nil(t, web.arena.SubstituteTeam(254, "B1"))
server, wsUrl := web.startTestServer()
defer server.Close()
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/field_monitor/websocket?displayId=1&fta=true",
nil)
assert.Nil(t, err)
defer conn.Close()
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "displayConfiguration")
readWebsocketType(t, ws, "arenaStatus")
readWebsocketType(t, ws, "eventStatus")
// Should not be able to update team notes.
ws.Write("updateTeamNotes", map[string]interface{}{"station": "B1", "notes": "Bypassed in M1"})
readWebsocketType(t, ws, "arenaStatus")
assert.Equal(t, "Bypassed in M1", web.arena.AllianceStations["B1"].Team.FtaNotes)
// Check error scenarios.
ws.Write("updateTeamNotes", map[string]interface{}{"station": "N", "notes": "Bypassed in M2"})
assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station")
ws.Write("updateTeamNotes", map[string]interface{}{"station": "R3", "notes": "Bypassed in M3"})
assert.Contains(t, readWebsocketError(t, ws), "No team present")
}

View File

@@ -63,7 +63,7 @@ func (notifier *Notifier) notifyListener(listener chan messageEnvelope, message
case listener <- message:
// The notification was sent and received successfully.
default:
log.Println("Failed to send a notification due to blocked listener.")
log.Printf("Failed to send a '%s' notification due to blocked listener.", notifier.messageType)
}
}