Skip to content

Commit

Permalink
Add support for the official Cypress team number signs introduced in …
Browse files Browse the repository at this point in the history
…2024.
  • Loading branch information
patfair committed Jun 2, 2024
1 parent aaae019 commit 1f9060b
Show file tree
Hide file tree
Showing 6 changed files with 497 additions and 3 deletions.
19 changes: 16 additions & 3 deletions field/arena.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type Arena struct {
NexusClient *partner.NexusClient
AllianceStations map[string]*AllianceStation
Displays map[string]*Display
TeamSigns *TeamSigns
ScoringPanelRegistry
ArenaNotifiers
MatchState
Expand Down Expand Up @@ -114,6 +115,8 @@ func NewArena(dbPath string) (*Arena, error) {

arena.Displays = make(map[string]*Display)

arena.TeamSigns = NewTeamSigns()

var err error
arena.Database, err = model.OpenDatabase(dbPath)
if err != nil {
Expand Down Expand Up @@ -150,6 +153,14 @@ func (arena *Arena) LoadSettings() error {
arena.EventSettings = settings

// Initialize the components that depend on settings.
arena.TeamSigns.Red1.SetAddress(settings.TeamSignRed1Address)
arena.TeamSigns.Red2.SetAddress(settings.TeamSignRed2Address)
arena.TeamSigns.Red3.SetAddress(settings.TeamSignRed3Address)
arena.TeamSigns.RedTimer.SetAddress(settings.TeamSignRedTimerAddress)
arena.TeamSigns.Blue1.SetAddress(settings.TeamSignBlue1Address)
arena.TeamSigns.Blue2.SetAddress(settings.TeamSignBlue2Address)
arena.TeamSigns.Blue3.SetAddress(settings.TeamSignBlue3Address)
arena.TeamSigns.BlueTimer.SetAddress(settings.TeamSignBlueTimerAddress)
accessPointWifiStatuses := [6]*network.TeamWifiStatus{
&arena.AllianceStations["R1"].WifiStatus,
&arena.AllianceStations["R2"].WifiStatus,
Expand Down Expand Up @@ -290,7 +301,6 @@ func (arena *Arena) LoadMatch(match *model.Match) error {
arena.soundsPlayed = make(map[*game.MatchSound]struct{})
arena.RedRealtimeScore = NewRealtimeScore()
arena.BlueRealtimeScore = NewRealtimeScore()
arena.FieldReset = false
arena.ScoringPanelRegistry.resetScoreCommitted()
arena.Plc.ResetMatch()

Expand Down Expand Up @@ -546,6 +556,7 @@ func (arena *Arena) Update() {
sendDsPacket = true
}
arena.Plc.ResetMatch()
arena.FieldReset = false
case WarmupPeriod:
auto = true
enabled = false
Expand All @@ -569,7 +580,6 @@ func (arena *Arena) Update() {
enabled = true
}
}
arena.FieldReset = false
case PausePeriod:
auto = false
enabled = false
Expand Down Expand Up @@ -601,7 +611,6 @@ func (arena *Arena) Update() {
arena.preLoadNextMatch()
}()
}
arena.FieldReset = false
case TimeoutActive:
if matchTimeSec >= float64(game.MatchTiming.TimeoutDurationSec) {
arena.MatchState = PostTimeout
Expand Down Expand Up @@ -636,6 +645,9 @@ func (arena *Arena) Update() {
// Handle field sensors/lights/actuators.
arena.handlePlcInputOutput()

// Handle the team number / timer displays.
arena.TeamSigns.Update(arena)

arena.LastMatchTimeSec = matchTimeSec
arena.lastMatchState = arena.MatchState
}
Expand Down Expand Up @@ -934,6 +946,7 @@ func (arena *Arena) handlePlcInputOutput() {

// Turn off lights if all teams become ready.
if redAllianceReady && blueAllianceReady {
arena.FieldReset = false
arena.Plc.SetFieldResetLight(false)
if arena.CurrentMatch.FieldReadyAt.IsZero() {
arena.CurrentMatch.FieldReadyAt = time.Now()
Expand Down
292 changes: 292 additions & 0 deletions field/team_sign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
// Copyright 2024 Team 254. All Rights Reserved.
// Author: [email protected] (Patrick Fairbank)
//
// Models and logic for controlling a Cypress team number / timer sign.

package field

import (
"fmt"
"github.com/Team254/cheesy-arena/game"
"image/color"
"log"
"net"
"strconv"
"strings"
)

// Represents a collection of team number and timer signs.
type TeamSigns struct {
Red1 TeamSign
Red2 TeamSign
Red3 TeamSign
RedTimer TeamSign
Blue1 TeamSign
Blue2 TeamSign
Blue3 TeamSign
BlueTimer TeamSign
}

// Represents a team number or timer sign.
type TeamSign struct {
isTimer bool
address byte
frontText string
frontColor color.RGBA
rearText string
lastFrontText string
lastFrontColor color.RGBA
lastRearText string
udpConn net.Conn
packetData [128]byte
packetIndex int
}

const (
teamSignPort = 10011
teamSignPacketMagicString = "CYPRX"
teamSignPacketHeaderLength = 7
teamSignCommandSetDisplay = 0x04
teamSignAddressSingle = 0x01
teamSignPacketTypeFrontText = 0x01
teamSignPacketTypeRearText = 0x02
teamSignPacketTypeFrontIntensity = 0x03
teamSignPacketTypeColor = 0x04
)

// Predefined colors for the team sign front text. The "A" channel is used as the intensity.
var redColor = color.RGBA{255, 0, 0, 255}
var blueColor = color.RGBA{0, 0, 255, 255}
var greenColor = color.RGBA{0, 255, 0, 255}
var orangeColor = color.RGBA{255, 165, 0, 255}
var whiteColor = color.RGBA{255, 255, 255, 255}

// Creates a new collection of team signs.
func NewTeamSigns() *TeamSigns {
signs := new(TeamSigns)
signs.RedTimer.isTimer = true
signs.BlueTimer.isTimer = true
return signs
}

// Updates the state of all signs with the latest data and sends packets to the signs if anything has changed.
func (signs *TeamSigns) Update(arena *Arena) {
// Generate the countdown string which is used in multiple places.
matchTimeSec := int(arena.MatchTimeSec())
var countdownSec int
switch arena.MatchState {
case PreMatch:
fallthrough
case StartMatch:
fallthrough
case WarmupPeriod:
countdownSec = game.MatchTiming.AutoDurationSec
case AutoPeriod:
countdownSec = game.MatchTiming.WarmupDurationSec + game.MatchTiming.AutoDurationSec - matchTimeSec
case TeleopPeriod:
countdownSec = game.MatchTiming.WarmupDurationSec + game.MatchTiming.AutoDurationSec +
game.MatchTiming.TeleopDurationSec + game.MatchTiming.PauseDurationSec - matchTimeSec
case TimeoutActive:
countdownSec = game.MatchTiming.TimeoutDurationSec - matchTimeSec
default:
countdownSec = 0
}
countdown := fmt.Sprintf("%02d:%02d", countdownSec/60, countdownSec%60)

// Generate the in-match rear text which is common to a whole alliance.
redInMatchRearText := generateInMatchRearText(countdown, arena.RedRealtimeScore, arena.BlueRealtimeScore)
blueInMatchRearText := generateInMatchRearText(countdown, arena.BlueRealtimeScore, arena.RedRealtimeScore)

signs.Red1.update(arena, arena.AllianceStations["R1"], true, countdown, redInMatchRearText)
signs.Red2.update(arena, arena.AllianceStations["R2"], true, countdown, redInMatchRearText)
signs.Red3.update(arena, arena.AllianceStations["R3"], true, countdown, redInMatchRearText)
signs.RedTimer.update(arena, nil, true, countdown, redInMatchRearText)
signs.Blue1.update(arena, arena.AllianceStations["B1"], false, countdown, blueInMatchRearText)
signs.Blue2.update(arena, arena.AllianceStations["B2"], false, countdown, blueInMatchRearText)
signs.Blue3.update(arena, arena.AllianceStations["B3"], false, countdown, blueInMatchRearText)
signs.BlueTimer.update(arena, nil, false, countdown, blueInMatchRearText)
}

// Sets the IP address of the sign.
func (sign *TeamSign) SetAddress(ipAddress string) {
if sign.udpConn != nil {
_ = sign.udpConn.Close()
}
if ipAddress == "" {
// The sign is not configured.
sign.address = 0
}

var err error
sign.udpConn, err = net.Dial("udp4", fmt.Sprintf("%s:%d", ipAddress, teamSignPort))
if err != nil {
log.Printf("Failed to connect to team sign at %s: %v", ipAddress, err)
return
}
addressParts := strings.Split(ipAddress, ".")
if len(addressParts) != 4 {
log.Printf("Failed to configure team sign: invalid IP address: %s", ipAddress)
return
}
address, _ := strconv.Atoi(addressParts[3])
sign.address = byte(address)

sign.lastFrontText = "dummy value to ensure it gets cleared"
sign.lastFrontColor = color.RGBA{}
sign.lastRearText = "dummy value to ensure it gets cleared"
}

// Updates the sign's internal state with the latest data and sends packets to the sign if anything has changed.
func (sign *TeamSign) update(
arena *Arena, allianceStation *AllianceStation, isRed bool, countdown, inMatchRearText string,
) {
if sign.address == 0 {
// Don't do anything if there is no sign configured in this position.
return
}

if sign.isTimer {
sign.frontText, sign.frontColor = generateTimerText(arena.FieldReset, countdown)
sign.rearText = inMatchRearText
} else {
sign.frontText, sign.frontColor, sign.rearText = generateTeamNumberTexts(
arena, allianceStation, isRed, inMatchRearText,
)
}

if err := sign.sendPacket(); err != nil {
log.Printf("Failed to send team sign packet: %v", err)
}
}

// Returns the in-match rear text that is common to a whole alliance.
func generateInMatchRearText(countdown string, realtimeScore, opponentRealtimeScore *RealtimeScore) string {
var amplifiedCountdown string
if realtimeScore.AmplifiedTimeRemainingSec > 0 {
amplifiedCountdown = fmt.Sprintf("Amp:%2d", realtimeScore.AmplifiedTimeRemainingSec)
}
scoreSummary := realtimeScore.CurrentScore.Summarize(&opponentRealtimeScore.CurrentScore)
return fmt.Sprintf(
"%s %02d/%02d %6s", countdown, scoreSummary.NumNotes, scoreSummary.NumNotesGoal, amplifiedCountdown,
)
}

// Returns the front text and color to display on the timer display.
func generateTimerText(fieldReset bool, countdown string) (string, color.RGBA) {
var frontText string
var frontColor color.RGBA
if fieldReset {
frontText = "SAFE"
frontColor = greenColor
} else {
frontText = countdown
frontColor = whiteColor
}
return frontText, frontColor
}

// Returns the front text, front color, and rear text to display on the sign for the given alliance station.
func generateTeamNumberTexts(
arena *Arena, allianceStation *AllianceStation, isRed bool, inMatchRearText string,
) (string, color.RGBA, string) {
if allianceStation.Team == nil {
return "", whiteColor, fmt.Sprintf("%20s", "No Team Assigned")
}

frontText := fmt.Sprintf("%5d", allianceStation.Team.Id)

var frontColor color.RGBA
if allianceStation.EStop || allianceStation.AStop && arena.MatchState == AutoPeriod {
frontColor = orangeColor
} else if arena.FieldReset {
frontColor = greenColor
} else if isRed {
frontColor = redColor
} else {
frontColor = blueColor
}

var message string
if allianceStation.EStop {
message = "E-STOP"
} else if allianceStation.AStop && arena.MatchState == AutoPeriod {
message = "A-STOP"
} else if arena.MatchState == PreMatch {
if allianceStation.Bypass {
message = "Bypassed"
} else if !allianceStation.Ethernet {
message = "Connect PC"
} else if allianceStation.DsConn == nil {
message = "Start DS"
} else if allianceStation.DsConn.WrongStation != "" {
message = "Move Station"
} else if !allianceStation.DsConn.RadioLinked {
message = "No Radio"
} else if !allianceStation.DsConn.RioLinked {
message = "No Rio"
} else if !allianceStation.DsConn.RobotLinked {
message = "No Code"
} else {
message = "Ready"
}
}

var rearText string
if len(message) > 0 {
rearText = fmt.Sprintf("%-5d %14s", allianceStation.Team.Id, message)
} else {
rearText = inMatchRearText
}

return frontText, frontColor, rearText
}

// Sends a UDP packet to the sign if its state has changed.
func (sign *TeamSign) sendPacket() error {
if sign.packetIndex == 0 {
// Write the static packet header the first time this method is invoked.
sign.writePacketData([]byte(teamSignPacketMagicString))
sign.writePacketData([]byte{sign.address, teamSignCommandSetDisplay})
} else {
// Reset the write index to just after the header.
sign.packetIndex = teamSignPacketHeaderLength
}

if sign.frontText != sign.lastFrontText {
sign.writePacketData([]byte{teamSignAddressSingle, sign.address, teamSignPacketTypeFrontText})
sign.writePacketData([]byte(sign.frontText))
sign.writePacketData([]byte{0, 0}) // Second byte is "show decimal point".
sign.lastFrontText = sign.frontText
}

if sign.frontColor != sign.lastFrontColor {
sign.writePacketData([]byte{teamSignAddressSingle, sign.address, teamSignPacketTypeColor})
sign.writePacketData([]byte{sign.frontColor.R, sign.frontColor.G, sign.frontColor.B})
sign.writePacketData([]byte{teamSignAddressSingle, sign.address, teamSignPacketTypeFrontIntensity})
sign.writePacketData([]byte{sign.frontColor.A})
sign.lastFrontColor = sign.frontColor
}

if sign.rearText != sign.lastRearText {
sign.writePacketData([]byte{teamSignAddressSingle, sign.address, teamSignPacketTypeRearText})
sign.writePacketData([]byte(sign.rearText))
sign.writePacketData([]byte{0})
sign.lastRearText = sign.rearText
}

if sign.packetIndex > teamSignPacketHeaderLength {
if _, err := sign.udpConn.Write(sign.packetData[:sign.packetIndex]); err != nil {
return err
}
}

return nil
}

// Writes the given data to the packet buffer and advances the write index.
func (sign *TeamSign) writePacketData(data []byte) {
for _, value := range data {
sign.packetData[sign.packetIndex] = value
sign.packetIndex++
}
}
Loading

0 comments on commit 1f9060b

Please sign in to comment.