forked from Team254/cheesy-arena
-
Notifications
You must be signed in to change notification settings - Fork 0
/
driver_station_connection.go
350 lines (307 loc) · 10.5 KB
/
driver_station_connection.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
// Copyright 2014 Team 254. All Rights Reserved.
// Author: [email protected] (Patrick Fairbank)
//
// Model and methods for interacting with a team's Driver Station.
package main
import (
"fmt"
"log"
"net"
"time"
)
const (
driverStationTcpListenPort = 1750
driverStationUdpSendPort = 1120
driverStationUdpReceivePort = 1160
driverStationTcpLinkTimeoutSec = 5
driverStationUdpLinkTimeoutSec = 1
maxTcpPacketBytes = 4096
)
type DriverStationConnection struct {
TeamId int
AllianceStation string
Auto bool
Enabled bool
EmergencyStop bool
DsLinked bool
RobotLinked bool
BatteryVoltage float64
DsRobotTripTimeMs int
MissedPacketCount int
MBpsToRobot float64
MBpsFromRobot float64
SecondsSinceLastRobotLink float64
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, allianceStation string, tcpConn net.Conn) (*DriverStationConnection, error) {
ipAddress, _, err := net.SplitHostPort(tcpConn.RemoteAddr().String())
if err != nil {
return nil, err
}
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, tcpConn: tcpConn, udpConn: udpConn}, nil
}
// Loops indefinitely to read packets and update connection status.
func ListenForDsUdpPackets() {
udpAddress, _ := net.ResolveUDPAddr("udp4", fmt.Sprintf(":%d", driverStationUdpReceivePort))
listener, err := net.ListenUDP("udp4", udpAddress)
if err != nil {
log.Fatalln("Error opening driver station UDP socket: %v", err.Error())
}
log.Printf("Listening for driver stations on UDP port %d\n", driverStationUdpReceivePort)
var data [50]byte
for {
listener.Read(data[:])
teamId := int(data[4])<<8 + int(data[5])
var dsConn *DriverStationConnection
for _, allianceStation := range mainArena.AllianceStations {
if allianceStation.Team != nil && allianceStation.Team.Id == teamId {
dsConn = allianceStation.DsConn
break
}
}
if dsConn != nil {
dsConn.DsLinked = true
dsConn.lastPacketTime = time.Now()
dsConn.RobotLinked = data[3]&0x20 != 0
if dsConn.RobotLinked {
dsConn.lastRobotLinkedTime = time.Now()
// Robot battery voltage, stored as volts * 256.
dsConn.BatteryVoltage = float64(data[6]) + float64(data[7])/256
}
}
}
}
// Sends a control packet to the Driver Station and checks for timeout conditions.
func (dsConn *DriverStationConnection) Update() error {
err := dsConn.sendControlPacket()
if err != nil {
return err
}
if time.Since(dsConn.lastPacketTime).Seconds() > driverStationUdpLinkTimeoutSec {
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() {
if dsConn.log != nil {
dsConn.log.Close()
}
if dsConn.udpConn != nil {
dsConn.udpConn.Close()
}
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.MissedPacketCount
var err error
dsConn.log, err = NewTeamMatchLog(dsConn.TeamId, match)
return err
}
// Serializes the control information into a packet.
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)
// Protocol version.
packet[2] = 0
// Robot status byte.
packet[3] = 0
if dsConn.Auto {
packet[3] |= 0x02
}
if dsConn.Enabled {
packet[3] |= 0x04
}
if dsConn.EmergencyStop {
packet[3] |= 0x80
}
// Unknown or unused.
packet[4] = 0
// Alliance station.
packet[5] = allianceStationPositionMap[dsConn.AllianceStation]
// 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 - 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++
return packet
}
// Builds and sends the next control packet to the Driver Station.
func (dsConn *DriverStationConnection) sendControlPacket() error {
packet := dsConn.encodeControlPacket()
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 (dsConn *DriverStationConnection) decodeStatusPacket(data [36]byte) {
// Average DS-robot trip time in milliseconds.
dsConn.DsRobotTripTimeMs = int(data[1]) / 2
// Number of missed packets sent from the DS to the robot.
dsConn.MissedPacketCount = int(data[2]) - dsConn.missedPacketOffset
}
// 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 TCP 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 TCP 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 * driverStationTcpLinkTimeoutSec))
_, err := dsConn.tcpConn.Read(buffer)
if err != nil {
log.Printf("Error reading from connection for Team %d: %v\n", dsConn.TeamId, err.Error())
dsConn.Close()
mainArena.AllianceStations[dsConn.AllianceStation].DsConn = nil
break
}
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)
}
}
}