mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Merge branch 'master' into lite
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
|
||||
|
||||
120
field/arena.go
120
field/arena.go
@@ -77,8 +77,6 @@ type Arena struct {
|
||||
MuteMatchSounds bool
|
||||
matchAborted bool
|
||||
soundsPlayed map[*game.MatchSound]struct{}
|
||||
RedControlPanel *game.ControlPanel
|
||||
BlueControlPanel *game.ControlPanel
|
||||
}
|
||||
|
||||
type AllianceStation struct {
|
||||
@@ -216,13 +214,10 @@ func (arena *Arena) LoadMatch(match *model.Match) error {
|
||||
arena.FieldVolunteers = false
|
||||
arena.FieldReset = false
|
||||
arena.ScoringPanelRegistry.resetScoreCommitted()
|
||||
arena.RedControlPanel = new(game.ControlPanel)
|
||||
arena.BlueControlPanel = new(game.ControlPanel)
|
||||
|
||||
// Notify any listeners about the new match.
|
||||
arena.MatchLoadNotifier.Notify()
|
||||
arena.RealtimeScoreNotifier.Notify()
|
||||
arena.ControlPanelColorNotifier.Notify()
|
||||
arena.AllianceStationDisplayMode = "match"
|
||||
arena.AllianceStationDisplayModeNotifier.Notify()
|
||||
|
||||
@@ -421,7 +416,7 @@ func (arena *Arena) Update() {
|
||||
case AutoPeriod:
|
||||
auto = true
|
||||
enabled = true
|
||||
if matchTimeSec >= float64(game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec) {
|
||||
if matchTimeSec >= game.GetDurationToAutoEnd().Seconds() {
|
||||
auto = false
|
||||
sendDsPacket = true
|
||||
if game.MatchTiming.PauseDurationSec > 0 {
|
||||
@@ -435,8 +430,7 @@ func (arena *Arena) Update() {
|
||||
case PausePeriod:
|
||||
auto = false
|
||||
enabled = false
|
||||
if matchTimeSec >= float64(game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+
|
||||
game.MatchTiming.PauseDurationSec) {
|
||||
if matchTimeSec >= game.GetDurationToTeleopStart().Seconds() {
|
||||
arena.MatchState = TeleopPeriod
|
||||
auto = false
|
||||
enabled = true
|
||||
@@ -448,8 +442,7 @@ func (arena *Arena) Update() {
|
||||
case TeleopPeriod:
|
||||
auto = false
|
||||
enabled = true
|
||||
if matchTimeSec >= float64(game.MatchTiming.WarmupDurationSec+game.MatchTiming.AutoDurationSec+
|
||||
game.MatchTiming.PauseDurationSec+game.MatchTiming.TeleopDurationSec) {
|
||||
if matchTimeSec >= game.GetDurationToTeleopEnd().Seconds() {
|
||||
arena.MatchState = PostMatch
|
||||
auto = false
|
||||
enabled = false
|
||||
@@ -669,6 +662,11 @@ func (arena *Arena) checkCanStartMatch() error {
|
||||
if arena.Plc.GetFieldEstop() {
|
||||
return fmt.Errorf("Cannot start match while field emergency stop is active.")
|
||||
}
|
||||
for name, status := range arena.Plc.GetArmorBlockStatuses() {
|
||||
if !status {
|
||||
return fmt.Errorf("Cannot start match while PLC ArmorBlock '%s' is not connected.", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -766,16 +764,60 @@ func (arena *Arena) handlePlcInput() {
|
||||
oldRedScore := *redScore
|
||||
blueScore := &arena.BlueRealtimeScore.CurrentScore
|
||||
oldBlueScore := *blueScore
|
||||
matchStartTime := arena.MatchStartTime
|
||||
currentTime := time.Now()
|
||||
teleopStarted := arena.MatchState >= TeleopPeriod
|
||||
|
||||
if arena.Plc.IsEnabled() {
|
||||
// Handle power ports.
|
||||
redPortCells, bluePortCells := arena.Plc.GetPowerPorts()
|
||||
redPowerPort := &arena.RedRealtimeScore.powerPort
|
||||
redPowerPort.UpdateState(redPortCells, redScore.CellCountingStage(teleopStarted), matchStartTime, currentTime)
|
||||
redScore.AutoCellsBottom = redPowerPort.AutoCellsBottom
|
||||
redScore.AutoCellsOuter = redPowerPort.AutoCellsOuter
|
||||
redScore.AutoCellsInner = redPowerPort.AutoCellsInner
|
||||
redScore.TeleopCellsBottom = redPowerPort.TeleopCellsBottom
|
||||
redScore.TeleopCellsOuter = redPowerPort.TeleopCellsOuter
|
||||
redScore.TeleopCellsInner = redPowerPort.TeleopCellsInner
|
||||
bluePowerPort := &arena.BlueRealtimeScore.powerPort
|
||||
bluePowerPort.UpdateState(bluePortCells, blueScore.CellCountingStage(teleopStarted), matchStartTime,
|
||||
currentTime)
|
||||
blueScore.AutoCellsBottom = bluePowerPort.AutoCellsBottom
|
||||
blueScore.AutoCellsOuter = bluePowerPort.AutoCellsOuter
|
||||
blueScore.AutoCellsInner = bluePowerPort.AutoCellsInner
|
||||
blueScore.TeleopCellsBottom = bluePowerPort.TeleopCellsBottom
|
||||
blueScore.TeleopCellsOuter = bluePowerPort.TeleopCellsOuter
|
||||
blueScore.TeleopCellsInner = bluePowerPort.TeleopCellsInner
|
||||
|
||||
// Handle control panel.
|
||||
redColor, redSegmentCount, blueColor, blueSegmentCount := arena.Plc.GetControlPanels()
|
||||
redControlPanel := &arena.RedRealtimeScore.ControlPanel
|
||||
redControlPanel.CurrentColor = redColor
|
||||
redControlPanel.UpdateState(redSegmentCount, redScore.StageAtCapacity(game.Stage2, teleopStarted),
|
||||
redScore.StageAtCapacity(game.Stage3, teleopStarted), currentTime)
|
||||
redScore.ControlPanelStatus = redControlPanel.ControlPanelStatus
|
||||
blueControlPanel := &arena.BlueRealtimeScore.ControlPanel
|
||||
blueControlPanel.CurrentColor = blueColor
|
||||
blueControlPanel.UpdateState(blueSegmentCount, blueScore.StageAtCapacity(game.Stage2, teleopStarted),
|
||||
blueScore.StageAtCapacity(game.Stage3, teleopStarted), currentTime)
|
||||
blueScore.ControlPanelStatus = blueControlPanel.ControlPanelStatus
|
||||
|
||||
// Handle shield generator rungs.
|
||||
if game.ShouldAssessRung(matchStartTime, currentTime) {
|
||||
redScore.RungIsLevel, blueScore.RungIsLevel = arena.Plc.GetRungs()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if either alliance has reached Stage 3 capacity.
|
||||
if redScore.StageAtCapacity(game.Stage3, arena.MatchState >= TeleopPeriod) &&
|
||||
redScore.Stage3TargetColor == game.ColorUnknown ||
|
||||
redScore.PositionControlTargetColor == game.ColorUnknown ||
|
||||
blueScore.StageAtCapacity(game.Stage3, arena.MatchState >= TeleopPeriod) &&
|
||||
blueScore.Stage3TargetColor == game.ColorUnknown {
|
||||
blueScore.PositionControlTargetColor == game.ColorUnknown {
|
||||
// Determine the position control target colors and send packets to inform the driver stations.
|
||||
redScore.Stage3TargetColor = arena.RedControlPanel.GetStage3TargetColor()
|
||||
blueScore.Stage3TargetColor = arena.BlueControlPanel.GetStage3TargetColor()
|
||||
arena.sendGameDataPacket(redScore.Stage3TargetColor, "R1", "R2", "R3")
|
||||
arena.sendGameDataPacket(blueScore.Stage3TargetColor, "B1", "B2", "B3")
|
||||
redScore.PositionControlTargetColor = arena.RedRealtimeScore.ControlPanel.GetPositionControlTargetColor()
|
||||
blueScore.PositionControlTargetColor = arena.BlueRealtimeScore.ControlPanel.GetPositionControlTargetColor()
|
||||
arena.sendGameDataPacket(redScore.PositionControlTargetColor, "R1", "R2", "R3")
|
||||
arena.sendGameDataPacket(blueScore.PositionControlTargetColor, "B1", "B2", "B3")
|
||||
}
|
||||
|
||||
if !oldRedScore.Equals(redScore) || !oldBlueScore.Equals(blueScore) {
|
||||
@@ -783,7 +825,13 @@ func (arena *Arena) handlePlcInput() {
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the PLC's coils based on its inputs and the current scoring state.
|
||||
func (arena *Arena) handlePlcOutput() {
|
||||
matchStartTime := arena.MatchStartTime
|
||||
currentTime := time.Now()
|
||||
redScore := &arena.RedRealtimeScore.CurrentScore
|
||||
blueScore := &arena.BlueRealtimeScore.CurrentScore
|
||||
|
||||
switch arena.MatchState {
|
||||
case PreMatch:
|
||||
if arena.lastMatchState != PreMatch {
|
||||
@@ -812,14 +860,48 @@ func (arena *Arena) handlePlcOutput() {
|
||||
scoreReady := arena.RedRealtimeScore.FoulsCommitted && arena.BlueRealtimeScore.FoulsCommitted &&
|
||||
arena.alliancePostMatchScoreReady("red") && arena.alliancePostMatchScoreReady("blue")
|
||||
arena.Plc.SetStackLights(false, false, !scoreReady, false)
|
||||
|
||||
if arena.lastMatchState != PostMatch {
|
||||
go func() {
|
||||
time.Sleep(time.Second * game.PowerPortTeleopGracePeriodSec)
|
||||
arena.Plc.SetPowerPortMotors(false)
|
||||
}()
|
||||
}
|
||||
arena.Plc.SetStageActivatedLights([3]bool{false, false, false}, [3]bool{false, false, false})
|
||||
arena.Plc.SetControlPanelLights(false, false)
|
||||
case AutoPeriod:
|
||||
arena.Plc.SetStackLights(false, false, false, true)
|
||||
arena.Plc.SetPowerPortMotors(true)
|
||||
fallthrough
|
||||
case PausePeriod:
|
||||
fallthrough
|
||||
case TeleopPeriod:
|
||||
arena.Plc.SetStackLights(false, false, false, true)
|
||||
if arena.lastMatchState != TeleopPeriod {
|
||||
arena.Plc.SetStageActivatedLights(arena.RedScoreSummary().StagesActivated,
|
||||
arena.BlueScoreSummary().StagesActivated)
|
||||
|
||||
controlPanelLightState := func(state game.ControlPanelLightState) bool {
|
||||
switch state {
|
||||
case game.ControlPanelLightOn:
|
||||
return true
|
||||
case game.ControlPanelLightFlashing:
|
||||
return arena.Plc.GetCycleState(2, 0, 2)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
arena.Plc.SetControlPanelLights(
|
||||
controlPanelLightState(arena.RedRealtimeScore.ControlPanel.ControlPanelLightState),
|
||||
controlPanelLightState(arena.BlueRealtimeScore.ControlPanel.ControlPanelLightState))
|
||||
|
||||
// If the PLC reports a ball jam, blink the orange light and the power port color.
|
||||
redJam, blueJam := arena.Plc.GetPowerPortJams()
|
||||
blink := arena.Plc.GetCycleState(2, 0, 2)
|
||||
arena.Plc.SetStackLights(redJam && blink, blueJam && blink, (redJam || blueJam) && !blink, true)
|
||||
}
|
||||
|
||||
if game.ShouldAssessRung(matchStartTime, currentTime) {
|
||||
arena.Plc.SetShieldGeneratorLights(redScore.RungIsLevel, blueScore.RungIsLevel)
|
||||
} else {
|
||||
arena.Plc.SetShieldGeneratorLights(false, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ type ArenaNotifiers struct {
|
||||
ReloadDisplaysNotifier *websocket.Notifier
|
||||
ScorePostedNotifier *websocket.Notifier
|
||||
ScoringStatusNotifier *websocket.Notifier
|
||||
ControlPanelColorNotifier *websocket.Notifier
|
||||
}
|
||||
|
||||
type MatchTimeMessage struct {
|
||||
@@ -41,6 +40,7 @@ type MatchTimeMessage struct {
|
||||
type audienceAllianceScoreFields struct {
|
||||
Score *game.Score
|
||||
ScoreSummary *game.ScoreSummary
|
||||
ControlPanel *game.ControlPanel
|
||||
}
|
||||
|
||||
// Instantiates notifiers and configures their message producing methods.
|
||||
@@ -63,7 +63,6 @@ func (arena *Arena) configureNotifiers() {
|
||||
arena.ReloadDisplaysNotifier = websocket.NewNotifier("reload", nil)
|
||||
arena.ScorePostedNotifier = websocket.NewNotifier("scorePosted", arena.generateScorePostedMessage)
|
||||
arena.ScoringStatusNotifier = websocket.NewNotifier("scoringStatus", arena.generateScoringStatusMessage)
|
||||
arena.ControlPanelColorNotifier = websocket.NewNotifier("controlPanelColor", arena.generateControlPanelColorMessage)
|
||||
}
|
||||
|
||||
func (arena *Arena) generateAllianceSelectionMessage() interface{} {
|
||||
@@ -90,11 +89,13 @@ func (arena *Arena) generateArenaStatusMessage() interface{} {
|
||||
AllianceStations map[string]*AllianceStation
|
||||
TeamWifiStatuses map[string]network.TeamWifiStatus
|
||||
MatchState
|
||||
CanStartMatch bool
|
||||
PlcIsHealthy bool
|
||||
FieldEstop bool
|
||||
CanStartMatch bool
|
||||
PlcIsHealthy bool
|
||||
FieldEstop bool
|
||||
PlcArmorBlockStatuses map[string]bool
|
||||
}{arena.CurrentMatch.Id, arena.AllianceStations, teamWifiStatuses, arena.MatchState,
|
||||
arena.checkCanStartMatch() == nil, arena.Plc.IsHealthy, arena.Plc.GetFieldEstop()}
|
||||
arena.checkCanStartMatch() == nil, arena.Plc.IsHealthy, arena.Plc.GetFieldEstop(),
|
||||
arena.Plc.GetArmorBlockStatuses()}
|
||||
}
|
||||
|
||||
func (arena *Arena) generateAudienceDisplayModeMessage() interface{} {
|
||||
@@ -236,19 +237,13 @@ func (arena *Arena) generateScoringStatusMessage() interface{} {
|
||||
arena.ScoringPanelRegistry.GetNumPanels("blue"), arena.ScoringPanelRegistry.GetNumScoreCommitted("blue")}
|
||||
}
|
||||
|
||||
func (arena *Arena) generateControlPanelColorMessage() interface{} {
|
||||
return &struct {
|
||||
RedControlPanelColor game.ControlPanelColor
|
||||
BlueControlPanelColor game.ControlPanelColor
|
||||
}{arena.RedControlPanel.CurrentColor, arena.BlueControlPanel.CurrentColor}
|
||||
}
|
||||
|
||||
// Constructs the data object for one alliance sent to the audience display for the realtime scoring overlay.
|
||||
func getAudienceAllianceScoreFields(allianceScore *RealtimeScore,
|
||||
allianceScoreSummary *game.ScoreSummary) *audienceAllianceScoreFields {
|
||||
fields := new(audienceAllianceScoreFields)
|
||||
fields.Score = &allianceScore.CurrentScore
|
||||
fields.ScoreSummary = allianceScoreSummary
|
||||
fields.ControlPanel = &allianceScore.ControlPanel
|
||||
return fields
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ type RealtimeScore struct {
|
||||
CurrentScore game.Score
|
||||
Cards map[string]string
|
||||
FoulsCommitted bool
|
||||
powerPort game.PowerPort
|
||||
ControlPanel game.ControlPanel
|
||||
}
|
||||
|
||||
func NewRealtimeScore() *RealtimeScore {
|
||||
|
||||
@@ -5,19 +5,33 @@
|
||||
|
||||
package game
|
||||
|
||||
import "math/rand"
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ControlPanel struct {
|
||||
CurrentColor ControlPanelColor
|
||||
ControlPanelStatus
|
||||
ControlPanelLightState
|
||||
rotationStarted bool
|
||||
rotationStartSegmentCount int
|
||||
lastSegmentCountDiff int
|
||||
rotationStopTime time.Time
|
||||
positionTargetColor ControlPanelColor
|
||||
lastPositionCorrect bool
|
||||
positionStopTime time.Time
|
||||
}
|
||||
|
||||
type ControlPanelColor int
|
||||
|
||||
// This ordering matches the values in the official FRC PLC code: 0:UnknownError, 1:Red, 2:Blue, 3:Green, 4:Yellow
|
||||
const (
|
||||
ColorUnknown ControlPanelColor = iota
|
||||
ColorRed
|
||||
ColorGreen
|
||||
ColorBlue
|
||||
ColorGreen
|
||||
ColorYellow
|
||||
)
|
||||
|
||||
@@ -29,17 +43,55 @@ const (
|
||||
ControlPanelPosition
|
||||
)
|
||||
|
||||
// Returns a random color that does not match the current color.
|
||||
func (controlPanel *ControlPanel) GetStage3TargetColor() ControlPanelColor {
|
||||
if controlPanel.CurrentColor == ColorUnknown {
|
||||
// If the sensor or manual scorekeeping did not detect/set the current color, pick one of the four at random.
|
||||
return ControlPanelColor(rand.Intn(4) + 1)
|
||||
type ControlPanelLightState int
|
||||
|
||||
const (
|
||||
ControlPanelLightOff ControlPanelLightState = iota
|
||||
ControlPanelLightOn
|
||||
ControlPanelLightFlashing
|
||||
)
|
||||
|
||||
const (
|
||||
rotationControlMinSegments = 24
|
||||
rotationControlMaxSegments = 40
|
||||
rotationControlStopDurationSec = 2
|
||||
positionControlStopMinDurationSec = 3
|
||||
positionControlStopMaxDurationSec = 5
|
||||
)
|
||||
|
||||
// Updates the internal state of the control panel given the current state of the hardware counts and the rest of the
|
||||
// score.
|
||||
func (controlPanel *ControlPanel) UpdateState(segmentCount int, stage2AtCapacity, stage3AtCapacity bool,
|
||||
currentTime time.Time) {
|
||||
if !stage2AtCapacity {
|
||||
controlPanel.ControlPanelStatus = ControlPanelNone
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightOff
|
||||
} else if controlPanel.ControlPanelStatus == ControlPanelNone {
|
||||
controlPanel.assessRotationControl(segmentCount, currentTime)
|
||||
} else if controlPanel.ControlPanelStatus == ControlPanelRotation && stage3AtCapacity {
|
||||
controlPanel.assessPositionControl(currentTime)
|
||||
} else {
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightOff
|
||||
}
|
||||
newColor := int(controlPanel.CurrentColor) + rand.Intn(3) + 1
|
||||
if newColor > 4 {
|
||||
newColor -= 4
|
||||
}
|
||||
|
||||
// Returns the target color for position control, assigning it randomly if it is not yet designated.
|
||||
func (controlPanel *ControlPanel) GetPositionControlTargetColor() ControlPanelColor {
|
||||
if controlPanel.positionTargetColor == ColorUnknown {
|
||||
if controlPanel.CurrentColor == ColorUnknown {
|
||||
// If the sensor or manual scorekeeping did not detect/set the current color, pick one of the four at
|
||||
// random.
|
||||
controlPanel.positionTargetColor = ControlPanelColor(rand.Intn(4) + 1)
|
||||
} else {
|
||||
// Randomly pick one of the non-current colors.
|
||||
newColor := int(controlPanel.CurrentColor) + rand.Intn(3) + 1
|
||||
if newColor > 4 {
|
||||
newColor -= 4
|
||||
}
|
||||
controlPanel.positionTargetColor = ControlPanelColor(newColor)
|
||||
}
|
||||
}
|
||||
return ControlPanelColor(newColor)
|
||||
return controlPanel.positionTargetColor
|
||||
}
|
||||
|
||||
// Returns the string that is to be sent to the driver station for the given color.
|
||||
@@ -47,12 +99,68 @@ func GetGameDataForColor(color ControlPanelColor) string {
|
||||
switch color {
|
||||
case ColorRed:
|
||||
return "R"
|
||||
case ColorGreen:
|
||||
return "G"
|
||||
case ColorBlue:
|
||||
return "B"
|
||||
case ColorGreen:
|
||||
return "G"
|
||||
case ColorYellow:
|
||||
return "Y"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Updates the state of the control panel while rotation control is in the process of being performed.
|
||||
func (controlPanel *ControlPanel) assessRotationControl(segmentCount int, currentTime time.Time) {
|
||||
if !controlPanel.rotationStarted {
|
||||
controlPanel.rotationStarted = true
|
||||
controlPanel.rotationStartSegmentCount = segmentCount
|
||||
}
|
||||
|
||||
segmentCountDiff := int(math.Abs(float64(segmentCount - controlPanel.rotationStartSegmentCount)))
|
||||
if segmentCountDiff < rotationControlMinSegments {
|
||||
// The control panel still needs to be rotated more.
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightOn
|
||||
} else if segmentCountDiff < rotationControlMaxSegments {
|
||||
// The control panel has been rotated the correct amount and needs to stop on a single color.
|
||||
if segmentCountDiff != controlPanel.lastSegmentCountDiff {
|
||||
// The control panel is still moving; reset the timer.
|
||||
controlPanel.rotationStopTime = currentTime
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightFlashing
|
||||
} else if currentTime.Sub(controlPanel.rotationStopTime) < rotationControlStopDurationSec*time.Second {
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightFlashing
|
||||
} else {
|
||||
// The control panel has been stopped long enough; rotation control is complete.
|
||||
controlPanel.ControlPanelStatus = ControlPanelRotation
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightOff
|
||||
}
|
||||
} else {
|
||||
// The control panel has been rotated too much; reset the count.
|
||||
controlPanel.rotationStartSegmentCount = segmentCount
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightOn
|
||||
}
|
||||
controlPanel.lastSegmentCountDiff = segmentCountDiff
|
||||
}
|
||||
|
||||
// Updates the state of the control panel while position control is in the process of being performed.
|
||||
func (controlPanel *ControlPanel) assessPositionControl(currentTime time.Time) {
|
||||
positionCorrect := controlPanel.CurrentColor == controlPanel.GetPositionControlTargetColor() &&
|
||||
controlPanel.CurrentColor != ColorUnknown
|
||||
if positionCorrect && !controlPanel.lastPositionCorrect {
|
||||
controlPanel.positionStopTime = currentTime
|
||||
}
|
||||
controlPanel.lastPositionCorrect = positionCorrect
|
||||
|
||||
if !positionCorrect {
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightOn
|
||||
} else if currentTime.Sub(controlPanel.positionStopTime) < positionControlStopMinDurationSec*time.Second {
|
||||
// The control panel is on the target color but may still be moving.
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightOn
|
||||
} else if currentTime.Sub(controlPanel.positionStopTime) < positionControlStopMaxDurationSec*time.Second {
|
||||
// The control panel is stopped on the target color, but not long enough to count.
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightFlashing
|
||||
} else {
|
||||
// The target color has been present for long enough; position control is complete.
|
||||
controlPanel.ControlPanelStatus = ControlPanelPosition
|
||||
controlPanel.ControlPanelLightState = ControlPanelLightOff
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,38 +7,150 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestControlPanelGetStage3TargetColor(t *testing.T) {
|
||||
func TestControlPanelGetPositionControlTargetColor(t *testing.T) {
|
||||
rand.Seed(0)
|
||||
var controlPanel ControlPanel
|
||||
|
||||
controlPanel.CurrentColor = ColorUnknown
|
||||
results := getStage3TargetColorNTimes(&controlPanel, 10000)
|
||||
results := getPositionTargetColorNTimes(&controlPanel, 10000)
|
||||
assert.Equal(t, [5]int{0, 2543, 2527, 2510, 2420}, results)
|
||||
|
||||
controlPanel.CurrentColor = ColorRed
|
||||
results = getStage3TargetColorNTimes(&controlPanel, 10000)
|
||||
results = getPositionTargetColorNTimes(&controlPanel, 10000)
|
||||
assert.Equal(t, [5]int{0, 0, 3351, 3311, 3338}, results)
|
||||
|
||||
controlPanel.CurrentColor = ColorGreen
|
||||
results = getStage3TargetColorNTimes(&controlPanel, 10000)
|
||||
controlPanel.CurrentColor = ColorBlue
|
||||
results = getPositionTargetColorNTimes(&controlPanel, 10000)
|
||||
assert.Equal(t, [5]int{0, 3335, 0, 3320, 3345}, results)
|
||||
|
||||
controlPanel.CurrentColor = ColorBlue
|
||||
results = getStage3TargetColorNTimes(&controlPanel, 10000)
|
||||
controlPanel.CurrentColor = ColorGreen
|
||||
results = getPositionTargetColorNTimes(&controlPanel, 10000)
|
||||
assert.Equal(t, [5]int{0, 3328, 3296, 0, 3376}, results)
|
||||
|
||||
controlPanel.CurrentColor = ColorYellow
|
||||
results = getStage3TargetColorNTimes(&controlPanel, 10000)
|
||||
results = getPositionTargetColorNTimes(&controlPanel, 10000)
|
||||
assert.Equal(t, [5]int{0, 3303, 3388, 3309, 0}, results)
|
||||
}
|
||||
|
||||
func TestGetGameDataForColor(t *testing.T) {
|
||||
assert.Equal(t, "", GetGameDataForColor(ColorUnknown))
|
||||
assert.Equal(t, "R", GetGameDataForColor(ColorRed))
|
||||
assert.Equal(t, "B", GetGameDataForColor(ColorBlue))
|
||||
assert.Equal(t, "G", GetGameDataForColor(ColorGreen))
|
||||
assert.Equal(t, "Y", GetGameDataForColor(ColorYellow))
|
||||
assert.Equal(t, "", GetGameDataForColor(-100))
|
||||
}
|
||||
|
||||
func TestControlPanelUpdateState(t *testing.T) {
|
||||
rand.Seed(0)
|
||||
var controlPanel ControlPanel
|
||||
controlPanel.ControlPanelStatus = ControlPanelRotation
|
||||
currentTime := time.Now()
|
||||
|
||||
// Check before Stage 2 capacity is reached.
|
||||
controlPanel.UpdateState(0, false, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOff, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(30, false, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOff, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(50, false, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOff, controlPanel.ControlPanelLightState)
|
||||
|
||||
// Check rotation control.
|
||||
controlPanel.UpdateState(60, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(80, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(37, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(36, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightFlashing, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(40, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(35, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightFlashing, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(21, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightFlashing, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(20, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(44, true, false, currentTime)
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightFlashing, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(55, true, false, currentTime.Add(1*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightFlashing, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(55, true, false, currentTime.Add(2000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelNone, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightFlashing, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(55, true, false, currentTime.Add(2001*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOff, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(-1000, true, false, currentTime.Add(3000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOff, controlPanel.ControlPanelLightState)
|
||||
|
||||
// Check position control.
|
||||
assert.Equal(t, ColorUnknown, controlPanel.positionTargetColor)
|
||||
controlPanel.UpdateState(1000, true, true, currentTime.Add(5000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
assert.Equal(t, ColorGreen, controlPanel.GetPositionControlTargetColor())
|
||||
controlPanel.CurrentColor = ColorBlue
|
||||
controlPanel.UpdateState(1001, true, true, currentTime.Add(6000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.CurrentColor = ColorGreen
|
||||
controlPanel.UpdateState(1002, true, true, currentTime.Add(7000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(1002, true, true, currentTime.Add(9999*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(1002, true, true, currentTime.Add(10000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightFlashing, controlPanel.ControlPanelLightState)
|
||||
controlPanel.CurrentColor = ColorYellow
|
||||
controlPanel.UpdateState(1003, true, true, currentTime.Add(11000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(1003, true, true, currentTime.Add(20000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.CurrentColor = ColorGreen
|
||||
controlPanel.UpdateState(1002, true, true, currentTime.Add(21000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOn, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(1002, true, true, currentTime.Add(25999*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelRotation, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightFlashing, controlPanel.ControlPanelLightState)
|
||||
controlPanel.UpdateState(1002, true, true, currentTime.Add(26000*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelPosition, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOff, controlPanel.ControlPanelLightState)
|
||||
controlPanel.CurrentColor = ColorRed
|
||||
controlPanel.UpdateState(0, true, true, currentTime.Add(26001*time.Millisecond))
|
||||
assert.Equal(t, ControlPanelPosition, controlPanel.ControlPanelStatus)
|
||||
assert.Equal(t, ControlPanelLightOff, controlPanel.ControlPanelLightState)
|
||||
}
|
||||
|
||||
// Invokes the method N times and returns a map of the counts for each result, for statistical testing.
|
||||
func getStage3TargetColorNTimes(controlPanel *ControlPanel, n int) [5]int {
|
||||
func getPositionTargetColorNTimes(controlPanel *ControlPanel, n int) [5]int {
|
||||
var results [5]int
|
||||
for i := 0; i < n; i++ {
|
||||
results[controlPanel.GetStage3TargetColor()]++
|
||||
controlPanel.positionTargetColor = ColorUnknown
|
||||
results[controlPanel.GetPositionControlTargetColor()]++
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
|
||||
package game
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
powerPortAutoGracePeriodSec = 5
|
||||
PowerPortTeleopGracePeriodSec = 5
|
||||
rungAssessmentDelaySec = 5
|
||||
)
|
||||
|
||||
var MatchTiming = struct {
|
||||
WarmupDurationSec int
|
||||
AutoDurationSec int
|
||||
@@ -13,3 +21,28 @@ var MatchTiming = struct {
|
||||
WarningRemainingDurationSec int
|
||||
TimeoutDurationSec int
|
||||
}{0, 15, 2, 135, 30, 0}
|
||||
|
||||
func GetDurationToAutoEnd() time.Duration {
|
||||
return time.Duration(MatchTiming.WarmupDurationSec+MatchTiming.AutoDurationSec) * time.Second
|
||||
}
|
||||
|
||||
func GetDurationToTeleopStart() time.Duration {
|
||||
return time.Duration(MatchTiming.WarmupDurationSec+MatchTiming.AutoDurationSec+MatchTiming.PauseDurationSec) *
|
||||
time.Second
|
||||
}
|
||||
|
||||
func GetDurationToWarning() time.Duration {
|
||||
return time.Duration(MatchTiming.WarmupDurationSec+MatchTiming.AutoDurationSec+MatchTiming.PauseDurationSec+
|
||||
MatchTiming.TeleopDurationSec-MatchTiming.WarningRemainingDurationSec) * time.Second
|
||||
}
|
||||
|
||||
func GetDurationToTeleopEnd() time.Duration {
|
||||
return time.Duration(MatchTiming.WarmupDurationSec+MatchTiming.AutoDurationSec+MatchTiming.PauseDurationSec+
|
||||
MatchTiming.TeleopDurationSec) * time.Second
|
||||
}
|
||||
|
||||
// Returns true if the given time is within the proper range for assessing the level state of the shield generator rung.
|
||||
func ShouldAssessRung(matchStartTime, currentTime time.Time) bool {
|
||||
return currentTime.After(matchStartTime.Add(GetDurationToWarning())) &&
|
||||
currentTime.Before(matchStartTime.Add(GetDurationToTeleopEnd()+rungAssessmentDelaySec*time.Second))
|
||||
}
|
||||
|
||||
18
game/match_timing_test.go
Normal file
18
game/match_timing_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2020 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
|
||||
package game
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestShouldAssessRung(t *testing.T) {
|
||||
assert.Equal(t, false, ShouldAssessRung(matchStartTime, timeAfterStart(0)))
|
||||
assert.Equal(t, false, ShouldAssessRung(matchStartTime, timeAfterStart(121.9)))
|
||||
assert.Equal(t, true, ShouldAssessRung(matchStartTime, timeAfterStart(122.1)))
|
||||
assert.Equal(t, true, ShouldAssessRung(matchStartTime, timeAfterStart(152.1)))
|
||||
assert.Equal(t, true, ShouldAssessRung(matchStartTime, timeAfterStart(156.9)))
|
||||
assert.Equal(t, false, ShouldAssessRung(matchStartTime, timeAfterStart(157.1)))
|
||||
}
|
||||
54
game/power_port.go
Normal file
54
game/power_port.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2020 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
//
|
||||
// Scoring logic for the 2020 Power Port element.
|
||||
|
||||
package game
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type PowerPort struct {
|
||||
AutoCellsBottom [2]int
|
||||
AutoCellsOuter [2]int
|
||||
AutoCellsInner [2]int
|
||||
TeleopCellsBottom [4]int
|
||||
TeleopCellsOuter [4]int
|
||||
TeleopCellsInner [4]int
|
||||
}
|
||||
|
||||
// Updates the internal counting state of the power port given the current state of the hardware counts. Allows the
|
||||
// score to accumulate before the match, since the counters will be reset in hardware.
|
||||
func (powerPort *PowerPort) UpdateState(portCells [3]int, stage Stage, matchStartTime, currentTime time.Time) {
|
||||
autoValidityDuration := GetDurationToAutoEnd() + powerPortAutoGracePeriodSec*time.Second
|
||||
autoValidityCutoff := matchStartTime.Add(autoValidityDuration)
|
||||
teleopValidityDuration := GetDurationToTeleopEnd() + PowerPortTeleopGracePeriodSec*time.Second
|
||||
teleopValidityCutoff := matchStartTime.Add(teleopValidityDuration)
|
||||
|
||||
newBottomCells := portCells[0] - totalPortCells(powerPort.AutoCellsBottom, powerPort.TeleopCellsBottom)
|
||||
newOuterCells := portCells[1] - totalPortCells(powerPort.AutoCellsOuter, powerPort.TeleopCellsOuter)
|
||||
newInnerCells := portCells[2] - totalPortCells(powerPort.AutoCellsInner, powerPort.TeleopCellsInner)
|
||||
|
||||
if currentTime.Before(autoValidityCutoff) && stage <= Stage2 {
|
||||
powerPort.AutoCellsBottom[stage] += newBottomCells
|
||||
powerPort.AutoCellsOuter[stage] += newOuterCells
|
||||
powerPort.AutoCellsInner[stage] += newInnerCells
|
||||
} else if currentTime.Before(teleopValidityCutoff) {
|
||||
powerPort.TeleopCellsBottom[stage] += newBottomCells
|
||||
powerPort.TeleopCellsOuter[stage] += newOuterCells
|
||||
powerPort.TeleopCellsInner[stage] += newInnerCells
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the total number of cells scored across all stages in a port level.
|
||||
func totalPortCells(autoCells [2]int, teleopCells [4]int) int {
|
||||
var total int
|
||||
for _, stageCount := range autoCells {
|
||||
total += stageCount
|
||||
}
|
||||
for _, stageCount := range teleopCells {
|
||||
total += stageCount
|
||||
}
|
||||
return total
|
||||
}
|
||||
70
game/power_port_test.go
Normal file
70
game/power_port_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright 2020 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
|
||||
package game
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var matchStartTime = time.Unix(10, 0)
|
||||
|
||||
func TestPowerPort(t *testing.T) {
|
||||
var powerPort PowerPort
|
||||
assertPowerPort(t, [3][2]int{}, [3][4]int{}, &powerPort)
|
||||
|
||||
// Check before match start and during the autonomous period.
|
||||
powerPort.UpdateState([3]int{0, 1, 2}, Stage1, matchStartTime, timeAfterStart(-1))
|
||||
assertPowerPort(t, [3][2]int{{0, 0}, {1, 0}, {2, 0}}, [3][4]int{}, &powerPort)
|
||||
powerPort.UpdateState([3]int{0, 0, 0}, Stage1, matchStartTime, timeAfterStart(1))
|
||||
assertPowerPort(t, [3][2]int{{0, 0}, {0, 0}, {0, 0}}, [3][4]int{}, &powerPort)
|
||||
powerPort.UpdateState([3]int{0, 1, 2}, Stage1, matchStartTime, timeAfterStart(2))
|
||||
assertPowerPort(t, [3][2]int{{0, 0}, {1, 0}, {2, 0}}, [3][4]int{}, &powerPort)
|
||||
powerPort.UpdateState([3]int{3, 5, 2}, Stage1, matchStartTime, timeAfterStart(5))
|
||||
assertPowerPort(t, [3][2]int{{3, 0}, {5, 0}, {2, 0}}, [3][4]int{}, &powerPort)
|
||||
|
||||
// Check boundary conditions around the auto end grace period.
|
||||
powerPort.UpdateState([3]int{4, 6, 3}, Stage1, matchStartTime, timeAfterStart(16.9))
|
||||
assertPowerPort(t, [3][2]int{{4, 0}, {6, 0}, {3, 0}}, [3][4]int{}, &powerPort)
|
||||
powerPort.UpdateState([3]int{5, 8, 6}, Stage2, matchStartTime, timeAfterStart(17.1))
|
||||
assertPowerPort(t, [3][2]int{{4, 1}, {6, 2}, {3, 3}}, [3][4]int{}, &powerPort)
|
||||
powerPort.UpdateState([3]int{8, 10, 7}, Stage2, matchStartTime, timeAfterStart(19.9))
|
||||
assertPowerPort(t, [3][2]int{{4, 4}, {6, 4}, {3, 4}}, [3][4]int{}, &powerPort)
|
||||
powerPort.UpdateState([3]int{8, 10, 8}, Stage2, matchStartTime, timeAfterStart(20.1))
|
||||
assertPowerPort(t, [3][2]int{{4, 4}, {6, 4}, {3, 4}}, [3][4]int{{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 1, 0, 0}},
|
||||
&powerPort)
|
||||
|
||||
// Check during the teleoperated period.
|
||||
powerPort.UpdateState([3]int{9, 10, 8}, Stage1, matchStartTime, timeAfterStart(30))
|
||||
assertPowerPort(t, [3][2]int{{4, 4}, {6, 4}, {3, 4}}, [3][4]int{{1, 0, 0, 0}, {0, 0, 0, 0}, {0, 1, 0, 0}},
|
||||
&powerPort)
|
||||
powerPort.UpdateState([3]int{10, 12, 11}, Stage3, matchStartTime, timeAfterStart(30))
|
||||
assertPowerPort(t, [3][2]int{{4, 4}, {6, 4}, {3, 4}}, [3][4]int{{1, 0, 1, 0}, {0, 0, 2, 0}, {0, 1, 3, 0}},
|
||||
&powerPort)
|
||||
powerPort.UpdateState([3]int{40, 32, 21}, StageExtra, matchStartTime, timeAfterStart(60))
|
||||
assertPowerPort(t, [3][2]int{{4, 4}, {6, 4}, {3, 4}}, [3][4]int{{1, 0, 1, 30}, {0, 0, 2, 20}, {0, 1, 3, 10}},
|
||||
&powerPort)
|
||||
|
||||
// Check boundary conditions around the teleop end grace period.
|
||||
powerPort.UpdateState([3]int{41, 32, 21}, StageExtra, matchStartTime, timeAfterStart(156.9))
|
||||
assertPowerPort(t, [3][2]int{{4, 4}, {6, 4}, {3, 4}}, [3][4]int{{1, 0, 1, 31}, {0, 0, 2, 20}, {0, 1, 3, 10}},
|
||||
&powerPort)
|
||||
powerPort.UpdateState([3]int{42, 33, 22}, StageExtra, matchStartTime, timeAfterStart(157.1))
|
||||
assertPowerPort(t, [3][2]int{{4, 4}, {6, 4}, {3, 4}}, [3][4]int{{1, 0, 1, 31}, {0, 0, 2, 20}, {0, 1, 3, 10}},
|
||||
&powerPort)
|
||||
}
|
||||
|
||||
func assertPowerPort(t *testing.T, expectedAutoCells [3][2]int, expectedTeleopCells [3][4]int, powerPort *PowerPort) {
|
||||
assert.Equal(t, expectedAutoCells[0], powerPort.AutoCellsBottom)
|
||||
assert.Equal(t, expectedAutoCells[1], powerPort.AutoCellsOuter)
|
||||
assert.Equal(t, expectedAutoCells[2], powerPort.AutoCellsInner)
|
||||
assert.Equal(t, expectedTeleopCells[0], powerPort.TeleopCellsBottom)
|
||||
assert.Equal(t, expectedTeleopCells[1], powerPort.TeleopCellsOuter)
|
||||
assert.Equal(t, expectedTeleopCells[2], powerPort.TeleopCellsInner)
|
||||
}
|
||||
|
||||
func timeAfterStart(sec float32) time.Time {
|
||||
return matchStartTime.Add(time.Duration(1000*sec) * time.Millisecond)
|
||||
}
|
||||
@@ -16,11 +16,11 @@ type Score struct {
|
||||
TeleopCellsOuter [4]int
|
||||
TeleopCellsInner [4]int
|
||||
ControlPanelStatus
|
||||
EndgameStatuses [3]EndgameStatus
|
||||
RungIsLevel bool
|
||||
Fouls []Foul
|
||||
ElimDq bool
|
||||
Stage3TargetColor ControlPanelColor
|
||||
EndgameStatuses [3]EndgameStatus
|
||||
RungIsLevel bool
|
||||
Fouls []Foul
|
||||
ElimDq bool
|
||||
PositionControlTargetColor ControlPanelColor
|
||||
}
|
||||
|
||||
type ScoreSummary struct {
|
||||
|
||||
@@ -18,6 +18,7 @@ type Team struct {
|
||||
WpaKey string
|
||||
YellowCard bool
|
||||
HasConnected bool
|
||||
FtaNotes string
|
||||
}
|
||||
|
||||
func (database *Database) CreateTeam(team *Team) error {
|
||||
|
||||
@@ -545,7 +545,7 @@ func createTbaScoringBreakdown(match *model.Match, matchResult *model.MatchResul
|
||||
breakdown.Stage1Activated = scoreSummary.StagesActivated[0]
|
||||
breakdown.Stage2Activated = scoreSummary.StagesActivated[1]
|
||||
breakdown.Stage3Activated = scoreSummary.StagesActivated[2]
|
||||
breakdown.Stage3TargetColor = controlPanelColorMapping[score.Stage3TargetColor]
|
||||
breakdown.Stage3TargetColor = controlPanelColorMapping[score.PositionControlTargetColor]
|
||||
breakdown.EndgameRobot1 = endgameMapping[score.EndgameStatuses[0]]
|
||||
breakdown.EndgameRobot2 = endgameMapping[score.EndgameStatuses[1]]
|
||||
breakdown.EndgameRobot3 = endgameMapping[score.EndgameStatuses[2]]
|
||||
|
||||
27
plc/armorblock_string.go
Normal file
27
plc/armorblock_string.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Code generated by "stringer -type=armorBlock"; DO NOT EDIT.
|
||||
|
||||
package plc
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[redDs-0]
|
||||
_ = x[blueDs-1]
|
||||
_ = x[shieldGenerator-2]
|
||||
_ = x[controlPanel-3]
|
||||
_ = x[armorBlockCount-4]
|
||||
}
|
||||
|
||||
const _armorBlock_name = "redDsblueDsshieldGeneratorcontrolPanelarmorBlockCount"
|
||||
|
||||
var _armorBlock_index = [...]uint8{0, 5, 11, 26, 38, 53}
|
||||
|
||||
func (i armorBlock) String() string {
|
||||
if i < 0 || i >= armorBlock(len(_armorBlock_index)-1) {
|
||||
return "armorBlock(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _armorBlock_name[_armorBlock_index[i]:_armorBlock_index[i+1]]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by "stringer -type=coil"; DO NOT EDIT.
|
||||
// Code generated by "stringer -type coil"; DO NOT EDIT.
|
||||
|
||||
package plc
|
||||
|
||||
@@ -16,12 +16,23 @@ func _() {
|
||||
_ = x[stackLightBlue-5]
|
||||
_ = x[stackLightBuzzer-6]
|
||||
_ = x[fieldResetLight-7]
|
||||
_ = x[coilCount-8]
|
||||
_ = x[powerPortMotors-8]
|
||||
_ = x[redStage1Light-9]
|
||||
_ = x[redStage2Light-10]
|
||||
_ = x[redStage3Light-11]
|
||||
_ = x[blueStage1Light-12]
|
||||
_ = x[blueStage2Light-13]
|
||||
_ = x[blueStage3Light-14]
|
||||
_ = x[redTrussLight-15]
|
||||
_ = x[blueTrussLight-16]
|
||||
_ = x[redControlPanelLight-17]
|
||||
_ = x[blueControlPanelLight-18]
|
||||
_ = x[coilCount-19]
|
||||
}
|
||||
|
||||
const _coil_name = "heartbeatmatchResetstackLightGreenstackLightOrangestackLightRedstackLightBluestackLightBuzzerfieldResetLightcoilCount"
|
||||
const _coil_name = "heartbeatmatchResetstackLightGreenstackLightOrangestackLightRedstackLightBluestackLightBuzzerfieldResetLightpowerPortMotorsredStage1LightredStage2LightredStage3LightblueStage1LightblueStage2LightblueStage3LightredTrussLightblueTrussLightredControlPanelLightblueControlPanelLightcoilCount"
|
||||
|
||||
var _coil_index = [...]uint8{0, 9, 19, 34, 50, 63, 77, 93, 108, 117}
|
||||
var _coil_index = [...]uint16{0, 9, 19, 34, 50, 63, 77, 93, 108, 123, 137, 151, 165, 180, 195, 210, 223, 237, 257, 278, 287}
|
||||
|
||||
func (i coil) String() string {
|
||||
if i < 0 || i >= coil(len(_coil_index)-1) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by "stringer -type=input"; DO NOT EDIT.
|
||||
// Code generated by "stringer -type input"; DO NOT EDIT.
|
||||
|
||||
package plc
|
||||
|
||||
@@ -21,12 +21,16 @@ func _() {
|
||||
_ = x[blueConnected1-10]
|
||||
_ = x[blueConnected2-11]
|
||||
_ = x[blueConnected3-12]
|
||||
_ = x[inputCount-13]
|
||||
_ = x[redRungIsLevel-13]
|
||||
_ = x[blueRungIsLevel-14]
|
||||
_ = x[redPowerPortJam-15]
|
||||
_ = x[bluePowerPortJam-16]
|
||||
_ = x[inputCount-17]
|
||||
}
|
||||
|
||||
const _input_name = "fieldEstopredEstop1redEstop2redEstop3blueEstop1blueEstop2blueEstop3redConnected1redConnected2redConnected3blueConnected1blueConnected2blueConnected3inputCount"
|
||||
const _input_name = "fieldEstopredEstop1redEstop2redEstop3blueEstop1blueEstop2blueEstop3redConnected1redConnected2redConnected3blueConnected1blueConnected2blueConnected3redRungIsLevelblueRungIsLevelredPowerPortJambluePowerPortJaminputCount"
|
||||
|
||||
var _input_index = [...]uint8{0, 10, 19, 28, 37, 47, 57, 67, 80, 93, 106, 120, 134, 148, 158}
|
||||
var _input_index = [...]uint8{0, 10, 19, 28, 37, 47, 57, 67, 80, 93, 106, 120, 134, 148, 162, 177, 192, 208, 218}
|
||||
|
||||
func (i input) String() string {
|
||||
if i < 0 || i >= input(len(_input_index)-1) {
|
||||
|
||||
128
plc/plc.go
128
plc/plc.go
@@ -7,9 +7,11 @@ package plc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"github.com/goburrow/modbus"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -52,6 +54,10 @@ const (
|
||||
blueConnected1
|
||||
blueConnected2
|
||||
blueConnected3
|
||||
redRungIsLevel
|
||||
blueRungIsLevel
|
||||
redPowerPortJam
|
||||
bluePowerPortJam
|
||||
inputCount
|
||||
)
|
||||
|
||||
@@ -59,7 +65,28 @@ const (
|
||||
type register int
|
||||
|
||||
const (
|
||||
registerCount register = iota
|
||||
fieldIoConnection register = iota
|
||||
redPowerPortBottom
|
||||
redPowerPortOuter
|
||||
redPowerPortInner
|
||||
bluePowerPortBottom
|
||||
bluePowerPortOuter
|
||||
bluePowerPortInner
|
||||
redControlPanelRed
|
||||
redControlPanelGreen
|
||||
redControlPanelBlue
|
||||
redControlPanelIntensity
|
||||
blueControlPanelRed
|
||||
blueControlPanelGreen
|
||||
blueControlPanelBlue
|
||||
blueControlPanelIntensity
|
||||
redControlPanelColor
|
||||
blueControlPanelColor
|
||||
redControlPanelLastColor
|
||||
blueControlPanelLastColor
|
||||
redControlPanelSegments
|
||||
blueControlPanelSegments
|
||||
registerCount
|
||||
)
|
||||
|
||||
// Coils
|
||||
@@ -74,9 +101,31 @@ const (
|
||||
stackLightBlue
|
||||
stackLightBuzzer
|
||||
fieldResetLight
|
||||
powerPortMotors
|
||||
redStage1Light
|
||||
redStage2Light
|
||||
redStage3Light
|
||||
blueStage1Light
|
||||
blueStage2Light
|
||||
blueStage3Light
|
||||
redTrussLight
|
||||
blueTrussLight
|
||||
redControlPanelLight
|
||||
blueControlPanelLight
|
||||
coilCount
|
||||
)
|
||||
|
||||
// Bitmask for decoding fieldIoConnection into individual ArmorBlock connection statuses.
|
||||
type armorBlock int
|
||||
|
||||
const (
|
||||
redDs armorBlock = iota
|
||||
blueDs
|
||||
shieldGenerator
|
||||
controlPanel
|
||||
armorBlockCount
|
||||
)
|
||||
|
||||
func (plc *Plc) SetAddress(address string) {
|
||||
plc.address = address
|
||||
plc.resetConnection()
|
||||
@@ -116,7 +165,7 @@ func (plc *Plc) Run() {
|
||||
isHealthy := true
|
||||
isHealthy = isHealthy && plc.writeCoils()
|
||||
isHealthy = isHealthy && plc.readInputs()
|
||||
isHealthy = isHealthy && plc.readCounters()
|
||||
isHealthy = isHealthy && plc.readRegisters()
|
||||
if !isHealthy {
|
||||
plc.resetConnection()
|
||||
}
|
||||
@@ -140,6 +189,15 @@ func (plc *Plc) Run() {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a map of ArmorBlocks I/O module names to whether they are connected properly.
|
||||
func (plc *Plc) GetArmorBlockStatuses() map[string]bool {
|
||||
statuses := make(map[string]bool, armorBlockCount)
|
||||
for i := 0; i < int(armorBlockCount); i++ {
|
||||
statuses[strings.Title(armorBlock(i).String())] = plc.registers[fieldIoConnection]&(1<<i) > 0
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
// Returns the state of the field emergency stop button (true if e-stop is active).
|
||||
func (plc *Plc) GetFieldEstop() bool {
|
||||
return plc.IsEnabled() && !plc.inputs[fieldEstop]
|
||||
@@ -173,7 +231,37 @@ func (plc *Plc) GetEthernetConnected() ([3]bool, [3]bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set the on/off state of the stack lights on the scoring table.
|
||||
// Returns the total number of power cells scored since match start in each level of the red and blue power ports.
|
||||
func (plc *Plc) GetPowerPorts() ([3]int, [3]int) {
|
||||
return [3]int{
|
||||
int(plc.registers[redPowerPortBottom]),
|
||||
int(plc.registers[redPowerPortOuter]),
|
||||
int(plc.registers[redPowerPortInner]),
|
||||
},
|
||||
[3]int{
|
||||
int(plc.registers[bluePowerPortBottom]),
|
||||
int(plc.registers[bluePowerPortOuter]),
|
||||
int(plc.registers[bluePowerPortInner]),
|
||||
}
|
||||
}
|
||||
|
||||
// Returns whether each of the red and blue power ports are jammed.
|
||||
func (plc *Plc) GetPowerPortJams() (bool, bool) {
|
||||
return plc.inputs[redPowerPortJam], plc.inputs[bluePowerPortJam]
|
||||
}
|
||||
|
||||
// Returns the current color and number of segment transitions for each of the red and blue control panels.
|
||||
func (plc *Plc) GetControlPanels() (game.ControlPanelColor, int, game.ControlPanelColor, int) {
|
||||
return game.ControlPanelColor(plc.registers[redControlPanelColor]), int(plc.registers[redControlPanelSegments]),
|
||||
game.ControlPanelColor(plc.registers[blueControlPanelColor]), int(plc.registers[blueControlPanelSegments])
|
||||
}
|
||||
|
||||
// Returns whether each of the red and blue rungs is level.
|
||||
func (plc *Plc) GetRungs() (bool, bool) {
|
||||
return plc.inputs[redRungIsLevel], plc.inputs[blueRungIsLevel]
|
||||
}
|
||||
|
||||
// Sets the on/off state of the stack lights on the scoring table.
|
||||
func (plc *Plc) SetStackLights(red, blue, orange, green bool) {
|
||||
plc.coils[stackLightRed] = red
|
||||
plc.coils[stackLightBlue] = blue
|
||||
@@ -181,15 +269,43 @@ func (plc *Plc) SetStackLights(red, blue, orange, green bool) {
|
||||
plc.coils[stackLightGreen] = green
|
||||
}
|
||||
|
||||
// Set the on/off state of the stack lights on the scoring table.
|
||||
// Triggers the "match ready" chime if the state is true.
|
||||
func (plc *Plc) SetStackBuzzer(state bool) {
|
||||
plc.coils[stackLightBuzzer] = state
|
||||
}
|
||||
|
||||
// Sets the on/off state of the field reset light.
|
||||
func (plc *Plc) SetFieldResetLight(state bool) {
|
||||
plc.coils[fieldResetLight] = state
|
||||
}
|
||||
|
||||
// Sets the on/off state of the agitator motors within each power port.
|
||||
func (plc *Plc) SetPowerPortMotors(state bool) {
|
||||
plc.coils[powerPortMotors] = state
|
||||
}
|
||||
|
||||
// Sets the on/off state of the lights mounted within the shield generator trussing.
|
||||
func (plc *Plc) SetStageActivatedLights(red, blue [3]bool) {
|
||||
plc.coils[redStage1Light] = red[0]
|
||||
plc.coils[redStage2Light] = red[1]
|
||||
plc.coils[redStage3Light] = red[2]
|
||||
plc.coils[blueStage1Light] = blue[0]
|
||||
plc.coils[blueStage2Light] = blue[1]
|
||||
plc.coils[blueStage3Light] = blue[2]
|
||||
}
|
||||
|
||||
// Sets the on/off state of the red and blue alliance stack lights mounted to the control panel.
|
||||
func (plc *Plc) SetControlPanelLights(red, blue bool) {
|
||||
plc.coils[redControlPanelLight] = red
|
||||
plc.coils[blueControlPanelLight] = blue
|
||||
}
|
||||
|
||||
// Sets the on/off state of the red and blue alliance stack lights mounted to the top of the shield generator.
|
||||
func (plc *Plc) SetShieldGeneratorLights(red, blue bool) {
|
||||
plc.coils[redTrussLight] = red
|
||||
plc.coils[blueTrussLight] = blue
|
||||
}
|
||||
|
||||
func (plc *Plc) GetCycleState(max, index, duration int) bool {
|
||||
return plc.cycleCounter/duration%max == index
|
||||
}
|
||||
@@ -261,7 +377,7 @@ func (plc *Plc) readInputs() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (plc *Plc) readCounters() bool {
|
||||
func (plc *Plc) readRegisters() bool {
|
||||
if len(plc.registers) == 0 {
|
||||
return true
|
||||
}
|
||||
@@ -272,7 +388,7 @@ func (plc *Plc) readCounters() bool {
|
||||
return false
|
||||
}
|
||||
if len(registers)/2 < len(plc.registers) {
|
||||
log.Printf("Insufficient length of PLC counters: got %d bytes, expected %d words.", len(registers),
|
||||
log.Printf("Insufficient length of PLC registers: got %d bytes, expected %d words.", len(registers),
|
||||
len(plc.registers))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -34,3 +34,32 @@ func TestBoolToByte(t *testing.T) {
|
||||
assert.Equal(t, bools, byteToBool(bytes, len(bools)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArmorBlockStatuses(t *testing.T) {
|
||||
var plc Plc
|
||||
|
||||
plc.registers[fieldIoConnection] = 0
|
||||
assert.Equal(t, map[string]bool{"RedDs": false, "BlueDs": false, "ShieldGenerator": false, "ControlPanel": false},
|
||||
plc.GetArmorBlockStatuses())
|
||||
plc.registers[fieldIoConnection] = 1
|
||||
assert.Equal(t, map[string]bool{"RedDs": true, "BlueDs": false, "ShieldGenerator": false, "ControlPanel": false},
|
||||
plc.GetArmorBlockStatuses())
|
||||
plc.registers[fieldIoConnection] = 2
|
||||
assert.Equal(t, map[string]bool{"RedDs": false, "BlueDs": true, "ShieldGenerator": false, "ControlPanel": false},
|
||||
plc.GetArmorBlockStatuses())
|
||||
plc.registers[fieldIoConnection] = 4
|
||||
assert.Equal(t, map[string]bool{"RedDs": false, "BlueDs": false, "ShieldGenerator": true, "ControlPanel": false},
|
||||
plc.GetArmorBlockStatuses())
|
||||
plc.registers[fieldIoConnection] = 8
|
||||
assert.Equal(t, map[string]bool{"RedDs": false, "BlueDs": false, "ShieldGenerator": false, "ControlPanel": true},
|
||||
plc.GetArmorBlockStatuses())
|
||||
plc.registers[fieldIoConnection] = 5
|
||||
assert.Equal(t, map[string]bool{"RedDs": true, "BlueDs": false, "ShieldGenerator": true, "ControlPanel": false},
|
||||
plc.GetArmorBlockStatuses())
|
||||
plc.registers[fieldIoConnection] = 10
|
||||
assert.Equal(t, map[string]bool{"RedDs": false, "BlueDs": true, "ShieldGenerator": false, "ControlPanel": true},
|
||||
plc.GetArmorBlockStatuses())
|
||||
plc.registers[fieldIoConnection] = 15
|
||||
assert.Equal(t, map[string]bool{"RedDs": true, "BlueDs": true, "ShieldGenerator": true, "ControlPanel": true},
|
||||
plc.GetArmorBlockStatuses())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by "stringer -type=register"; DO NOT EDIT.
|
||||
// Code generated by "stringer -type register"; DO NOT EDIT.
|
||||
|
||||
package plc
|
||||
|
||||
@@ -8,12 +8,33 @@ func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[registerCount-0]
|
||||
_ = x[fieldIoConnection-0]
|
||||
_ = x[redPowerPortBottom-1]
|
||||
_ = x[redPowerPortOuter-2]
|
||||
_ = x[redPowerPortInner-3]
|
||||
_ = x[bluePowerPortBottom-4]
|
||||
_ = x[bluePowerPortOuter-5]
|
||||
_ = x[bluePowerPortInner-6]
|
||||
_ = x[redControlPanelRed-7]
|
||||
_ = x[redControlPanelGreen-8]
|
||||
_ = x[redControlPanelBlue-9]
|
||||
_ = x[redControlPanelIntensity-10]
|
||||
_ = x[blueControlPanelRed-11]
|
||||
_ = x[blueControlPanelGreen-12]
|
||||
_ = x[blueControlPanelBlue-13]
|
||||
_ = x[blueControlPanelIntensity-14]
|
||||
_ = x[redControlPanelColor-15]
|
||||
_ = x[blueControlPanelColor-16]
|
||||
_ = x[redControlPanelLastColor-17]
|
||||
_ = x[blueControlPanelLastColor-18]
|
||||
_ = x[redControlPanelSegments-19]
|
||||
_ = x[blueControlPanelSegments-20]
|
||||
_ = x[registerCount-21]
|
||||
}
|
||||
|
||||
const _register_name = "registerCount"
|
||||
const _register_name = "fieldIoConnectionredPowerPortBottomredPowerPortOuterredPowerPortInnerbluePowerPortBottombluePowerPortOuterbluePowerPortInnerredControlPanelRedredControlPanelGreenredControlPanelBlueredControlPanelIntensityblueControlPanelRedblueControlPanelGreenblueControlPanelBlueblueControlPanelIntensityredControlPanelColorblueControlPanelColorredControlPanelLastColorblueControlPanelLastColorredControlPanelSegmentsblueControlPanelSegmentsregisterCount"
|
||||
|
||||
var _register_index = [...]uint8{0, 13}
|
||||
var _register_index = [...]uint16{0, 17, 35, 52, 69, 88, 106, 124, 142, 162, 181, 205, 224, 245, 265, 290, 310, 331, 355, 380, 403, 427, 440}
|
||||
|
||||
func (i register) String() string {
|
||||
if i < 0 || i >= register(len(_register_index)-1) {
|
||||
|
||||
@@ -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,21 @@ 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 teamNotesTextElement = $(element);
|
||||
var textArea = $("<textarea />");
|
||||
textArea.val(teamNotesTextElement.text());
|
||||
teamNotesTextElement.replaceWith(textArea);
|
||||
textArea.focus();
|
||||
textArea.blur(function() {
|
||||
textArea.replaceWith(teamNotesTextElement);
|
||||
if (textArea.val() !== teamNotesTextElement.text()) {
|
||||
websocket.send("updateTeamNotes", { station: teamNotesTextElement.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 +154,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", {
|
||||
|
||||
@@ -187,6 +187,9 @@ var handleArenaStatus = function(data) {
|
||||
$("#plcStatus").attr("data-ready", false);
|
||||
}
|
||||
$("#fieldEstop").attr("data-ready", !data.FieldEstop);
|
||||
$.each(data.PlcArmorBlockStatuses, function(name, status) {
|
||||
$("#plc" + name + "Status").attr("data-ready", status);
|
||||
});
|
||||
};
|
||||
|
||||
// Handles a websocket message to update the match time countdown.
|
||||
|
||||
@@ -85,22 +85,11 @@ var handleRealtimeScore = function(data) {
|
||||
}
|
||||
$("#rungIsLevel>.value").text(score.RungIsLevel ? "Yes" : "No");
|
||||
$("#rungIsLevel").attr("data-value", score.RungIsLevel);
|
||||
$("#controlPanelColor>.value").text(getControlPanelColorText(realtimeScore.ControlPanel.CurrentColor));
|
||||
$("#controlPanelColor").attr("data-value", realtimeScore.ControlPanel.CurrentColor);
|
||||
$("#controlPanelColor").attr("data-control-panel-status", score.ControlPanelStatus)
|
||||
};
|
||||
|
||||
// Handles a websocket message to update the Control Panel color.
|
||||
var handleControlPanelColor = function(data) {
|
||||
var color;
|
||||
if (alliance === "red") {
|
||||
color = data.RedControlPanelColor;
|
||||
} else {
|
||||
color = data.BlueControlPanelColor;
|
||||
}
|
||||
|
||||
$("#controlPanelColor>.value").text(getControlPanelColorText(color));
|
||||
$("#controlPanelColor").attr("data-value", color);
|
||||
};
|
||||
|
||||
// Handles a keyboard event and sends the appropriate websocket message.
|
||||
var handleKeyPress = function(event) {
|
||||
websocket.send(String.fromCharCode(event.keyCode));
|
||||
@@ -164,7 +153,6 @@ $(function() {
|
||||
matchLoad: function(event) { handleMatchLoad(event.data); },
|
||||
matchTime: function(event) { handleMatchTime(event.data); },
|
||||
realtimeScore: function(event) { handleRealtimeScore(event.data); },
|
||||
controlPanelColor: function(event) { handleControlPanelColor(event.data); }
|
||||
});
|
||||
|
||||
$(document).keypress(handleKeyPress);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
<p>
|
||||
<span class="label label-scoring" id="plcStatus"></span><br />
|
||||
<span class="label label-scoring" id="fieldEstop">E-Stop</span>
|
||||
{{range $name, $status := .PlcArmorBlockStatuses}}
|
||||
<br /><span class="label label-scoring" id="plc{{$name}}Status">{{$name}}</span>
|
||||
{{end}}
|
||||
</p>
|
||||
{{end}}
|
||||
</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")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/google/uuid"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -67,7 +68,11 @@ func (web *Web) userIsAdmin(w http.ResponseWriter, r *http.Request) bool {
|
||||
if session != nil && session.Username == adminUser {
|
||||
return true
|
||||
} else {
|
||||
http.Redirect(w, r, "/login?redirect="+r.URL.Path, 307)
|
||||
redirect := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
redirect += "?" + r.URL.RawQuery
|
||||
}
|
||||
http.Redirect(w, r, "/login?redirect="+url.QueryEscape(redirect), 307)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,32 +13,35 @@ func TestLoginDisplay(t *testing.T) {
|
||||
web.arena.EventSettings.AdminPassword = "admin"
|
||||
|
||||
// Check that hitting a protected page redirects to the login.
|
||||
recorder := web.getHttpResponse("/match_play")
|
||||
recorder := web.getHttpResponse("/match_play?p1=v1&p2=v2")
|
||||
assert.Equal(t, 307, recorder.Code)
|
||||
assert.Equal(t, "/login?redirect=/match_play", recorder.Header().Get("Location"))
|
||||
assert.Equal(t, "/login?redirect=%2Fmatch_play%3Fp1%3Dv1%26p2%3Dv2", recorder.Header().Get("Location"))
|
||||
|
||||
recorder = web.getHttpResponse("/login?redirect=/match_play")
|
||||
recorder = web.getHttpResponse("/login?redirect=%2Fmatch_play%3Fp1%3Dv1%26p2%3Dv2")
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "Log In - Untitled Event - Cheesy Arena")
|
||||
|
||||
// Check logging in with the wrong username and right password.
|
||||
recorder = web.postHttpResponse("/login?redirect=/match_play", "username=blorpy&password=reader")
|
||||
recorder = web.postHttpResponse("/login?redirect=%2Fmatch_play%3Fp1%3Dv1%26p2%3Dv2",
|
||||
"username=blorpy&password=reader")
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "Invalid login credentials.")
|
||||
|
||||
// Check logging in with the right username and wrong password.
|
||||
recorder = web.postHttpResponse("/login?redirect=/match_play", "username=admin&password=blorpy")
|
||||
recorder = web.postHttpResponse("/login?redirect=%2Fmatch_play%3Fp1%3Dv1%26p2%3Dv2",
|
||||
"username=admin&password=blorpy")
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "Invalid login credentials.")
|
||||
|
||||
// Check logging in with the right username and password.
|
||||
recorder = web.postHttpResponse("/login?redirect=/match_play", "username=admin&password=admin")
|
||||
recorder = web.postHttpResponse("/login?redirect=%2Fmatch_play%3Fp1%3Dv1%26p2%3Dv2",
|
||||
"username=admin&password=admin")
|
||||
assert.Equal(t, 303, recorder.Code)
|
||||
assert.Equal(t, "/match_play", recorder.Header().Get("Location"))
|
||||
assert.Equal(t, "/match_play?p1=v1&p2=v2", recorder.Header().Get("Location"))
|
||||
cookie := recorder.Header().Get("Set-Cookie")
|
||||
assert.Contains(t, cookie, "session_token=")
|
||||
|
||||
// Check that hitting the reader-level protected page works now.
|
||||
recorder = web.getHttpResponseWithHeaders("/match_play", map[string]string{"Cookie": cookie})
|
||||
recorder = web.getHttpResponseWithHeaders("/match_play?p1=v1&p2=v2", map[string]string{"Cookie": cookie})
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
}
|
||||
|
||||
@@ -72,14 +72,15 @@ func (web *Web) matchPlayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
isReplay := matchResult != nil
|
||||
data := struct {
|
||||
*model.EventSettings
|
||||
PlcIsEnabled bool
|
||||
MatchesByType map[string]MatchPlayList
|
||||
CurrentMatchType string
|
||||
Match *model.Match
|
||||
AllowSubstitution bool
|
||||
IsReplay bool
|
||||
PlcIsEnabled bool
|
||||
MatchesByType map[string]MatchPlayList
|
||||
CurrentMatchType string
|
||||
Match *model.Match
|
||||
AllowSubstitution bool
|
||||
IsReplay bool
|
||||
PlcArmorBlockStatuses map[string]bool
|
||||
}{web.arena.EventSettings, web.arena.Plc.IsEnabled(), matchesByType, currentMatchType, web.arena.CurrentMatch,
|
||||
web.arena.CurrentMatch.ShouldAllowSubstitution(), isReplay}
|
||||
web.arena.CurrentMatch.ShouldAllowSubstitution(), isReplay, web.arena.Plc.GetArmorBlockStatuses()}
|
||||
err = template.ExecuteTemplate(w, "base", data)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
|
||||
@@ -82,7 +82,7 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine.
|
||||
go ws.HandleNotifiers(web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
|
||||
web.arena.ControlPanelColorNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
web.arena.ReloadDisplaysNotifier)
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
@@ -192,17 +192,12 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ
|
||||
scoreChanged = true
|
||||
case "K":
|
||||
if score.ControlPanelStatus == game.ControlPanelRotation {
|
||||
var controlPanel *game.ControlPanel
|
||||
if alliance == "red" {
|
||||
controlPanel = web.arena.RedControlPanel
|
||||
} else {
|
||||
controlPanel = web.arena.BlueControlPanel
|
||||
}
|
||||
controlPanel := &(*realtimeScore).ControlPanel
|
||||
controlPanel.CurrentColor++
|
||||
if controlPanel.CurrentColor == 5 {
|
||||
controlPanel.CurrentColor = 1
|
||||
}
|
||||
web.arena.ControlPanelColorNotifier.Notify()
|
||||
scoreChanged = true
|
||||
}
|
||||
case "P":
|
||||
if score.ControlPanelStatus == game.ControlPanelPosition {
|
||||
|
||||
@@ -50,11 +50,9 @@ func TestScoringPanelWebsocket(t *testing.T) {
|
||||
readWebsocketType(t, redWs, "matchLoad")
|
||||
readWebsocketType(t, redWs, "matchTime")
|
||||
readWebsocketType(t, redWs, "realtimeScore")
|
||||
readWebsocketType(t, redWs, "controlPanelColor")
|
||||
readWebsocketType(t, blueWs, "matchLoad")
|
||||
readWebsocketType(t, blueWs, "matchTime")
|
||||
readWebsocketType(t, blueWs, "realtimeScore")
|
||||
readWebsocketType(t, blueWs, "controlPanelColor")
|
||||
|
||||
// Send some autonomous period scoring commands.
|
||||
web.arena.MatchState = field.AutoPeriod
|
||||
@@ -109,13 +107,9 @@ func TestScoringPanelWebsocket(t *testing.T) {
|
||||
web.arena.ResetMatch()
|
||||
web.arena.LoadTestMatch()
|
||||
readWebsocketType(t, redWs, "matchLoad")
|
||||
messages := readWebsocketMultiple(t, redWs, 2)
|
||||
assert.Contains(t, messages, "realtimeScore")
|
||||
assert.Contains(t, messages, "controlPanelColor")
|
||||
readWebsocketType(t, redWs, "realtimeScore")
|
||||
readWebsocketType(t, blueWs, "matchLoad")
|
||||
messages = readWebsocketMultiple(t, blueWs, 2)
|
||||
assert.Contains(t, messages, "realtimeScore")
|
||||
assert.Contains(t, messages, "controlPanelColor")
|
||||
readWebsocketType(t, blueWs, "realtimeScore")
|
||||
assert.Equal(t, field.NewRealtimeScore(), web.arena.RedRealtimeScore)
|
||||
assert.Equal(t, field.NewRealtimeScore(), web.arena.BlueRealtimeScore)
|
||||
assert.Equal(t, 0, web.arena.ScoringPanelRegistry.GetNumScoreCommitted("red"))
|
||||
|
||||
@@ -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