Merge branch 'master' into lite

This commit is contained in:
Ken Schenke
2020-04-05 15:02:35 -05:00
34 changed files with 962 additions and 139 deletions

View File

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

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
View 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
View 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
View 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)
}

View File

@@ -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 {

View File

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

View File

@@ -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
View 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]]
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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) {

View File

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

View File

@@ -19,6 +19,8 @@ var handleArenaStatus = function(data) {
teamElementPrefix = "#" + blueSide + "Team" + station[1];
}
var teamIdElement = $(teamElementPrefix + "Id");
var teamNotesElement = $(teamElementPrefix + "Notes");
var teamNotesTextElement = $(teamElementPrefix + "Notes div");
var teamEthernetElement = $(teamElementPrefix + "Ethernet");
var teamDsElement = $(teamElementPrefix + "Ds");
var teamRadioElement = $(teamElementPrefix + "Radio");
@@ -26,6 +28,8 @@ var handleArenaStatus = function(data) {
var teamRobotElement = $(teamElementPrefix + "Robot");
var teamBypassElement = $(teamElementPrefix + "Bypass");
teamNotesTextElement.attr("data-station", station);
if (stationStatus.Team) {
// Set the team number and status.
teamIdElement.text(stationStatus.Team.Id);
@@ -42,10 +46,13 @@ var handleArenaStatus = function(data) {
}
}
teamIdElement.attr("data-status", status);
teamNotesTextElement.text(stationStatus.Team.FtaNotes);
teamNotesElement.attr("data-status", status);
} else {
// No team is present in this position for this match; blank out the status.
teamIdElement.text("");
teamIdElement.attr("data-status", "");
teamNotesTextElement.text("");
teamNotesElement.attr("data-status", "");
}
// Format the Ethernet status box.
@@ -119,6 +126,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", {

View File

@@ -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.

View File

@@ -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);

View File

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

View File

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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"))

View File

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