Add Pit Scouting Tab

Signed-off-by: Emily Markova <emily.markova@gmail.com>
Change-Id: Iede446546e20f2915bb53e134050b5025976da36
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index 795d0ee..a7593e7 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -21,6 +21,8 @@
         "//scouting/webserver/requests/messages:request_all_notes_response_go_fbs",
         "//scouting/webserver/requests/messages:request_notes_for_team_go_fbs",
         "//scouting/webserver/requests/messages:request_notes_for_team_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_pit_images_go_fbs",
+        "//scouting/webserver/requests/messages:request_pit_images_response_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_actions_go_fbs",
@@ -29,6 +31,8 @@
         "//scouting/webserver/requests/messages:submit_driver_ranking_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_pit_image_go_fbs",
+        "//scouting/webserver/requests/messages:submit_pit_image_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_shift_schedule_go_fbs",
         "//scouting/webserver/requests/messages:submit_shift_schedule_response_go_fbs",
         "//scouting/webserver/server",
@@ -54,11 +58,14 @@
         "//scouting/webserver/requests/messages:request_all_notes_go_fbs",
         "//scouting/webserver/requests/messages:request_all_notes_response_go_fbs",
         "//scouting/webserver/requests/messages:request_notes_for_team_go_fbs",
+        "//scouting/webserver/requests/messages:request_pit_images_go_fbs",
+        "//scouting/webserver/requests/messages:request_pit_images_response_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_actions_go_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_go_fbs",
+        "//scouting/webserver/requests/messages:submit_pit_image_go_fbs",
         "//scouting/webserver/requests/messages:submit_shift_schedule_go_fbs",
         "//scouting/webserver/server",
         "@com_github_google_flatbuffers//go:go_default_library",
diff --git a/scouting/webserver/requests/debug/BUILD b/scouting/webserver/requests/debug/BUILD
index ef14e5a..11311d2 100644
--- a/scouting/webserver/requests/debug/BUILD
+++ b/scouting/webserver/requests/debug/BUILD
@@ -14,10 +14,12 @@
         "//scouting/webserver/requests/messages:request_all_matches_response_go_fbs",
         "//scouting/webserver/requests/messages:request_all_notes_response_go_fbs",
         "//scouting/webserver/requests/messages:request_notes_for_team_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_pit_images_response_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_actions_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_pit_image_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_shift_schedule_response_go_fbs",
         "@com_github_google_flatbuffers//go:go_default_library",
     ],
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index 4837cfe..3e5b4f9 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -16,10 +16,12 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_pit_image_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule_response"
 	flatbuffers "github.com/google/flatbuffers/go"
 )
@@ -136,6 +138,12 @@
 		request_notes_for_team_response.GetRootAsRequestNotesForTeamResponse)
 }
 
+func RequestPitImages(server string, requestBytes []byte) (*request_pit_images_response.RequestPitImagesResponseT, error) {
+	return sendMessage[request_pit_images_response.RequestPitImagesResponseT](
+		server+"/requests/request/pit_images", requestBytes,
+		request_pit_images_response.GetRootAsRequestPitImagesResponse)
+}
+
 func RequestAllNotes(server string, requestBytes []byte) (*request_all_notes_response.RequestAllNotesResponseT, error) {
 	return sendMessage[request_all_notes_response.RequestAllNotesResponseT](
 		server+"/requests/request/all_notes", requestBytes,
@@ -166,6 +174,12 @@
 		submit_actions_response.GetRootAsSubmitActionsResponse)
 }
 
