diff --git a/field/team_sign.go b/field/team_sign.go index 874513ae..1009e76d 100644 --- a/field/team_sign.go +++ b/field/team_sign.go @@ -14,6 +14,7 @@ import ( "net" "strconv" "strings" + "time" ) // Represents a collection of team number and timer signs. @@ -42,6 +43,7 @@ type TeamSign struct { udpConn net.Conn packetData [128]byte packetIndex int + lastPacketTime time.Time } const ( @@ -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. @@ -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. @@ -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, @@ -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 { @@ -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 { @@ -277,14 +293,16 @@ 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}) @@ -292,7 +310,7 @@ func (sign *TeamSign) sendPacket() error { 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}) @@ -300,6 +318,7 @@ func (sign *TeamSign) sendPacket() error { } if sign.packetIndex > teamSignPacketHeaderLength { + sign.lastPacketTime = time.Now() if _, err := sign.udpConn.Write(sign.packetData[:sign.packetIndex]); err != nil { return err } @@ -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} +} diff --git a/field/team_sign_test.go b/field/team_sign_test.go index 106c108d..2d570085 100644 --- a/field/team_sign_test.go +++ b/field/team_sign_test.go @@ -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) { @@ -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") @@ -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") @@ -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, "") }