diff --git a/db/migrations/20140520222523_CreateTeams.sql b/db/migrations/20140520222523_CreateTeams.sql index 7736b58..29f312f 100644 --- a/db/migrations/20140520222523_CreateTeams.sql +++ b/db/migrations/20140520222523_CreateTeams.sql @@ -11,7 +11,8 @@ CREATE TABLE teams ( accomplishments VARCHAR(1000), wpakey VARCHAR(16), yellowcard bool, - hasconnected bool + hasconnected bool, + ftanotes VARCHAR(1000) ); -- +goose Down diff --git a/field/arena.go b/field/arena.go index 9aca253..1cfbf48 100644 --- a/field/arena.go +++ b/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) } } diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index 950954f..d586d20 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -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 } diff --git a/field/realtime_score.go b/field/realtime_score.go index 5e23321..6c294df 100644 --- a/field/realtime_score.go +++ b/field/realtime_score.go @@ -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 { diff --git a/game/control_panel.go b/game/control_panel.go index d1b7cdd..66c1364 100644 --- a/game/control_panel.go +++ b/game/control_panel.go @@ -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 + } +} diff --git a/game/control_panel_test.go b/game/control_panel_test.go index 785d69f..4c65614 100644 --- a/game/control_panel_test.go +++ b/game/control_panel_test.go @@ -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 } diff --git a/game/match_timing.go b/game/match_timing.go index 370adce..1d8529e 100644 --- a/game/match_timing.go +++ b/game/match_timing.go @@ -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)) +} diff --git a/game/match_timing_test.go b/game/match_timing_test.go new file mode 100644 index 0000000..aa14f0c --- /dev/null +++ b/game/match_timing_test.go @@ -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))) +} diff --git a/game/power_port.go b/game/power_port.go new file mode 100644 index 0000000..e8ce43d --- /dev/null +++ b/game/power_port.go @@ -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 +} diff --git a/game/power_port_test.go b/game/power_port_test.go new file mode 100644 index 0000000..414f8ee --- /dev/null +++ b/game/power_port_test.go @@ -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) +} diff --git a/game/score.go b/game/score.go index cb2b97c..bfed366 100644 --- a/game/score.go +++ b/game/score.go @@ -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 { diff --git a/model/team.go b/model/team.go index 8de963a..d267695 100644 --- a/model/team.go +++ b/model/team.go @@ -18,6 +18,7 @@ type Team struct { WpaKey string YellowCard bool HasConnected bool + FtaNotes string } func (database *Database) CreateTeam(team *Team) error { diff --git a/partner/tba.go b/partner/tba.go index 6269fa3..96ffb96 100644 --- a/partner/tba.go +++ b/partner/tba.go @@ -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]] diff --git a/plc/armorblock_string.go b/plc/armorblock_string.go new file mode 100644 index 0000000..4efa98d --- /dev/null +++ b/plc/armorblock_string.go @@ -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]] +} diff --git a/plc/coil_string.go b/plc/coil_string.go index 83b646e..d349c32 100644 --- a/plc/coil_string.go +++ b/plc/coil_string.go @@ -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) { diff --git a/plc/input_string.go b/plc/input_string.go index aeedd69..e2d002e 100644 --- a/plc/input_string.go +++ b/plc/input_string.go @@ -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) { diff --git a/plc/plc.go b/plc/plc.go index 4217205..ec77361 100644 --- a/plc/plc.go +++ b/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< 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 } diff --git a/plc/plc_test.go b/plc/plc_test.go index 992ca6d..56d96c9 100644 --- a/plc/plc_test.go +++ b/plc/plc_test.go @@ -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()) +} diff --git a/plc/register_string.go b/plc/register_string.go index d7e6528..8bf6c0a 100644 --- a/plc/register_string.go +++ b/plc/register_string.go @@ -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) { diff --git a/static/css/field_monitor_display.css b/static/css/field_monitor_display.css index 899ae79..cf5c59c 100644 --- a/static/css/field_monitor_display.css +++ b/static/css/field_monitor_display.css @@ -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; +} diff --git a/static/js/field_monitor_display.js b/static/js/field_monitor_display.js index 85dc220..9ae8c18 100644 --- a/static/js/field_monitor_display.js +++ b/static/js/field_monitor_display.js @@ -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 = $("