+func SubmitPitImage(server string, requestBytes []byte) (*submit_pit_image_response.SubmitPitImageResponseT, error) {
+	return sendMessage[submit_pit_image_response.SubmitPitImageResponseT](
+		server+"/requests/submit/submit_pit_image", requestBytes,
+		submit_pit_image_response.GetRootAsSubmitPitImageResponse)
+}
+
 func Delete2023DataScouting(server string, requestBytes []byte) (*delete_2023_data_scouting_response.Delete2023DataScoutingResponseT, error) {
 	return sendMessage[delete_2023_data_scouting_response.Delete2023DataScoutingResponseT](
 		server+"/requests/delete/delete_2023_data_scouting", requestBytes,
diff --git a/scouting/webserver/requests/messages/BUILD b/scouting/webserver/requests/messages/BUILD
index db422ed..13fdc45 100644
--- a/scouting/webserver/requests/messages/BUILD
+++ b/scouting/webserver/requests/messages/BUILD
@@ -15,6 +15,10 @@
     "submit_notes_response",
     "request_notes_for_team",
     "request_notes_for_team_response",
+    "submit_pit_image",
+    "submit_pit_image_response",
+    "request_pit_images",
+    "request_pit_images_response",
     "request_shift_schedule",
     "request_shift_schedule_response",
     "submit_shift_schedule",
diff --git a/scouting/webserver/requests/messages/request_pit_images.fbs b/scouting/webserver/requests/messages/request_pit_images.fbs
new file mode 100644
index 0000000..b28f0a8
--- /dev/null
+++ b/scouting/webserver/requests/messages/request_pit_images.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table RequestPitImages {
+    team_number:string (id: 0);
+}
+
+root_type RequestPitImages;
diff --git a/scouting/webserver/requests/messages/request_pit_images_response.fbs b/scouting/webserver/requests/messages/request_pit_images_response.fbs
new file mode 100644
index 0000000..d9bb2cc
--- /dev/null
+++ b/scouting/webserver/requests/messages/request_pit_images_response.fbs
@@ -0,0 +1,13 @@
+namespace scouting.webserver.requests;
+
+table PitImage {
+	team_number:string (id:0);
+	check_sum:string (id:1);
+	image_path:string (id:2);
+}
+
+table RequestPitImagesResponse {
+	pit_image_list: [PitImage] (id:0);
+}
+
+root_type RequestPitImagesResponse;
diff --git a/scouting/webserver/requests/messages/submit_pit_image.fbs b/scouting/webserver/requests/messages/submit_pit_image.fbs
new file mode 100644
index 0000000..ffbaca2
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_pit_image.fbs
@@ -0,0 +1,9 @@
+namespace scouting.webserver.requests;
+
+table SubmitPitImage {
+	team_number:string (id:0);
+	image_path:string (id:1);
+	image_data:[ubyte] (id:2);
+}
+
+root_type SubmitPitImage;
diff --git a/scouting/webserver/requests/messages/submit_pit_image_response.fbs b/scouting/webserver/requests/messages/submit_pit_image_response.fbs
new file mode 100644
index 0000000..0b9d907
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_pit_image_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table SubmitPitImageResponse {
+    // empty response
+}
+
+root_type SubmitPitImageResponse;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 533959c..51612ff 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -25,6 +25,8 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions"
@@ -33,6 +35,8 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_pit_image"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_pit_image_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
@@ -49,6 +53,10 @@
 type Request2023DataScoutingResponseT = request_2023_data_scouting_response.Request2023DataScoutingResponseT
 type SubmitNotes = submit_notes.SubmitNotes
 type SubmitNotesResponseT = submit_notes_response.SubmitNotesResponseT
+type SubmitPitImage = submit_pit_image.SubmitPitImage
+type SubmitPitImageResponseT = submit_pit_image_response.SubmitPitImageResponseT
+type RequestPitImages = request_pit_images.RequestPitImages
+type RequestPitImagesResponseT = request_pit_images_response.RequestPitImagesResponseT
 type RequestNotesForTeam = request_notes_for_team.RequestNotesForTeam
 type RequestNotesForTeamResponseT = request_notes_for_team_response.RequestNotesForTeamResponseT
 type RequestShiftSchedule = request_shift_schedule.RequestShiftSchedule
@@ -77,7 +85,9 @@
 	ReturnStats2023ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string, preScouting bool) ([]db.Stats2023, error)
 	QueryAllShifts(int) ([]db.Shift, error)
 	QueryNotes(int32) ([]string, error)
+	QueryPitImages(string) ([]db.RequestedPitImage, error)
 	AddNotes(db.NotesData) error
+	AddPitImage(db.PitImage) error
 	AddDriverRanking(db.DriverRankingData) error
 	AddAction(db.Action) error
 	DeleteFromStats(string, int32, int32, string) error
@@ -378,6 +388,39 @@
 	w.Write(builder.FinishedBytes())
 }
 
+type submitPitImageScoutingHandler struct {
+	db Database
+}
+
+func (handler submitPitImageScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	request, success := parseRequest(w, requestBytes, "SubmitPitImage", submit_pit_image.GetRootAsSubmitPitImage)
+	if !success {
+		return
+	}
+
+	err = handler.db.AddPitImage(db.PitImage{
+		TeamNumber: string(request.TeamNumber()),
+		CheckSum:   db.ComputeSha256FromByteArray(request.ImageDataBytes()),
+		ImagePath:  string(request.ImagePath()),
+		ImageData:  request.ImageDataBytes(),
+	})
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
+		return
+	}
+
+	var response SubmitPitImageResponseT
+	builder := flatbuffers.NewBuilder(10)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 func ConvertActionsToStat(submitActions *submit_actions.SubmitActions) (db.Stats2023, error) {
 	overall_time := int64(0)
 	cycles := int64(0)
@@ -576,6 +619,42 @@
 	w.Write(builder.FinishedBytes())
 }
 
+type requestPitImagesHandler struct {
+	db Database
+}
+
+func (handler requestPitImagesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	request, success := parseRequest(w, requestBytes, "RequestPitImages", request_pit_images.GetRootAsRequestPitImages)
+	if !success {
+		return
+	}
+
+	images, err := handler.db.QueryPitImages(string(request.TeamNumber()))
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query pit images: %v", err))
+		return
+	}
+
+	var response RequestPitImagesResponseT
+	for _, data := range images {
+		response.PitImageList = append(response.PitImageList, &request_pit_images_response.PitImageT{
+			TeamNumber: data.TeamNumber,
+			ImagePath:  data.ImagePath,
+			CheckSum:   data.CheckSum,
+		})
+	}
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 type requestNotesForTeamHandler struct {
 	db Database
 }
