mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Add FTA variant of field monitor with ability to save notes (closes #58).
This commit is contained in:
@@ -11,7 +11,8 @@ CREATE TABLE teams (
|
||||
accomplishments VARCHAR(1000),
|
||||
wpakey VARCHAR(16),
|
||||
yellowcard bool,
|
||||
hasconnected bool
|
||||
hasconnected bool,
|
||||
ftanotes VARCHAR(1000)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
@@ -18,6 +18,7 @@ type Team struct {
|
||||
WpaKey string
|
||||
YellowCard bool
|
||||
HasConnected bool
|
||||
FtaNotes string
|
||||
}
|
||||
|
||||
func (database *Database) CreateTeam(team *Team) error {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 Trip Time (ms)">ETH</div>
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user