From a987086160385eb65ff0e1bac7e63a529ad1bd0d Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Wed, 24 Aug 2016 20:54:31 -0700 Subject: [PATCH] Implement 2015 driver station protocol. --- arena.go | 32 ++- arena_test.go | 27 +- bandwidth_monitor.go | 5 +- catalyst.go | 4 +- catalyst_test.go | 2 +- driver_station_connection.go | 366 ++++++++++++++++---------- driver_station_connection_test.go | 322 +++++++++++----------- main.go | 4 +- static/js/alliance_station_display.js | 4 +- static/js/fta_display.js | 16 +- static/js/match_play.js | 12 +- switch_config.txt | 12 +- team_match_log.go | 13 +- 13 files changed, 447 insertions(+), 372 deletions(-) diff --git a/arena.go b/arena.go index a5bead2..ad128ae 100644 --- a/arena.go +++ b/arena.go @@ -160,10 +160,7 @@ func (arena *Arena) AssignTeam(teamId int, station string) error { return nil } if dsConn != nil { - err := dsConn.Close() - if err != nil { - return err - } + dsConn.Close() arena.AllianceStations[station].team = nil arena.AllianceStations[station].DsConn = nil } @@ -183,10 +180,6 @@ func (arena *Arena) AssignTeam(teamId int, station string) error { } arena.AllianceStations[station].team = team - arena.AllianceStations[station].DsConn, err = NewDriverStationConnection(team.Id, station) - if err != nil { - return err - } return nil } @@ -327,7 +320,7 @@ func (arena *Arena) CheckCanStartMatch() error { return fmt.Errorf("Cannot start match while an emergency stop is active.") } if !allianceStation.Bypass { - if allianceStation.DsConn == nil || !allianceStation.DsConn.DriverStationStatus.RobotLinked { + if allianceStation.DsConn == nil || !allianceStation.DsConn.RobotLinked { return fmt.Errorf("Cannot start match until all robots are connected or bypassed.") } } @@ -514,10 +507,11 @@ func (arena *Arena) Run() { func (arena *Arena) sendDsPacket(auto bool, enabled bool) { for _, allianceStation := range arena.AllianceStations { - if allianceStation.DsConn != nil { - allianceStation.DsConn.Auto = auto - allianceStation.DsConn.Enabled = enabled && !allianceStation.EmergencyStop && !allianceStation.Bypass - err := allianceStation.DsConn.Update() + dsConn := allianceStation.DsConn + if dsConn != nil { + dsConn.Auto = auto + dsConn.Enabled = enabled && !allianceStation.EmergencyStop && !allianceStation.Bypass + err := dsConn.Update() if err != nil { log.Printf("Unable to send driver station packet for team %d.", allianceStation.team.Id) } @@ -564,3 +558,15 @@ func (arena *Arena) handleLighting() { } } } + +// Returns the alliance station identifier for the given team, or the empty string if the team is not present +// in the current match. +func (arena *Arena) getAssignedAllianceStation(teamId int) string { + for station, allianceStation := range arena.AllianceStations { + if allianceStation.team != nil && allianceStation.team.Id == teamId { + return station + } + } + + return "" +} diff --git a/arena_test.go b/arena_test.go index 481bad9..9faccbf 100644 --- a/arena_test.go +++ b/arena_test.go @@ -29,24 +29,21 @@ func TestAssignTeam(t *testing.T) { err = mainArena.AssignTeam(254, "B1") assert.Nil(t, err) assert.Equal(t, team, *mainArena.AllianceStations["B1"].team) - dsConn := mainArena.AllianceStations["B1"].DsConn - assert.Equal(t, 254, dsConn.TeamId) - assert.Equal(t, "B1", dsConn.AllianceStation) + dummyDs := &DriverStationConnection{TeamId: 254} + mainArena.AllianceStations["B1"].DsConn = dummyDs // Nothing should happen if the same team is assigned to the same station. err = mainArena.AssignTeam(254, "B1") assert.Nil(t, err) assert.Equal(t, team, *mainArena.AllianceStations["B1"].team) - dsConn2 := mainArena.AllianceStations["B1"].DsConn - assert.Equal(t, dsConn, dsConn2) // Pointer equality + assert.NotNil(t, mainArena.AllianceStations["B1"]) + assert.Equal(t, dummyDs, mainArena.AllianceStations["B1"].DsConn) // Pointer equality // Test reassignment to another team. err = mainArena.AssignTeam(1114, "B1") assert.Nil(t, err) assert.NotEqual(t, team, *mainArena.AllianceStations["B1"].team) - assert.Equal(t, 1114, mainArena.AllianceStations["B1"].DsConn.TeamId) - err = dsConn.conn.Close() - assert.NotNil(t, err) // Connection should have already been closed. + assert.Nil(t, mainArena.AllianceStations["B1"].DsConn) // Check assigning zero as the team number. err = mainArena.AssignTeam(0, "R2") @@ -73,6 +70,8 @@ func TestArenaMatchFlow(t *testing.T) { mainArena.Setup() db.CreateTeam(&Team{Id: 254}) err = mainArena.AssignTeam(254, "B3") + dummyDs := &DriverStationConnection{TeamId: 254} + mainArena.AllianceStations["B3"].DsConn = dummyDs assert.Nil(t, err) // Check pre-match state and packet timing. @@ -94,7 +93,7 @@ func TestArenaMatchFlow(t *testing.T) { mainArena.AllianceStations["R3"].Bypass = true mainArena.AllianceStations["B1"].Bypass = true mainArena.AllianceStations["B2"].Bypass = true - mainArena.AllianceStations["B3"].DsConn.DriverStationStatus.RobotLinked = true + mainArena.AllianceStations["B3"].DsConn.RobotLinked = true err = mainArena.StartMatch() assert.Nil(t, err) mainArena.Update() @@ -309,8 +308,14 @@ func TestMatchStartRobotLinkEnforcement(t *testing.T) { err = mainArena.LoadMatch(&match) assert.Nil(t, err) + mainArena.AllianceStations["R1"].DsConn = &DriverStationConnection{TeamId: 101} + mainArena.AllianceStations["R2"].DsConn = &DriverStationConnection{TeamId: 102} + mainArena.AllianceStations["R3"].DsConn = &DriverStationConnection{TeamId: 103} + mainArena.AllianceStations["B1"].DsConn = &DriverStationConnection{TeamId: 104} + mainArena.AllianceStations["B2"].DsConn = &DriverStationConnection{TeamId: 105} + mainArena.AllianceStations["B3"].DsConn = &DriverStationConnection{TeamId: 106} for _, station := range mainArena.AllianceStations { - station.DsConn.DriverStationStatus.RobotLinked = true + station.DsConn.RobotLinked = true } err = mainArena.StartMatch() assert.Nil(t, err) @@ -323,7 +328,7 @@ func TestMatchStartRobotLinkEnforcement(t *testing.T) { assert.Contains(t, err.Error(), "while an emergency stop is active") } mainArena.AllianceStations["R1"].EmergencyStop = false - mainArena.AllianceStations["R1"].DsConn.DriverStationStatus.RobotLinked = false + mainArena.AllianceStations["R1"].DsConn.RobotLinked = false err = mainArena.StartMatch() if assert.NotNil(t, err) { assert.Contains(t, err.Error(), "until all robots are connected or bypassed") diff --git a/bandwidth_monitor.go b/bandwidth_monitor.go index 1fb0927..4606e23 100644 --- a/bandwidth_monitor.go +++ b/bandwidth_monitor.go @@ -95,14 +95,13 @@ func (monitor *BandwidthMonitor) updateStationBandwidth(station string, port int // No team assigned; just skip it. return } - dsStatus := dsConn.DriverStationStatus secondsSinceLast := time.Now().Sub(monitor.lastBytesTime).Seconds() toRobotBytesForPort := uint32(toRobotBytes[fmt.Sprintf("%s.%d", toRobotBytesOid, port)].(wapsnmp.Counter)) lastToRobotBytesForPort := uint32(monitor.lastToRobotBytes[fmt.Sprintf("%s.%d", toRobotBytesOid, port)].(wapsnmp.Counter)) - dsStatus.MBpsToRobot = float64(toRobotBytesForPort-lastToRobotBytesForPort) / 1024 / 1024 / secondsSinceLast + dsConn.MBpsToRobot = float64(toRobotBytesForPort-lastToRobotBytesForPort) / 1024 / 1024 / secondsSinceLast fromRobotBytesForPort := uint32(fromRobotBytes[fmt.Sprintf("%s.%d", fromRobotBytesOid, port)].(wapsnmp.Counter)) lastFromRobotBytesForPort := uint32(monitor.lastFromRobotBytes[fmt.Sprintf("%s.%d", fromRobotBytesOid, port)].(wapsnmp.Counter)) - dsStatus.MBpsFromRobot = float64(fromRobotBytesForPort-lastFromRobotBytesForPort) / 1024 / 1024 / secondsSinceLast + dsConn.MBpsFromRobot = float64(fromRobotBytesForPort-lastFromRobotBytesForPort) / 1024 / 1024 / secondsSinceLast } diff --git a/catalyst.go b/catalyst.go index b300d3c..bd9b62a 100644 --- a/catalyst.go +++ b/catalyst.go @@ -17,8 +17,6 @@ import ( var catalystTelnetPort = 23 -const eventServerAddress = "10.0.100.50" - var catalystMutex sync.Mutex // Sets up wired networks for the given set of teams. @@ -42,7 +40,7 @@ func ConfigureTeamEthernet(red1, red2, red3, blue1, blue2, blue3 *Team) error { } else { addTeamVlansCommand += fmt.Sprintf("no access-list 1%d\naccess-list 1%d permit ip "+ "10.%d.%d.0 0.0.0.255 host %s\ninterface Vlan%d\nip address 10.%d.%d.61 255.255.255.0\n", vlan, - vlan, team.Id/100, team.Id%100, eventServerAddress, vlan, team.Id/100, team.Id%100) + vlan, team.Id/100, team.Id%100, driverStationTcpListenAddress, vlan, team.Id/100, team.Id%100) } } replaceTeamVlan(red1, red1Vlan) diff --git a/catalyst_test.go b/catalyst_test.go index 01cac7e..05ea6ea 100644 --- a/catalyst_test.go +++ b/catalyst_test.go @@ -31,6 +31,6 @@ func TestConfigureCatalyst(t *testing.T) { mockTelnet(t, catalystTelnetPort, "interface Vlan15\nip address 10.2.54.61\n", &command) assert.Nil(t, ConfigureTeamEthernet(nil, &Team{Id: 1114}, nil, nil, &Team{Id: 254}, nil)) assert.Equal(t, "password\nenable\npassword\nterminal length 0\nconfig terminal\nno access-list 112\n"+ - "access-list 112 permit ip 10.11.14.0 0.0.0.255 host 10.0.100.50\ninterface Vlan12\nip address "+ + "access-list 112 permit ip 10.11.14.0 0.0.0.255 host 10.0.100.5\ninterface Vlan12\nip address "+ "10.11.14.61 255.255.255.0\nend\ncopy running-config startup-config\n\nexit\n", command) } diff --git a/driver_station_connection.go b/driver_station_connection.go index e136172..6d39f62 100644 --- a/driver_station_connection.go +++ b/driver_station_connection.go @@ -7,34 +7,17 @@ package main import ( "fmt" - "hash/crc32" + "log" "net" - "strconv" "time" ) -// UDP port numbers that the Driver Station sends and receives on. -const driverStationSendPort = 1120 -const driverStationReceivePort = 1160 -const driverStationProtocolVersion = "11191100" -const driverStationLinkTimeoutMs = 500 - -type DriverStationStatus struct { - TeamId int - AllianceStation string - DsLinked bool - RobotLinked bool - Auto bool - Enabled bool - EmergencyStop bool - BatteryVoltage float64 - DsVersion string - PacketCount int - MissedPacketCount int - DsRobotTripTimeMs int - MBpsToRobot float64 - MBpsFromRobot float64 -} +const ( + driverStationTcpListenPort = 1750 + driverStationUdpSendPort = 1120 + driverStationLinkTimeoutSec = 5 + maxTcpPacketBytes = 4096 +) type DriverStationConnection struct { TeamId int @@ -42,24 +25,40 @@ type DriverStationConnection struct { Auto bool Enabled bool EmergencyStop bool - DriverStationStatus *DriverStationStatus - LastPacketTime time.Time - LastRobotLinkedTime time.Time + DsLinked bool + RobotLinked bool + BatteryVoltage float64 + DsRobotTripTimeMs int + MissedPacketCount int + MBpsToRobot float64 + MBpsFromRobot float64 SecondsSinceLastRobotLink float64 - conn net.Conn + lastPacketTime time.Time + lastRobotLinkedTime time.Time packetCount int missedPacketOffset int + tcpConn net.Conn + udpConn net.Conn log *TeamMatchLog } +var allianceStationPositionMap = map[string]byte{"R1": 0, "R2": 1, "R3": 2, "B1": 3, "B2": 4, "B3": 5} +var driverStationTcpListenAddress = "10.0.100.5" // The DS will try to connect to this address only. + // Opens a UDP connection for communicating to the driver station. -func NewDriverStationConnection(teamId int, station string) (*DriverStationConnection, error) { - conn, err := net.Dial("udp4", fmt.Sprintf("10.%d.%d.5:%d", teamId/100, teamId%100, driverStationSendPort)) +func NewDriverStationConnection(teamId int, allianceStation string, tcpConn net.Conn) (*DriverStationConnection, error) { + ipAddress, _, err := net.SplitHostPort(tcpConn.RemoteAddr().String()) if err != nil { return nil, err } - return &DriverStationConnection{TeamId: teamId, AllianceStation: station, - DriverStationStatus: new(DriverStationStatus), conn: conn}, nil + log.Printf("Driver station for Team %d connected from %s\n", teamId, ipAddress) + + udpConn, err := net.Dial("udp4", fmt.Sprintf("%s:%d", ipAddress, driverStationUdpSendPort)) + if err != nil { + return nil, err + } + return &DriverStationConnection{TeamId: teamId, AllianceStation: allianceStation, DsLinked: true, + lastPacketTime: time.Now(), tcpConn: tcpConn, udpConn: udpConn}, nil } // Sends a control packet to the Driver Station and checks for timeout conditions. @@ -69,112 +68,120 @@ func (dsConn *DriverStationConnection) Update() error { return err } - if time.Since(dsConn.LastPacketTime).Seconds()*1000 > driverStationLinkTimeoutMs { - dsConn.DriverStationStatus.DsLinked = false - dsConn.DriverStationStatus.RobotLinked = false - dsConn.DriverStationStatus.BatteryVoltage = 0 - dsConn.DriverStationStatus.MBpsToRobot = 0 - dsConn.DriverStationStatus.MBpsFromRobot = 0 + if time.Since(dsConn.lastPacketTime).Seconds() > driverStationLinkTimeoutSec { + dsConn.DsLinked = false + dsConn.RobotLinked = false + dsConn.BatteryVoltage = 0 + dsConn.MBpsToRobot = 0 + dsConn.MBpsFromRobot = 0 } + dsConn.SecondsSinceLastRobotLink = time.Since(dsConn.lastRobotLinkedTime).Seconds() return nil } -func (dsConn *DriverStationConnection) Close() error { +func (dsConn *DriverStationConnection) Close() { if dsConn.log != nil { dsConn.log.Close() } - return dsConn.conn.Close() -} - -// Sets up a watch on the UDP port that Driver Stations send on. -func DsPacketListener() (*net.UDPConn, error) { - udpAddress, err := net.ResolveUDPAddr("udp4", fmt.Sprintf(":%d", driverStationReceivePort)) - if err != nil { - return nil, err + if dsConn.udpConn != nil { + dsConn.udpConn.Close() } - listen, err := net.ListenUDP("udp4", udpAddress) - if err != nil { - return nil, err - } - return listen, nil -} - -// Loops indefinitely to read packets and update connection status. -func ListenForDsPackets(listener *net.UDPConn) { - var data [50]byte - for { - listener.Read(data[:]) - dsStatus := decodeStatusPacket(data) - - // Update the status and last packet times for this alliance/team in the global struct. - dsConn := mainArena.AllianceStations[dsStatus.AllianceStation].DsConn - if dsConn != nil && dsConn.TeamId == dsStatus.TeamId { - dsConn.DriverStationStatus = dsStatus - dsConn.LastPacketTime = time.Now() - if dsStatus.RobotLinked { - dsConn.LastRobotLinkedTime = time.Now() - } - dsConn.SecondsSinceLastRobotLink = time.Since(dsConn.LastRobotLinkedTime).Seconds() - dsConn.DriverStationStatus.MissedPacketCount -= dsConn.missedPacketOffset - - // Log the packet if the match is in progress. - matchTimeSec := mainArena.MatchTimeSec() - if matchTimeSec > 0 && dsConn.log != nil { - dsConn.log.LogDsStatus(matchTimeSec, dsStatus) - } - } + if dsConn.tcpConn != nil { + dsConn.tcpConn.Close() } } // Called at the start of the match to allow for driver station initialization. func (dsConn *DriverStationConnection) signalMatchStart(match *Match) error { // Zero out missed packet count and begin logging. - dsConn.missedPacketOffset = dsConn.DriverStationStatus.MissedPacketCount + dsConn.missedPacketOffset = dsConn.MissedPacketCount var err error dsConn.log, err = NewTeamMatchLog(dsConn.TeamId, match) return err } // Serializes the control information into a packet. -func (dsConn *DriverStationConnection) encodeControlPacket() [74]byte { - var packet [74]byte +func (dsConn *DriverStationConnection) encodeControlPacket() [22]byte { + var packet [22]byte // Packet number, stored big-endian in two bytes. packet[0] = byte((dsConn.packetCount >> 8) & 0xff) packet[1] = byte(dsConn.packetCount & 0xff) - // Robot status byte. 0x01=competition mode, 0x02=link, 0x04=check version, 0x08=request DS ID, - // 0x10=autonomous, 0x20=enable, 0x40=e-stop not on - packet[2] = 0x03 + // Protocol version. + packet[2] = 0 + + // Robot status byte. + packet[3] = 0 if dsConn.Auto { - packet[2] |= 0x10 + packet[3] |= 0x02 } if dsConn.Enabled { - packet[2] |= 0x20 + packet[3] |= 0x04 } - if !dsConn.EmergencyStop { - packet[2] |= 0x40 + if dsConn.EmergencyStop { + packet[3] |= 0x80 } - // Alliance station, stored as ASCII characters 'R/B' and '1/2/3'. - packet[3] = dsConn.AllianceStation[0] - packet[4] = dsConn.AllianceStation[1] + // Unknown or unused. + packet[4] = 0 - // Static protocol version repeated twice. - for i := 0; i < 8; i++ { - packet[10+i] = driverStationProtocolVersion[i] - } - for i := 0; i < 8; i++ { - packet[18+i] = driverStationProtocolVersion[i] - } + // Alliance station. + packet[5] = allianceStationPositionMap[dsConn.AllianceStation] - // Calculate and store the 4-byte CRC32 checksum. - checksum := crc32.ChecksumIEEE(packet[:]) - packet[70] = byte((checksum >> 24) & 0xff) - packet[71] = byte((checksum >> 16) & 0xff) - packet[72] = byte((checksum >> 8) & 0xff) - packet[73] = byte((checksum) & 0xff) + // Match information. + match := mainArena.currentMatch + if match.Type == "practice" { + packet[6] = 1 + } else if match.Type == "qualification" { + packet[6] = 2 + } else if match.Type == "elimination" { + packet[6] = 3 + } else { + packet[6] = 0 + } + // TODO(patrick): Implement if it ever becomes necessary; the official FMS has a different concept of + // match numbers so it's hard to translate. + packet[7] = 0 // Match number + packet[8] = 1 // Match number + packet[9] = 1 // Match repeat number + + // Current time. + currentTime := time.Now() + packet[10] = byte(((currentTime.Nanosecond() / 1000) >> 24) & 0xff) + packet[11] = byte(((currentTime.Nanosecond() / 1000) >> 16) & 0xff) + packet[12] = byte(((currentTime.Nanosecond() / 1000) >> 8) & 0xff) + packet[13] = byte((currentTime.Nanosecond() / 1000) & 0xff) + packet[14] = byte(currentTime.Second()) + packet[15] = byte(currentTime.Minute()) + packet[16] = byte(currentTime.Hour()) + packet[17] = byte(currentTime.Day()) + packet[18] = byte(currentTime.Month()) + packet[19] = byte(currentTime.Year() - 1900) + + // Remaining number of seconds in match. + var matchSecondsRemaining int + switch mainArena.MatchState { + case PRE_MATCH: + fallthrough + case START_MATCH: + fallthrough + case AUTO_PERIOD: + matchSecondsRemaining = mainArena.matchTiming.AutoDurationSec + mainArena.matchTiming.TeleopDurationSec - + int(mainArena.MatchTimeSec()) + case PAUSE_PERIOD: + matchSecondsRemaining = mainArena.matchTiming.TeleopDurationSec + case TELEOP_PERIOD: + fallthrough + case ENDGAME_PERIOD: + matchSecondsRemaining = mainArena.matchTiming.AutoDurationSec + mainArena.matchTiming.TeleopDurationSec + + mainArena.matchTiming.PauseDurationSec - int(mainArena.MatchTimeSec()) + default: + matchSecondsRemaining = 0 + } + packet[20] = byte(matchSecondsRemaining >> 8 & 0xff) + packet[21] = byte(matchSecondsRemaining & 0xff) // Increment the packet count for next time. dsConn.packetCount++ @@ -185,45 +192,136 @@ func (dsConn *DriverStationConnection) encodeControlPacket() [74]byte { // Builds and sends the next control packet to the Driver Station. func (dsConn *DriverStationConnection) sendControlPacket() error { packet := dsConn.encodeControlPacket() - _, err := dsConn.conn.Write(packet[:]) - if err != nil { - return err + if dsConn.udpConn != nil { + _, err := dsConn.udpConn.Write(packet[:]) + if err != nil { + return err + } } return nil } // Deserializes a packet from the DS into a structure representing the DS/robot status. -func decodeStatusPacket(data [50]byte) *DriverStationStatus { - dsStatus := new(DriverStationStatus) - dsStatus.DsLinked = true +func (dsConn *DriverStationConnection) decodeStatusPacket(data [36]byte) { + if data[6]&0x01 != 0 && data[6]&0x08 == 0 { + // Robot is not connected. + dsConn.RobotLinked = false + return + } - // Robot status byte. - dsStatus.RobotLinked = (data[2] & 0x02) != 0 - dsStatus.Auto = (data[2] & 0x10) != 0 - dsStatus.Enabled = (data[2] & 0x20) != 0 - dsStatus.EmergencyStop = (data[2] & 0x40) == 0 + dsConn.RobotLinked = true - // Team number, stored in two bytes as hundreds and then ones (like the IP address). - dsStatus.TeamId = int(data[4])*100 + int(data[5]) + // Average DS-robot trip time in milliseconds. + dsConn.DsRobotTripTimeMs = int(data[1]) / 2 - // Alliance station, stored as ASCII characters 'R/B' and '1/2/3'. - dsStatus.AllianceStation = string(data[10:12]) + // Number of missed packets sent from the DS to the robot. + dsConn.MissedPacketCount = int(data[2]) - dsConn.missedPacketOffset - // Driver Station software version, stored as 8-byte string. - dsStatus.DsVersion = string(data[18:26]) + // Robot battery voltage, stored as volts * 256. + dsConn.BatteryVoltage = float64(data[3]) + float64(data[4])/256 - // Number of missed packets sent from the DS to the robot, stored in two big-endian bytes. - dsStatus.MissedPacketCount = int(data[26])*256 + int(data[27]) - - // Total number of packets sent from the DS to the robot, stored in two big-endian bytes. - dsStatus.PacketCount = int(data[28])*256 + int(data[29]) - - // Average DS-robot trip time in milliseconds, stored in two big-endian bytes. - dsStatus.DsRobotTripTimeMs = int(data[30])*256 + int(data[31]) - - // Robot battery voltage, stored (bizarrely) what it looks like in decimal but as two hexadecimal numbers. - dsStatus.BatteryVoltage, _ = strconv.ParseFloat(fmt.Sprintf("%x.%x", data[40], data[41]), 32) - - return dsStatus + dsConn.lastRobotLinkedTime = time.Now() +} + +// Listens for TCP connection requests to Cheesy Arena from driver stations. +func ListenForDriverStations() { + l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", driverStationTcpListenAddress, driverStationTcpListenPort)) + if err != nil { + log.Printf("Error opening driver station socket: %v", err.Error()) + log.Printf("Change IP address to %s and restart Cheesy Arena to fix.", driverStationTcpListenAddress) + return + } + defer l.Close() + + log.Printf("Listening for driver stations on port %d\n", driverStationTcpListenPort) + for { + tcpConn, err := l.Accept() + if err != nil { + log.Println("Error accepting driver station connection: ", err.Error()) + continue + } + + // Read the team number back and start tracking the driver station. + var packet [5]byte + _, err = tcpConn.Read(packet[:]) + if err != nil { + log.Println("Error reading initial packet: ", err.Error()) + continue + } + if !(packet[0] == 0 && packet[1] == 3 && packet[2] == 24) { + log.Printf("Invalid initial packet received: %v", packet) + tcpConn.Close() + continue + } + teamId := int(packet[3])<<8 + int(packet[4]) + + // Check to see if the team is supposed to be on the field, and notify the DS accordingly. + var assignmentPacket [5]byte + assignmentPacket[0] = 0 // Packet size + assignmentPacket[1] = 3 // Packet size + assignmentPacket[2] = 25 // Packet type + assignedStation := mainArena.getAssignedAllianceStation(teamId) + if assignedStation == "" { + log.Printf("Rejecting connection from Team %d, who is not in the current match.", teamId) + assignmentPacket[3] = 0 + assignmentPacket[4] = 2 + tcpConn.Write(assignmentPacket[:]) + tcpConn.Close() + continue + } + log.Printf("Accepting connection from Team %d in station %s.", teamId, assignedStation) + assignmentPacket[3] = allianceStationPositionMap[assignedStation] + assignmentPacket[4] = 0 + _, err = tcpConn.Write(assignmentPacket[:]) + if err != nil { + log.Println("Error sending driver station assignment packet: %v", err) + tcpConn.Close() + continue + } + + dsConn, err := NewDriverStationConnection(teamId, assignedStation, tcpConn) + if err != nil { + log.Println("Error registering driver station connection: %v", err) + tcpConn.Close() + continue + } + mainArena.AllianceStations[assignedStation].DsConn = dsConn + + // Spin up a goroutine to handle further TCP communication with this driver station. + go dsConn.handleTcpConnection() + } +} + +func (dsConn *DriverStationConnection) handleTcpConnection() { + buffer := make([]byte, maxTcpPacketBytes) + for { + dsConn.tcpConn.SetReadDeadline(time.Now().Add(time.Second * driverStationLinkTimeoutSec)) + _, err := dsConn.tcpConn.Read(buffer) + if err != nil { + fmt.Printf("Error reading from connection for Team %d: %v\n", dsConn.TeamId, err.Error()) + dsConn.Close() + break + } + + dsConn.DsLinked = true + dsConn.lastPacketTime = time.Now() + + packetType := int(buffer[2]) + switch packetType { + case 28: + // DS keepalive packet; do nothing. + case 22: + // Robot status packet. + var statusPacket [36]byte + copy(statusPacket[:], buffer[2:38]) + dsConn.decodeStatusPacket(statusPacket) + } + + // Log the packet if the match is in progress. + matchTimeSec := mainArena.MatchTimeSec() + if matchTimeSec > 0 && dsConn.log != nil { + dsConn.log.LogDsPacket(matchTimeSec, packetType, dsConn) + } + } } diff --git a/driver_station_connection_test.go b/driver_station_connection_test.go index f2fcedb..9afc1ec 100644 --- a/driver_station_connection_test.go +++ b/driver_station_connection_test.go @@ -4,50 +4,50 @@ package main import ( - "fmt" "github.com/stretchr/testify/assert" - "io/ioutil" "net" "testing" "time" ) func TestEncodeControlPacket(t *testing.T) { - dsConn, err := NewDriverStationConnection(254, "R1") + clearDb() + defer clearDb() + var err error + db, err = OpenDatabase(testDbPath) + assert.Nil(t, err) + defer db.Close() + eventSettings, _ = db.GetEventSettings() + mainArena.Setup() + + tcpConn := setupFakeTcpConnection(t) + defer tcpConn.Close() + dsConn, err := NewDriverStationConnection(254, "R1", tcpConn) assert.Nil(t, err) defer dsConn.Close() data := dsConn.encodeControlPacket() - assert.Equal(t, [74]byte{0, 0, 67, 82, 49, 0, 0, 0, 0, 0, 49, 49, 49, 57, 49, 49, 48, 48, 49, 49, 49, 57, - 49, 49, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 110, 235, 5, 29}, data) + assert.Equal(t, byte(0), data[5]) + assert.Equal(t, byte(0), data[6]) + assert.Equal(t, byte(0), data[20]) + assert.Equal(t, byte(150), data[21]) - // Check the different alliance station values as well as the checksums. + // Check the different alliance station values. dsConn.AllianceStation = "R2" data = dsConn.encodeControlPacket() - assert.Equal(t, [74]byte{0, 1, 67, 82, 50, 0, 0, 0, 0, 0, 49, 49, 49, 57, 49, 49, 48, 48, 49, 49, 49, 57, - 49, 49, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 141, 17, 174}, data) + assert.Equal(t, byte(1), data[5]) dsConn.AllianceStation = "R3" data = dsConn.encodeControlPacket() - assert.Equal(t, [74]byte{0, 2, 67, 82, 51, 0, 0, 0, 0, 0, 49, 49, 49, 57, 49, 49, 48, 48, 49, 49, 49, 57, - 49, 49, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 232, 206, 203, 150}, data) + assert.Equal(t, byte(2), data[5]) dsConn.AllianceStation = "B1" data = dsConn.encodeControlPacket() - assert.Equal(t, [74]byte{0, 3, 67, 66, 49, 0, 0, 0, 0, 0, 49, 49, 49, 57, 49, 49, 48, 48, 49, 49, 49, 57, - 49, 49, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 57, 55, 68}, data) + assert.Equal(t, byte(3), data[5]) dsConn.AllianceStation = "B2" data = dsConn.encodeControlPacket() - assert.Equal(t, [74]byte{0, 4, 67, 66, 50, 0, 0, 0, 0, 0, 49, 49, 49, 57, 49, 49, 48, 48, 49, 49, 49, 57, - 49, 49, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 101, 225, 16}, data) + assert.Equal(t, byte(4), data[5]) dsConn.AllianceStation = "B3" data = dsConn.encodeControlPacket() - assert.Equal(t, [74]byte{0, 5, 67, 66, 51, 0, 0, 0, 0, 0, 49, 49, 49, 57, 49, 49, 48, 48, 49, 49, 49, 57, - 49, 49, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 140, 207, 133, 117}, data) + assert.Equal(t, byte(5), data[5]) // Check packet count rollover. dsConn.packetCount = 255 @@ -71,23 +71,58 @@ func TestEncodeControlPacket(t *testing.T) { // Check different robot statuses. dsConn.Auto = true data = dsConn.encodeControlPacket() - assert.Equal(t, byte(83), data[2]) + assert.Equal(t, byte(2), data[3]) dsConn.Enabled = true data = dsConn.encodeControlPacket() - assert.Equal(t, byte(115), data[2]) + assert.Equal(t, byte(6), data[3]) dsConn.Auto = false data = dsConn.encodeControlPacket() - assert.Equal(t, byte(99), data[2]) + assert.Equal(t, byte(4), data[3]) dsConn.EmergencyStop = true data = dsConn.encodeControlPacket() - assert.Equal(t, byte(35), data[2]) + assert.Equal(t, byte(132), data[3]) + + // Check different match types. + mainArena.currentMatch.Type = "practice" + data = dsConn.encodeControlPacket() + assert.Equal(t, byte(1), data[6]) + mainArena.currentMatch.Type = "qualification" + data = dsConn.encodeControlPacket() + assert.Equal(t, byte(2), data[6]) + mainArena.currentMatch.Type = "elimination" + data = dsConn.encodeControlPacket() + assert.Equal(t, byte(3), data[6]) + + // Check the countdown at different points during the match. + mainArena.MatchState = AUTO_PERIOD + mainArena.matchStartTime = time.Now().Add(-time.Duration(4 * time.Second)) + data = dsConn.encodeControlPacket() + assert.Equal(t, byte(146), data[21]) + mainArena.MatchState = PAUSE_PERIOD + mainArena.matchStartTime = time.Now().Add(-time.Duration(16 * time.Second)) + data = dsConn.encodeControlPacket() + assert.Equal(t, byte(135), data[21]) + mainArena.MatchState = TELEOP_PERIOD + mainArena.matchStartTime = time.Now().Add(-time.Duration(33 * time.Second)) + data = dsConn.encodeControlPacket() + assert.Equal(t, byte(119), data[21]) + mainArena.MatchState = ENDGAME_PERIOD + mainArena.matchStartTime = time.Now().Add(-time.Duration(150 * time.Second)) + data = dsConn.encodeControlPacket() + assert.Equal(t, byte(2), data[21]) + mainArena.MatchState = POST_MATCH + mainArena.matchStartTime = time.Now().Add(-time.Duration(180 * time.Second)) + data = dsConn.encodeControlPacket() + assert.Equal(t, byte(0), data[21]) } func TestSendControlPacket(t *testing.T) { - dsConn, err := NewDriverStationConnection(254, "R1") + tcpConn := setupFakeTcpConnection(t) + defer tcpConn.Close() + dsConn, err := NewDriverStationConnection(254, "R1", tcpConn) assert.Nil(t, err) defer dsConn.Close() @@ -97,64 +132,32 @@ func TestSendControlPacket(t *testing.T) { } func TestDecodeStatusPacket(t *testing.T) { + tcpConn := setupFakeTcpConnection(t) + defer tcpConn.Close() + dsConn, err := NewDriverStationConnection(254, "R1", tcpConn) + assert.Nil(t, err) + defer dsConn.Close() + // Check with no linked robot. - data := [50]byte{0, 0, 64, 1, 2, 54, 0, 0, 0, 0, 82, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 98, 200, 63, 43, 0, 11, 0, 240, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus := decodeStatusPacket(data) - assert.Equal(t, 254, dsStatus.TeamId) - assert.Equal(t, "R1", dsStatus.AllianceStation) - assert.Equal(t, false, dsStatus.RobotLinked) - assert.Equal(t, false, dsStatus.Auto) - assert.Equal(t, false, dsStatus.Enabled) - assert.Equal(t, false, dsStatus.EmergencyStop) - assert.Equal(t, 0.0, dsStatus.BatteryVoltage) - assert.Equal(t, "02121300", dsStatus.DsVersion) - assert.Equal(t, 16171, dsStatus.PacketCount) - assert.Equal(t, 25288, dsStatus.MissedPacketCount) - assert.Equal(t, 11, dsStatus.DsRobotTripTimeMs) + data := [36]byte{22, 28, 103, 19, 192, 0, 247, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0} + dsConn.decodeStatusPacket(data) + assert.Equal(t, false, dsConn.RobotLinked) + assert.Equal(t, 0.0, dsConn.BatteryVoltage) + assert.Equal(t, 0, dsConn.MissedPacketCount) + assert.Equal(t, 0, dsConn.DsRobotTripTimeMs) - // Check different team numbers. - data = [50]byte{0, 0, 64, 1, 7, 66, 0, 0, 0, 0, 82, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus = decodeStatusPacket(data) - assert.Equal(t, 766, dsStatus.TeamId) - data = [50]byte{0, 0, 64, 1, 51, 36, 0, 0, 0, 0, 82, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus = decodeStatusPacket(data) - assert.Equal(t, 5136, dsStatus.TeamId) - - // Check different alliance stations. - data = [50]byte{0, 0, 64, 1, 51, 36, 0, 0, 0, 0, 66, 51, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus = decodeStatusPacket(data) - assert.Equal(t, "B3", dsStatus.AllianceStation) - - // Check different robot statuses. - data = [50]byte{0, 0, 66, 1, 7, 66, 0, 0, 0, 0, 82, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus = decodeStatusPacket(data) - assert.Equal(t, true, dsStatus.RobotLinked) - data = [50]byte{0, 0, 98, 1, 7, 66, 0, 0, 0, 0, 82, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus = decodeStatusPacket(data) - assert.Equal(t, true, dsStatus.Enabled) - data = [50]byte{0, 0, 114, 1, 7, 66, 0, 0, 0, 0, 82, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus = decodeStatusPacket(data) - assert.Equal(t, true, dsStatus.Auto) - data = [50]byte{0, 0, 50, 1, 7, 66, 0, 0, 0, 0, 82, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus = decodeStatusPacket(data) - assert.Equal(t, true, dsStatus.EmergencyStop) - - // Check different battery voltages. - data = [50]byte{0, 0, 64, 1, 7, 66, 0, 0, 0, 0, 82, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 25, 117, 0, 0, 0, 0, 42, 7, 189, 111} - dsStatus = decodeStatusPacket(data) - assert.Equal(t, 19.75, dsStatus.BatteryVoltage) + // Check with linked robot. + data = [36]byte{22, 28, 103, 19, 192, 0, 246, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0} + dsConn.decodeStatusPacket(data) + assert.Equal(t, true, dsConn.RobotLinked) + assert.Equal(t, 19.75, dsConn.BatteryVoltage) + assert.Equal(t, 103, dsConn.MissedPacketCount) + assert.Equal(t, 14, dsConn.DsRobotTripTimeMs) } -func TestListenForDsPackets(t *testing.T) { +func TestListenForDriverStations(t *testing.T) { clearDb() defer clearDb() var err error @@ -163,105 +166,74 @@ func TestListenForDsPackets(t *testing.T) { defer db.Close() eventSettings, _ = db.GetEventSettings() - listener, err := DsPacketListener() - if assert.Nil(t, err) { - go ListenForDsPackets(listener) - } + driverStationTcpListenAddress = "127.0.0.1" + go ListenForDriverStations() mainArena.Setup() + time.Sleep(time.Millisecond * 10) - dsConn, err := NewDriverStationConnection(254, "B1") - defer dsConn.Close() - assert.Nil(t, err) - mainArena.AllianceStations["B1"].DsConn = dsConn - dsConn, err = NewDriverStationConnection(1114, "R3") - defer dsConn.Close() - assert.Nil(t, err) - mainArena.AllianceStations["R3"].DsConn = dsConn - - // Create a socket to send fake DS packets to localhost. - conn, err := net.Dial("udp4", fmt.Sprintf("127.0.0.1:%d", driverStationReceivePort)) - assert.Nil(t, err) - - // Check receiving a packet from an expected team. - packet := [50]byte{0, 0, 48, 1, 2, 54, 0, 0, 0, 0, 66, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48, - 152, 160, 152, 160, 1, 0, 255, 255, 82, 0, 0, 0, 0, 0, 25, 117, 0, 0, 0, 0, 42, 7, 189, 111} - _, err = conn.Write(packet[:]) - assert.Nil(t, err) - time.Sleep(time.Millisecond * 10) // Allow some time for the goroutine to process the incoming packet. - dsStatus := mainArena.AllianceStations["B1"].DsConn.DriverStationStatus - if assert.NotNil(t, dsStatus) { - assert.Equal(t, 254, dsStatus.TeamId) - assert.Equal(t, "B1", dsStatus.AllianceStation) - assert.Equal(t, true, dsStatus.DsLinked) - assert.Equal(t, false, dsStatus.RobotLinked) - assert.Equal(t, true, dsStatus.Auto) - assert.Equal(t, true, dsStatus.Enabled) - assert.Equal(t, true, dsStatus.EmergencyStop) - assert.Equal(t, 19.75, dsStatus.BatteryVoltage) - assert.Equal(t, "02121300", dsStatus.DsVersion) - assert.Equal(t, 39072, dsStatus.PacketCount) - assert.Equal(t, 39072, dsStatus.MissedPacketCount) - assert.Equal(t, 256, dsStatus.DsRobotTripTimeMs) + // Connect with an invalid initial packet. + tcpConn, err := net.Dial("tcp", "127.0.0.1:1750") + if assert.Nil(t, err) { + dataSend := [5]byte{0, 3, 29, 0, 0} + tcpConn.Write(dataSend[:]) + var dataReceived [100]byte + _, err = tcpConn.Read(dataReceived[:]) + assert.NotNil(t, err) + tcpConn.Close() } - assert.True(t, time.Since(mainArena.AllianceStations["B1"].DsConn.LastPacketTime).Seconds() < 0.1) - assert.True(t, time.Since(mainArena.AllianceStations["B1"].DsConn.LastRobotLinkedTime).Seconds() > 100) - packet[2] = byte(98) - _, err = conn.Write(packet[:]) - assert.Nil(t, err) - time.Sleep(time.Millisecond * 10) - dsStatus2 := mainArena.AllianceStations["B1"].DsConn.DriverStationStatus - if assert.NotNil(t, dsStatus2) { - assert.Equal(t, true, dsStatus2.RobotLinked) - assert.Equal(t, false, dsStatus2.Auto) - assert.Equal(t, true, dsStatus2.Enabled) - assert.Equal(t, false, dsStatus2.EmergencyStop) + + // Connect as a team not in the current match. + tcpConn, err = net.Dial("tcp", "127.0.0.1:1750") + if assert.Nil(t, err) { + dataSend := [5]byte{0, 3, 24, 5, 223} + tcpConn.Write(dataSend[:]) + var dataReceived [5]byte + _, err = tcpConn.Read(dataReceived[:]) + assert.Nil(t, err) + assert.Equal(t, [5]byte{0, 3, 25, 0, 2}, dataReceived) + tcpConn.Close() } - assert.True(t, time.Since(mainArena.AllianceStations["B1"].DsConn.LastPacketTime).Seconds() < 0.1) - assert.True(t, time.Since(mainArena.AllianceStations["B1"].DsConn.LastRobotLinkedTime).Seconds() < 0.1) - // Should log the packet to file if received during a match. - dsConn = mainArena.AllianceStations["B1"].DsConn - dsConn.signalMatchStart(&Match{Type: "Qualification", DisplayName: "1"}) - mainArena.matchStartTime = time.Now().Add(-1 * time.Second) - mainArena.MatchState = AUTO_PERIOD - _, err = conn.Write(packet[:]) - assert.Nil(t, err) - time.Sleep(time.Millisecond * 10) - logContents, err := ioutil.ReadFile(dsConn.log.logFile.Name()) - assert.Nil(t, err) - assert.Contains(t, string(logContents), "254,B1,true,false,true,false,19.750000,02121300,39072,0,256") + // Connect as a team in the current match. + mainArena.AssignTeam(1503, "B2") + tcpConn, err = net.Dial("tcp", "127.0.0.1:1750") + if assert.Nil(t, err) { + defer tcpConn.Close() + dataSend := [5]byte{0, 3, 24, 5, 223} + tcpConn.Write(dataSend[:]) + var dataReceived [5]byte + _, err = tcpConn.Read(dataReceived[:]) + assert.Nil(t, err) + assert.Equal(t, [5]byte{0, 3, 25, 4, 0}, dataReceived) - // Should ignore a packet coming from an expected team in the wrong position. - statusBefore := mainArena.AllianceStations["R3"].DsConn.DriverStationStatus - packet[10] = 'R' - packet[11] = '3' - packet[2] = 48 - _, err = conn.Write(packet[:]) - assert.Nil(t, err) - time.Sleep(time.Millisecond * 10) - assert.Equal(t, statusBefore, mainArena.AllianceStations["R3"].DsConn.DriverStationStatus) - assert.Equal(t, true, mainArena.AllianceStations["B1"].DsConn.DriverStationStatus.RobotLinked) + time.Sleep(time.Millisecond * 10) + dsConn := mainArena.AllianceStations["B2"].DsConn + if assert.NotNil(t, dsConn) { + assert.Equal(t, 1503, dsConn.TeamId) + assert.Equal(t, "B2", dsConn.AllianceStation) - // Should ignore a packet coming from an unexpected team. - packet[4] = byte(15) - packet[5] = byte(3) - packet[10] = 'B' - packet[11] = '1' - packet[2] = 48 - _, err = conn.Write(packet[:]) - assert.Nil(t, err) - time.Sleep(time.Millisecond * 10) - assert.Equal(t, true, mainArena.AllianceStations["B1"].DsConn.DriverStationStatus.RobotLinked) - - // Should indicate that the connection has dropped if a response isn't received before the timeout. - dsConn = mainArena.AllianceStations["B1"].DsConn - dsConn.Update() - assert.Equal(t, true, dsConn.DriverStationStatus.DsLinked) - assert.Equal(t, true, dsConn.DriverStationStatus.RobotLinked) - assert.NotEqual(t, 0, dsConn.DriverStationStatus.BatteryVoltage) - dsConn.LastPacketTime = dsConn.LastPacketTime.Add(-1 * time.Second) - dsConn.Update() - assert.Equal(t, false, dsConn.DriverStationStatus.DsLinked) - assert.Equal(t, false, dsConn.DriverStationStatus.RobotLinked) - assert.Equal(t, 0.0, dsConn.DriverStationStatus.BatteryVoltage) + // Check that an unknown packet type gets ignored and a status packet gets decoded. + dataSend = [5]byte{0, 3, 37, 0, 0} + tcpConn.Write(dataSend[:]) + time.Sleep(time.Millisecond * 10) + dataSend2 := [38]byte{0, 36, 22, 28, 103, 19, 192, 0, 246, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + tcpConn.Write(dataSend2[:]) + time.Sleep(time.Millisecond * 10) + assert.Equal(t, true, dsConn.RobotLinked) + assert.Equal(t, 19.75, dsConn.BatteryVoltage) + assert.Equal(t, 103, dsConn.MissedPacketCount) + assert.Equal(t, 14, dsConn.DsRobotTripTimeMs) + } + } +} + +func setupFakeTcpConnection(t *testing.T) net.Conn { + // Set up a fake TCP endpoint and connection to it. + l, err := net.Listen("tcp", ":9999") + assert.Nil(t, err) + defer l.Close() + tcpConn, err := net.Dial("tcp", "127.0.0.1:9999") + assert.Nil(t, err) + return tcpConn } diff --git a/main.go b/main.go index 6476799..549065a 100644 --- a/main.go +++ b/main.go @@ -21,9 +21,7 @@ func main() { // Run the webserver and DS packet listener in goroutines and use the main one for the arena state machine. go ServeWebInterface() - listener, err := DsPacketListener() - checkErr(err) - go ListenForDsPackets(listener) + go ListenForDriverStations() go MonitorBandwidth() mainArena.Setup() mainArena.Run() diff --git a/static/js/alliance_station_display.js b/static/js/alliance_station_display.js index 7f8553c..2c56e08 100644 --- a/static/js/alliance_station_display.js +++ b/static/js/alliance_station_display.js @@ -70,9 +70,9 @@ var handleStatus = function(data) { if (stationStatus && stationStatus.Bypass) { $("#match").attr("data-status", "bypass"); } else if (stationStatus && stationStatus.DsConn) { - if (!stationStatus.DsConn.DriverStationStatus.DsLinked) { + if (!stationStatus.DsConn.DsLinked) { $("#match").attr("data-status", allianceStation[0]); - } else if (!stationStatus.DsConn.DriverStationStatus.RobotLinked) { + } else if (!stationStatus.DsConn.RobotLinked) { blink = true; if (!blinkInterval) { blinkInterval = setInterval(function() { diff --git a/static/js/fta_display.js b/static/js/fta_display.js index 4c2ea31..6823cd1 100644 --- a/static/js/fta_display.js +++ b/static/js/fta_display.js @@ -11,11 +11,11 @@ var handleStatus = function(data) { // Update the team status view. $.each(data.AllianceStations, function(station, stationStatus) { if (stationStatus.DsConn) { + var dsConn = stationStatus.DsConn; $("#status" + station + " .team").text(stationStatus.DsConn.TeamId); - var dsStatus = stationStatus.DsConn.DriverStationStatus; - $("#status" + station + " .ds-status").attr("data-status-ok", dsStatus.DsLinked); - $("#status" + station + " .ds-status").text(dsStatus.MBpsToRobot.toFixed(1) + "/" + dsStatus.MBpsFromRobot.toFixed(1)); - $("#status" + station + " .robot-status").attr("data-status-ok", dsStatus.RobotLinked); + $("#status" + station + " .ds-status").attr("data-status-ok", dsConn.DsLinked); + $("#status" + station + " .ds-status").text(dsConn.MBpsToRobot.toFixed(1) + "/" + dsConn.MBpsFromRobot.toFixed(1)); + $("#status" + station + " .robot-status").attr("data-status-ok", dsConn.RobotLinked); if (stationStatus.DsConn.SecondsSinceLastRobotLink > 1 && stationStatus.DsConn.SecondsSinceLastRobotLink < 1000) { $("#status" + station + " .robot-status").text(stationStatus.DsConn.SecondsSinceLastRobotLink.toFixed()); } else { @@ -26,12 +26,12 @@ var handleStatus = function(data) { lowBatteryThreshold = 12; } $("#status" + station + " .battery-status").attr("data-status-ok", - dsStatus.BatteryVoltage > lowBatteryThreshold && dsStatus.RobotLinked); - $("#status" + station + " .battery-status").text(dsStatus.BatteryVoltage.toFixed(1) + "V"); + dsConn.BatteryVoltage > lowBatteryThreshold && dsConn.RobotLinked); + $("#status" + station + " .battery-status").text(dsConn.BatteryVoltage.toFixed(1) + "V"); $("#status" + station + " .trip-time").attr("data-status-ok", true); - $("#status" + station + " .trip-time").text(dsStatus.DsRobotTripTimeMs.toFixed(1) + "ms"); + $("#status" + station + " .trip-time").text(dsConn.DsRobotTripTimeMs.toFixed(1) + "ms"); $("#status" + station + " .packet-loss").attr("data-status-ok", true); - $("#status" + station + " .packet-loss").text(dsStatus.MissedPacketCount); + $("#status" + station + " .packet-loss").text(dsConn.MissedPacketCount); } else { $("#status" + station + " .ds-status").attr("data-status-ok", ""); $("#status" + station + " .ds-status").text(""); diff --git a/static/js/match_play.js b/static/js/match_play.js index 1c2dd96..7f03cd3 100644 --- a/static/js/match_play.js +++ b/static/js/match_play.js @@ -62,10 +62,10 @@ var handleStatus = function(data) { // Update the team status view. $.each(data.AllianceStations, function(station, stationStatus) { if (stationStatus.DsConn) { - var dsStatus = stationStatus.DsConn.DriverStationStatus; - $("#status" + station + " .ds-status").attr("data-status-ok", dsStatus.DsLinked); - $("#status" + station + " .ds-status").text(dsStatus.MBpsToRobot.toFixed(1) + "/" + dsStatus.MBpsFromRobot.toFixed(1)); - $("#status" + station + " .robot-status").attr("data-status-ok", dsStatus.RobotLinked); + var dsConn = stationStatus.DsConn; + $("#status" + station + " .ds-status").attr("data-status-ok", dsConn.DsLinked); + $("#status" + station + " .ds-status").text(dsConn.MBpsToRobot.toFixed(1) + "/" + dsConn.MBpsFromRobot.toFixed(1)); + $("#status" + station + " .robot-status").attr("data-status-ok", dsConn.RobotLinked); if (stationStatus.DsConn.SecondsSinceLastRobotLink > 1 && stationStatus.DsConn.SecondsSinceLastRobotLink < 1000) { $("#status" + station + " .robot-status").text(stationStatus.DsConn.SecondsSinceLastRobotLink.toFixed()); } else { @@ -76,8 +76,8 @@ var handleStatus = function(data) { lowBatteryThreshold = 12; } $("#status" + station + " .battery-status").attr("data-status-ok", - dsStatus.BatteryVoltage > lowBatteryThreshold && dsStatus.RobotLinked); - $("#status" + station + " .battery-status").text(dsStatus.BatteryVoltage.toFixed(1) + "V"); + dsConn.BatteryVoltage > lowBatteryThreshold && dsConn.RobotLinked); + $("#status" + station + " .battery-status").text(dsConn.BatteryVoltage.toFixed(1) + "V"); } else { $("#status" + station + " .ds-status").attr("data-status-ok", ""); $("#status" + station + " .ds-status").text(""); diff --git a/switch_config.txt b/switch_config.txt index 11fd284..a6d534d 100644 --- a/switch_config.txt +++ b/switch_config.txt @@ -166,12 +166,12 @@ interface Vlan16 ip classless ip http server ! -access-list 111 permit ip 10.0.1.0 0.0.0.255 host 10.0.100.50 -access-list 112 permit ip 10.0.2.0 0.0.0.255 host 10.0.100.50 -access-list 113 permit ip 10.0.3.0 0.0.0.255 host 10.0.100.50 -access-list 114 permit ip 10.0.4.0 0.0.0.255 host 10.0.100.50 -access-list 115 permit ip 10.0.5.0 0.0.0.255 host 10.0.100.50 -access-list 116 permit ip 10.0.6.0 0.0.0.255 host 10.0.100.50 +access-list 111 permit ip 10.0.1.0 0.0.0.255 host 10.0.100.5 +access-list 112 permit ip 10.0.2.0 0.0.0.255 host 10.0.100.5 +access-list 113 permit ip 10.0.3.0 0.0.0.255 host 10.0.100.5 +access-list 114 permit ip 10.0.4.0 0.0.0.255 host 10.0.100.5 +access-list 115 permit ip 10.0.5.0 0.0.0.255 host 10.0.100.5 +access-list 116 permit ip 10.0.6.0 0.0.0.255 host 10.0.100.5 ! snmp-server community 1234Five RO ! diff --git a/team_match_log.go b/team_match_log.go index d4d24ae..2aa5fb5 100644 --- a/team_match_log.go +++ b/team_match_log.go @@ -34,18 +34,17 @@ func NewTeamMatchLog(teamId int, match *Match) (*TeamMatchLog, error) { } log := TeamMatchLog{log.New(logFile, "", 0), logFile} - log.logger.Println("matchTimeSec,teamId,allianceStation,robotLinked,auto,enabled,emergencyStop," + - "batteryVoltage,dsVersion,packetCount,missedPacketCount,dsRobotTripTimeMs") + log.logger.Println("matchTimeSec,packetType,teamId,allianceStation,robotLinked,auto,enabled," + + "emergencyStop,batteryVoltage,missedPacketCount,dsRobotTripTimeMs") return &log, nil } // Adds a line to the log when a packet is received. -func (log *TeamMatchLog) LogDsStatus(matchTimeSec float64, dsStatus *DriverStationStatus) { - log.logger.Printf("%f,%d,%s,%v,%v,%v,%v,%f,%s,%d,%d,%d", matchTimeSec, dsStatus.TeamId, - dsStatus.AllianceStation, dsStatus.RobotLinked, dsStatus.Auto, dsStatus.Enabled, - dsStatus.EmergencyStop, dsStatus.BatteryVoltage, dsStatus.DsVersion, dsStatus.PacketCount, - dsStatus.MissedPacketCount, dsStatus.DsRobotTripTimeMs) +func (log *TeamMatchLog) LogDsPacket(matchTimeSec float64, packetType int, dsConn *DriverStationConnection) { + log.logger.Printf("%f,%d,%d,%s,%v,%v,%v,%v,%f,%d,%d", matchTimeSec, packetType, dsConn.TeamId, + dsConn.AllianceStation, dsConn.RobotLinked, dsConn.Auto, dsConn.Enabled, dsConn.EmergencyStop, + dsConn.BatteryVoltage, dsConn.MissedPacketCount, dsConn.DsRobotTripTimeMs) } func (log *TeamMatchLog) Close() {