@@ -928,6 +1007,8 @@
 	scoutingServer.Handle("/requests/request/all_driver_rankings", requestAllDriverRankingsHandler{db})
 	scoutingServer.Handle("/requests/request/2023_data_scouting", request2023DataScoutingHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_notes", submitNoteScoutingHandler{db})
+	scoutingServer.Handle("/requests/submit/submit_pit_image", submitPitImageScoutingHandler{db})
+	scoutingServer.Handle("/requests/request/pit_images", requestPitImagesHandler{db})
 	scoutingServer.Handle("/requests/request/notes_for_team", requestNotesForTeamHandler{db})
 	scoutingServer.Handle("/requests/submit/shift_schedule", submitShiftScheduleHandler{db})
 	scoutingServer.Handle("/requests/request/shift_schedule", requestShiftScheduleHandler{db})
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 20a63ca..e50583b 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -17,11 +17,14 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_pit_images_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_pit_image"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
 	flatbuffers "github.com/google/flatbuffers/go"
@@ -514,6 +517,86 @@
 	}
 }
 
+func TestSubmitPitImage(t *testing.T) {
+	database := MockDatabase{}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&database, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&submit_pit_image.SubmitPitImageT{
+		TeamNumber: "483A", ImagePath: "483Arobot.jpg",
+		ImageData: []byte{12, 43, 54, 34, 98},
+	}).Pack(builder))
+
+	_, err := debug.SubmitPitImage("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to submit pit image: ", err)
+	}
+
+	expected := []db.PitImage{
+		{
+			TeamNumber: "483A", CheckSum: "177d9dc52bc25f391232e82521259c378964c068832a9178d73448ba4ac5e0b1",
+			ImagePath: "483Arobot.jpg", ImageData: []byte{12, 43, 54, 34, 98},
+		},
+	}
+
+	if !reflect.DeepEqual(database.images, expected) {
+		t.Fatal("Submitted image did not match", expected, database.images)
+	}
+}
+
+func TestRequestPitImages(t *testing.T) {
+	db := MockDatabase{
+		images: []db.PitImage{
+			{
+				TeamNumber: "932", ImagePath: "pitimage.jpg",
+				ImageData: []byte{3, 34, 44, 65}, CheckSum: "abcdf",
+			},
+			{
+				TeamNumber: "234", ImagePath: "234robot.png",
+				ImageData: []byte{64, 54, 21, 21, 76, 32}, CheckSum: "egrfd",
+			},
+			{
+				TeamNumber: "93A", ImagePath: "abcd.jpg",
+				ImageData: []byte{92, 94, 10, 30, 57, 32, 32}, CheckSum: "rgegfd",
+			},
+		},
+	}
+
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&db, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&request_pit_images.RequestPitImagesT{"932"}).Pack(builder))
+
+	response, err := debug.RequestPitImages("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to request pit images: ", err)
+	}
+
+	expected := request_pit_images_response.RequestPitImagesResponseT{
+		PitImageList: []*request_pit_images_response.PitImageT{
+			{
+				TeamNumber: "932", ImagePath: "pitimage.jpg", CheckSum: "abcdf",
+			},
+		},
+	}
+
+	if len(expected.PitImageList) != len(response.PitImageList) {
+		t.Fatal("Expected ", expected, ", but got ", *response)
+	}
+
+	for i, pit_image := range expected.PitImageList {
+		if !reflect.DeepEqual(*pit_image, *response.PitImageList[i]) {
+			t.Fatal("Expected for pit image", i, ":", *pit_image, ", but got:", *response.PitImageList[i])
+		}
+	}
+}
+
 func TestRequestShiftSchedule(t *testing.T) {
 	db := MockDatabase{
 		shiftSchedule: []db.Shift{
@@ -1024,6 +1107,7 @@
 	driver_ranking []db.DriverRankingData
 	stats2023      []db.Stats2023
 	actions        []db.Action
+	images         []db.PitImage
 }
 
 func (database *MockDatabase) AddToMatch(match db.TeamMatch) error {
@@ -1085,6 +1169,20 @@
 	return []db.Shift{}, nil
 }
 
+func (database *MockDatabase) QueryPitImages(requestedTeam string) ([]db.RequestedPitImage, error) {
+	var results []db.RequestedPitImage
+	for _, data := range database.images {
+		if data.TeamNumber == requestedTeam {
+			results = append(results, db.RequestedPitImage{
+				TeamNumber: data.TeamNumber,
+				ImagePath:  data.ImagePath,
+				CheckSum:   data.CheckSum,
+			})
+		}
+	}
+	return results, nil
+}
+
 func (database *MockDatabase) AddDriverRanking(data db.DriverRankingData) error {
 	database.driver_ranking = append(database.driver_ranking, data)
 	return nil
@@ -1099,6 +1197,11 @@
 	return nil
 }
 
+func (database *MockDatabase) AddPitImage(pitImage db.PitImage) error {
+	database.images = append(database.images, pitImage)
+	return nil
+}
+
 func (database *MockDatabase) ReturnActions() ([]db.Action, error) {
 	return database.actions, nil
 }