blob: 245891a54aaa233d1fde56b483d18f724359a888 [file] [log] [blame]
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08001package requests
2
3import (
Philipp Schraderd3fac192022-03-02 20:35:46 -08004 "errors"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08005 "fmt"
6 "io"
7 "net/http"
Philipp Schraderd3fac192022-03-02 20:35:46 -08008 "strconv"
9 "strings"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080010
Philipp Schrader8747f1b2022-02-23 23:56:22 -080011 "github.com/frc971/971-Robot-Code/scouting/db"
Philipp Schraderd3fac192022-03-02 20:35:46 -080012 "github.com/frc971/971-Robot-Code/scouting/scraping"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080013 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
Philipp Schraderd3fac192022-03-02 20:35:46 -080014 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list"
15 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list_response"
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080016 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
17 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
Philipp Schraderacf96232022-03-01 22:03:30 -080018 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting"
19 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting_response"
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080020 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team"
21 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080022 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
Philipp Schrader30005e42022-03-06 13:53:58 -080023 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080024 "github.com/frc971/971-Robot-Code/scouting/webserver/server"
25 flatbuffers "github.com/google/flatbuffers/go"
26)
27
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080028type SubmitDataScouting = submit_data_scouting.SubmitDataScouting
Philipp Schrader30005e42022-03-06 13:53:58 -080029type SubmitDataScoutingResponseT = submit_data_scouting_response.SubmitDataScoutingResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080030type RequestAllMatches = request_all_matches.RequestAllMatches
31type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080032type RequestMatchesForTeam = request_matches_for_team.RequestMatchesForTeam
33type RequestMatchesForTeamResponseT = request_matches_for_team_response.RequestMatchesForTeamResponseT
Philipp Schraderacf96232022-03-01 22:03:30 -080034type RequestDataScouting = request_data_scouting.RequestDataScouting
35type RequestDataScoutingResponseT = request_data_scouting_response.RequestDataScoutingResponseT
Philipp Schraderd3fac192022-03-02 20:35:46 -080036type RefreshMatchList = refresh_match_list.RefreshMatchList
37type RefreshMatchListResponseT = refresh_match_list_response.RefreshMatchListResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080038
Philipp Schrader8747f1b2022-02-23 23:56:22 -080039// The interface we expect the database abstraction to conform to.
40// We use an interface here because it makes unit testing easier.
41type Database interface {
42 AddToMatch(db.Match) error
43 AddToStats(db.Stats) error
44 ReturnMatches() ([]db.Match, error)
45 ReturnStats() ([]db.Stats, error)
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080046 QueryMatches(int32) ([]db.Match, error)
Philipp Schrader8747f1b2022-02-23 23:56:22 -080047 QueryStats(int) ([]db.Stats, error)
48}
49
Philipp Schraderd3fac192022-03-02 20:35:46 -080050type ScrapeMatchList func(int32, string) ([]scraping.Match, error)
51
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080052// Handles unknown requests. Just returns a 404.
53func unknown(w http.ResponseWriter, req *http.Request) {
54 w.WriteHeader(http.StatusNotFound)
55}
56
57func respondWithError(w http.ResponseWriter, statusCode int, errorMessage string) {
58 builder := flatbuffers.NewBuilder(1024)
59 builder.Finish((&error_response.ErrorResponseT{
60 ErrorMessage: errorMessage,
61 }).Pack(builder))
62 w.WriteHeader(statusCode)
63 w.Write(builder.FinishedBytes())
64}
65
66func respondNotImplemented(w http.ResponseWriter) {
67 respondWithError(w, http.StatusNotImplemented, "")
68}
69
70// TODO(phil): Can we turn this into a generic?
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080071func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*SubmitDataScouting, bool) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080072 success := true
73 defer func() {
74 if r := recover(); r != nil {
75 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
76 success = false
77 }
78 }()
79 result := submit_data_scouting.GetRootAsSubmitDataScouting(buf, 0)
80 return result, success
81}
82
83// Handles a SubmitDataScouting request.
Philipp Schrader8747f1b2022-02-23 23:56:22 -080084type submitDataScoutingHandler struct {
85 db Database
86}
87
88func (handler submitDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080089 requestBytes, err := io.ReadAll(req.Body)
90 if err != nil {
91 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
92 return
93 }
94
Philipp Schrader30005e42022-03-06 13:53:58 -080095 request, success := parseSubmitDataScouting(w, requestBytes)
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080096 if !success {
97 return
98 }
99
Philipp Schrader30005e42022-03-06 13:53:58 -0800100 stats := db.Stats{
101 TeamNumber: request.Team(),
102 MatchNumber: request.Match(),
103 ShotsMissedAuto: request.MissedShotsAuto(),
104 UpperGoalAuto: request.UpperGoalAuto(),
105 LowerGoalAuto: request.LowerGoalAuto(),
106 ShotsMissed: request.MissedShotsTele(),
107 UpperGoalShots: request.UpperGoalTele(),
108 LowerGoalShots: request.LowerGoalTele(),
109 PlayedDefense: request.DefenseRating(),
110 Climbing: request.Climbing(),
111 }
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800112
Philipp Schrader30005e42022-03-06 13:53:58 -0800113 err = handler.db.AddToStats(stats)
114 if err != nil {
115 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit datascouting data: ", err))
116 }
117
118 builder := flatbuffers.NewBuilder(50 * 1024)
119 builder.Finish((&SubmitDataScoutingResponseT{}).Pack(builder))
120 w.Write(builder.FinishedBytes())
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800121}
122
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800123// TODO(phil): Can we turn this into a generic?
124func parseRequestAllMatches(w http.ResponseWriter, buf []byte) (*RequestAllMatches, bool) {
125 success := true
126 defer func() {
127 if r := recover(); r != nil {
128 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
129 success = false
130 }
131 }()
132 result := request_all_matches.GetRootAsRequestAllMatches(buf, 0)
133 return result, success
134}
135
136// Handles a RequestAllMaches request.
137type requestAllMatchesHandler struct {
138 db Database
139}
140
141func (handler requestAllMatchesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
142 requestBytes, err := io.ReadAll(req.Body)
143 if err != nil {
144 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
145 return
146 }
147
148 _, success := parseRequestAllMatches(w, requestBytes)
149 if !success {
150 return
151 }
152
153 matches, err := handler.db.ReturnMatches()
154 if err != nil {
155 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800156 return
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800157 }
158
159 var response RequestAllMatchesResponseT
160 for _, match := range matches {
161 response.MatchList = append(response.MatchList, &request_all_matches_response.MatchT{
162 MatchNumber: match.MatchNumber,
163 Round: match.Round,
164 CompLevel: match.CompLevel,
165 R1: match.R1,
166 R2: match.R2,
167 R3: match.R3,
168 B1: match.B1,
169 B2: match.B2,
170 B3: match.B3,
171 })
172 }
173
174 builder := flatbuffers.NewBuilder(50 * 1024)
175 builder.Finish((&response).Pack(builder))
176 w.Write(builder.FinishedBytes())
177}
178
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800179// TODO(phil): Can we turn this into a generic?
180func parseRequestMatchesForTeam(w http.ResponseWriter, buf []byte) (*RequestMatchesForTeam, bool) {
181 success := true
182 defer func() {
183 if r := recover(); r != nil {
184 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
185 success = false
186 }
187 }()
188 result := request_matches_for_team.GetRootAsRequestMatchesForTeam(buf, 0)
189 return result, success
190}
191
192// Handles a RequestMatchesForTeam request.
193type requestMatchesForTeamHandler struct {
194 db Database
195}
196
197func (handler requestMatchesForTeamHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
198 requestBytes, err := io.ReadAll(req.Body)
199 if err != nil {
200 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
201 return
202 }
203
204 request, success := parseRequestMatchesForTeam(w, requestBytes)
205 if !success {
206 return
207 }
208
209 matches, err := handler.db.QueryMatches(request.Team())
210 if err != nil {
211 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800212 return
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800213 }
214
215 var response RequestAllMatchesResponseT
216 for _, match := range matches {
217 response.MatchList = append(response.MatchList, &request_all_matches_response.MatchT{
218 MatchNumber: match.MatchNumber,
219 Round: match.Round,
220 CompLevel: match.CompLevel,
221 R1: match.R1,
222 R2: match.R2,
223 R3: match.R3,
224 B1: match.B1,
225 B2: match.B2,
226 B3: match.B3,
227 })
228 }
229
230 builder := flatbuffers.NewBuilder(50 * 1024)
231 builder.Finish((&response).Pack(builder))
232 w.Write(builder.FinishedBytes())
233}
234
Philipp Schraderacf96232022-03-01 22:03:30 -0800235// TODO(phil): Can we turn this into a generic?
236func parseRequestDataScouting(w http.ResponseWriter, buf []byte) (*RequestDataScouting, bool) {
237 success := true
238 defer func() {
239 if r := recover(); r != nil {
240 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
241 success = false
242 }
243 }()
244 result := request_data_scouting.GetRootAsRequestDataScouting(buf, 0)
245 return result, success
246}
247
248// Handles a RequestDataScouting request.
249type requestDataScoutingHandler struct {
250 db Database
251}
252
253func (handler requestDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
254 requestBytes, err := io.ReadAll(req.Body)
255 if err != nil {
256 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
257 return
258 }
259
260 _, success := parseRequestDataScouting(w, requestBytes)
261 if !success {
262 return
263 }
264
265 stats, err := handler.db.ReturnStats()
266 if err != nil {
267 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800268 return
Philipp Schraderacf96232022-03-01 22:03:30 -0800269 }
270
271 var response RequestDataScoutingResponseT
272 for _, stat := range stats {
273 response.StatsList = append(response.StatsList, &request_data_scouting_response.StatsT{
274 Team: stat.TeamNumber,
275 Match: stat.MatchNumber,
276 MissedShotsAuto: stat.ShotsMissedAuto,
277 UpperGoalAuto: stat.UpperGoalAuto,
278 LowerGoalAuto: stat.LowerGoalAuto,
279 MissedShotsTele: stat.ShotsMissed,
280 UpperGoalTele: stat.UpperGoalShots,
281 LowerGoalTele: stat.LowerGoalShots,
282 DefenseRating: stat.PlayedDefense,
283 Climbing: stat.Climbing,
284 })
285 }
286
287 builder := flatbuffers.NewBuilder(50 * 1024)
288 builder.Finish((&response).Pack(builder))
289 w.Write(builder.FinishedBytes())
290}
291
Philipp Schraderd3fac192022-03-02 20:35:46 -0800292// TODO(phil): Can we turn this into a generic?
293func parseRefreshMatchList(w http.ResponseWriter, buf []byte) (*RefreshMatchList, bool) {
294 success := true
295 defer func() {
296 if r := recover(); r != nil {
297 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
298 success = false
299 }
300 }()
301 result := refresh_match_list.GetRootAsRefreshMatchList(buf, 0)
302 return result, success
303}
304
305func parseTeamKey(teamKey string) (int, error) {
306 // TBA prefixes teams with "frc". Not sure why. Get rid of that.
307 teamKey = strings.TrimPrefix(teamKey, "frc")
308 return strconv.Atoi(teamKey)
309}
310
311// Parses the alliance data from the specified match and returns the three red
312// teams and the three blue teams.
313func parseTeamKeys(match *scraping.Match) ([3]int32, [3]int32, error) {
314 redKeys := match.Alliances.Red.TeamKeys
315 blueKeys := match.Alliances.Blue.TeamKeys
316
317 if len(redKeys) != 3 || len(blueKeys) != 3 {
318 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
319 "Found %d red teams and %d blue teams.", len(redKeys), len(blueKeys)))
320 }
321
322 var red [3]int32
323 for i, key := range redKeys {
324 team, err := parseTeamKey(key)
325 if err != nil {
326 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
327 "Failed to parse red %d team '%s' as integer: %v", i+1, key, err))
328 }
329 red[i] = int32(team)
330 }
331 var blue [3]int32
332 for i, key := range blueKeys {
333 team, err := parseTeamKey(key)
334 if err != nil {
335 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
336 "Failed to parse blue %d team '%s' as integer: %v", i+1, key, err))
337 }
338 blue[i] = int32(team)
339 }
340 return red, blue, nil
341}
342
343type refreshMatchListHandler struct {
344 db Database
345 scrape ScrapeMatchList
346}
347
348func (handler refreshMatchListHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
349 requestBytes, err := io.ReadAll(req.Body)
350 if err != nil {
351 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
352 return
353 }
354
355 request, success := parseRefreshMatchList(w, requestBytes)
356 if !success {
357 return
358 }
359
360 matches, err := handler.scrape(request.Year(), string(request.EventCode()))
361 if err != nil {
362 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to scrape match list: ", err))
363 return
364 }
365
366 for _, match := range matches {
367 // Make sure the data is valid.
368 red, blue, err := parseTeamKeys(&match)
369 if err != nil {
370 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
371 "TheBlueAlliance data for match %d is malformed: %v", match.MatchNumber, err))
372 return
373 }
374 // Add the match to the database.
Philipp Schrader7365d322022-03-06 16:40:08 -0800375 err = handler.db.AddToMatch(db.Match{
Philipp Schraderd3fac192022-03-02 20:35:46 -0800376 MatchNumber: int32(match.MatchNumber),
377 // TODO(phil): What does Round mean?
378 Round: 1,
379 CompLevel: match.CompLevel,
380 R1: red[0],
381 R2: red[1],
382 R3: red[2],
383 B1: blue[0],
384 B2: blue[1],
385 B3: blue[2],
386 })
Philipp Schrader7365d322022-03-06 16:40:08 -0800387 if err != nil {
388 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
389 "Failed to add match %d to the database: %v", match.MatchNumber, err))
390 return
391 }
Philipp Schraderd3fac192022-03-02 20:35:46 -0800392 }
393
394 var response RefreshMatchListResponseT
395 builder := flatbuffers.NewBuilder(1024)
396 builder.Finish((&response).Pack(builder))
397 w.Write(builder.FinishedBytes())
398}
399
400func HandleRequests(db Database, scrape ScrapeMatchList, scoutingServer server.ScoutingServer) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800401 scoutingServer.HandleFunc("/requests", unknown)
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800402 scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800403 scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800404 scoutingServer.Handle("/requests/request/matches_for_team", requestMatchesForTeamHandler{db})
Philipp Schraderacf96232022-03-01 22:03:30 -0800405 scoutingServer.Handle("/requests/request/data_scouting", requestDataScoutingHandler{db})
Philipp Schraderd3fac192022-03-02 20:35:46 -0800406 scoutingServer.Handle("/requests/refresh_match_list", refreshMatchListHandler{db, scrape})
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800407}