Skip to content

Commit

Permalink
Tweak team sign logic.
Browse files Browse the repository at this point in the history
  • Loading branch information
patfair committed Jul 28, 2024
1 parent a29b40b commit 5743ce7
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 28 deletions.
61 changes: 44 additions & 17 deletions field/team_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"net"
"strconv"
"strings"
"time"
)

// Represents a collection of team number and timer signs.
Expand Down Expand Up @@ -42,6 +43,7 @@ type TeamSign struct {
udpConn net.Conn
packetData [128]byte
packetIndex int
lastPacketTime time.Time
}

const (
Expand All @@ -54,13 +56,15 @@ const (
teamSignPacketTypeRearText = 0x02
teamSignPacketTypeFrontIntensity = 0x03
teamSignPacketTypeColor = 0x04
teamSignPacketPeriodMs = 5000
teamSignBlinkPeriodMs = 750
)

// 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 orangeColor = color.RGBA{255, 50, 0, 255}
var whiteColor = color.RGBA{255, 255, 255, 255}

// Creates a new collection of team signs.
Expand Down Expand Up @@ -144,9 +148,9 @@ func (sign *TeamSign) SetAddress(ipAddress string) {
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"
// Reset the sign's state to ensure that the next packet sent will update the sign.
sign.packetIndex = 0
sign.lastPacketTime = time.Time{}
}

// Updates the sign's internal state with the latest data and sends packets to the sign if anything has changed.
Expand All @@ -159,8 +163,7 @@ func (sign *TeamSign) update(
}

if sign.isTimer {
sign.frontText, sign.frontColor = generateTimerText(arena.FieldReset, countdown)
sign.rearText = inMatchRearText
sign.frontText, sign.frontColor, sign.rearText = generateTimerTexts(arena, countdown, inMatchRearText)
} else {
sign.frontText, sign.frontColor, sign.rearText = sign.generateTeamNumberTexts(
arena, allianceStation, isRed, inMatchRearText,
Expand Down Expand Up @@ -192,33 +195,46 @@ func generateInMatchRearText(isRed bool, countdown string, realtimeScore, oppone
)
}

// Returns the front text and color to display on the timer display.
func generateTimerText(fieldReset bool, countdown string) (string, color.RGBA) {
// Returns the front text, front color, and rear text to display on the timer display.
func generateTimerTexts(arena *Arena, countdown, inMatchRearText string) (string, color.RGBA, string) {
if arena.AllianceStationDisplayMode == "blank" {
return " ", whiteColor, ""
}

var frontText string
var frontColor color.RGBA
if fieldReset {
frontText = "SAFE"
rearText := inMatchRearText
if arena.MatchState == TimeoutActive {
frontText = countdown
frontColor = whiteColor
rearText = fmt.Sprintf("Field Break: %s", countdown)
} else if arena.FieldReset && arena.MatchState != TimeoutActive {
frontText = "SAFE "
frontColor = greenColor
} else {
frontText = countdown
frontColor = whiteColor
}
return frontText, frontColor
return frontText, frontColor, rearText
}

// Returns the front text, front color, and rear text to display on the sign for the given alliance station.
func (sign *TeamSign) generateTeamNumberTexts(
arena *Arena, allianceStation *AllianceStation, isRed bool, inMatchRearText string,
) (string, color.RGBA, string) {
if arena.AllianceStationDisplayMode == "blank" {
return " ", whiteColor, ""
}

if allianceStation.Team == nil {
return "", whiteColor, fmt.Sprintf("%20s", "No Team Assigned")
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
frontColor = blinkColor(orangeColor)
} else if arena.FieldReset {
frontColor = greenColor
} else if isRed {
Expand All @@ -232,7 +248,7 @@ func (sign *TeamSign) generateTeamNumberTexts(
message = "E-STOP"
} else if allianceStation.AStop && arena.MatchState == AutoPeriod {
message = "A-STOP"
} else if arena.MatchState == PreMatch {
} else if arena.MatchState == PreMatch || arena.MatchState == TimeoutActive {
if allianceStation.Bypass {
message = "Bypassed"
} else if !allianceStation.Ethernet {
Expand Down Expand Up @@ -277,29 +293,32 @@ func (sign *TeamSign) sendPacket() error {
sign.packetIndex = teamSignPacketHeaderLength
}

if sign.frontText != sign.lastFrontText {
isStale := time.Now().Sub(sign.lastPacketTime).Milliseconds() >= teamSignPacketPeriodMs

if sign.frontText != sign.lastFrontText || isStale {
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 {
if sign.frontColor != sign.lastFrontColor || isStale {
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 {
if sign.rearText != sign.lastRearText || isStale {
sign.writePacketData([]byte{teamSignAddressSingle, sign.address, teamSignPacketTypeRearText})
sign.writePacketData([]byte(sign.rearText))
sign.writePacketData([]byte{0})
sign.lastRearText = sign.rearText
}

if sign.packetIndex > teamSignPacketHeaderLength {
sign.lastPacketTime = time.Now()
if _, err := sign.udpConn.Write(sign.packetData[:sign.packetIndex]); err != nil {
return err
}
Expand All @@ -315,3 +334,11 @@ func (sign *TeamSign) writePacketData(data []byte) {
sign.packetIndex++
}
}

// Periodically modifies the given color to zero brightness to create a blinking effect.
func blinkColor(originalColor color.RGBA) color.RGBA {
if time.Now().UnixMilli()%teamSignBlinkPeriodMs < teamSignBlinkPeriodMs/2 {
return originalColor
}
return color.RGBA{originalColor.R, originalColor.G, originalColor.B, 0}
}
50 changes: 39 additions & 11 deletions field/team_sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,31 @@ func TestTeamSign_Timer(t *testing.T) {
assert.Equal(t, 56, int(sign.packetData[5]))
assert.Equal(t, 0x04, int(sign.packetData[6]))
assert.Equal(t, "12:34", string(sign.packetData[10:15]))
assert.Equal(t, []byte{0, 0}, sign.packetData[15:17])
assert.Equal(t, "Rear Text", string(sign.packetData[30:39]))
assert.Equal(t, 40, sign.packetIndex)

assertSign := func(expectedFrontText string, expectedFrontColor color.RGBA, expectedRearText string) {
frontText, frontColor, rearText := generateTimerTexts(arena, "23:45", "Rear Text")
assert.Equal(t, expectedFrontText, frontText)
assert.Equal(t, expectedFrontColor, frontColor)
assert.Equal(t, expectedRearText, rearText)
}

// Check field reset.
arena.FieldReset = false
frontText, frontColor := generateTimerText(false, "23:45")
assert.Equal(t, "23:45", frontText)
assert.Equal(t, whiteColor, frontColor)
frontText, frontColor = generateTimerText(true, "23:45")
assert.Equal(t, "SAFE", frontText)
assert.Equal(t, greenColor, frontColor)
assertSign("23:45", whiteColor, "Rear Text")
arena.FieldReset = true
assertSign("SAFE ", greenColor, "Rear Text")

// Check timeout mode.
arena.FieldReset = true
arena.MatchState = TimeoutActive
assertSign("23:45", whiteColor, "Field Break: 23:45")

// Check blank mode.
arena.AllianceStationDisplayMode = "blank"
assertSign(" ", whiteColor, "")
}

func TestTeamSign_TeamNumber(t *testing.T) {
Expand All @@ -67,18 +82,23 @@ func TestTeamSign_TeamNumber(t *testing.T) {
assert.Equal(t, "CYPRX", string(sign.packetData[0:5]))
assert.Equal(t, 53, int(sign.packetData[5]))
assert.Equal(t, 0x04, int(sign.packetData[6]))
assert.Equal(t, []byte{0x01, 53, 0x01, 0, 0}, sign.packetData[7:12])
assert.Equal(t, "No Team Assigned", string(sign.packetData[29:45]))
assert.Equal(t, 46, sign.packetIndex)
assert.Equal(t, []byte{0x01, 53, 0x01}, sign.packetData[7:10])
assert.Equal(t, " ", string(sign.packetData[10:15]))
assert.Equal(t, []byte{0, 0}, sign.packetData[15:17])
assert.Equal(t, "No Team Assigned", string(sign.packetData[34:50]))
assert.Equal(t, 51, sign.packetIndex)

assertSign := func(isRed bool, expectedFrontText string, expectedFrontColor color.RGBA, expectedRearText string) {
frontText, frontColor, rearText := sign.generateTeamNumberTexts(arena, allianceStation, isRed, "Rear Text")
assert.Equal(t, expectedFrontText, frontText)
assert.Equal(t, expectedFrontColor, frontColor)
assert.Equal(t, expectedRearText, rearText)

// Modify front color to account for time-based blinking.
frontColor.A = 255
assert.Equal(t, expectedFrontColor, frontColor)
}

assertSign(true, "", whiteColor, " No Team Assigned")
assertSign(true, " ", whiteColor, " No Team Assigned")
arena.FieldReset = true
arena.assignTeam(254, "R1")
assertSign(true, " 254", greenColor, "254 Connect PC")
Expand All @@ -104,6 +124,10 @@ func TestTeamSign_TeamNumber(t *testing.T) {
allianceStation.Bypass = true
assertSign(true, " 254", redColor, "254 Bypassed")

// Check that timeout mode has no effect on the team sign.
arena.MatchState = TimeoutActive
assertSign(true, " 254", redColor, "254 Bypassed")

// Check E-stop and A-stop.
arena.MatchState = AutoPeriod
assertSign(true, " 254", redColor, "Rear Text")
Expand All @@ -128,4 +152,8 @@ func TestTeamSign_TeamNumber(t *testing.T) {
arena.MatchState = PreMatch
arena.assignTeam(1503, "R1")
assertSign(false, " 1503", blueColor, "1503 Connect PC")

// Check blank mode.
arena.AllianceStationDisplayMode = "blank"
assertSign(true, " ", whiteColor, "")
}

0 comments on commit 5743ce7

Please sign in to comment.