Add Pit Scouting Tab
Signed-off-by: Emily Markova <emily.markova@gmail.com>
Change-Id: Iede446546e20f2915bb53e134050b5025976da36
diff --git a/scouting/db/BUILD b/scouting/db/BUILD
index 154cab6..9447a5f 100644
--- a/scouting/db/BUILD
+++ b/scouting/db/BUILD
@@ -18,9 +18,7 @@
name = "db_test",
size = "small",
srcs = ["db_test.go"],
- data = [
- "//scouting/db/testdb_server",
- ],
+ data = ["//scouting/db/testdb_server"],
embed = [":db"],
target_compatible_with = ["@platforms//cpu:x86_64"],
deps = ["@com_github_davecgh_go_spew//spew"],
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 1a43634..578edb6 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -1,6 +1,7 @@
package db
import (
+ "crypto/sha256"
"errors"
"fmt"
"gorm.io/driver/postgres"
@@ -27,6 +28,19 @@
R1scouter, R2scouter, R3scouter, B1scouter, B2scouter, B3scouter string
}
+type PitImage struct {
+ TeamNumber string `gorm:"primaryKey"`
+ CheckSum string `gorm:"primaryKey"`
+ ImagePath string
+ ImageData []byte
+}
+
+type RequestedPitImage struct {
+ TeamNumber string
+ CheckSum string `gorm:"primaryKey"`
+ ImagePath string
+}
+
type Stats2023 struct {
// This is set to `true` for "pre-scouted" matches. This means that the
// match information is unlikely to correspond with an entry in the
@@ -125,7 +139,7 @@
return nil, errors.New(fmt.Sprint("Failed to connect to postgres: ", err))
}
- err = database.AutoMigrate(&TeamMatch{}, &Shift{}, &Stats2023{}, &Action{}, &NotesData{}, &Ranking{}, &DriverRankingData{}, &ParsedDriverRankingData{})
+ err = database.AutoMigrate(&TeamMatch{}, &Shift{}, &Stats2023{}, &Action{}, &PitImage{}, &NotesData{}, &Ranking{}, &DriverRankingData{}, &ParsedDriverRankingData{})
if err != nil {
database.Delete()
return nil, errors.New(fmt.Sprint("Failed to create/migrate tables: ", err))
@@ -167,6 +181,11 @@
return result.Error
}
+func (database *Database) AddPitImage(p PitImage) error {
+ result := database.Create(&p)
+ return result.Error
+}
+
func (database *Database) AddToStats2023(s Stats2023) error {
if !s.PreScouting {
matches, err := database.QueryMatchesString(s.TeamNumber)
@@ -250,6 +269,12 @@
return actions, result.Error
}
+func (database *Database) ReturnPitImages() ([]PitImage, error) {
+ var images []PitImage
+ result := database.Find(&images)
+ return images, result.Error
+}
+
func (database *Database) ReturnStats2023() ([]Stats2023, error) {
var stats2023 []Stats2023
result := database.Find(&stats2023)
@@ -279,6 +304,28 @@
return matches, result.Error
}
+func (database *Database) QueryPitImages(teamNumber_ string) ([]RequestedPitImage, error) {
+ var requestedPitImages []RequestedPitImage
+ result := database.Model(&PitImage{}).
+ Where("team_number = $1", teamNumber_).
+ Find(&requestedPitImages)
+
+ return requestedPitImages, result.Error
+}
+
+func (database *Database) QueryPitImageByChecksum(checksum_ string) (PitImage, error) {
+ var pitImage PitImage
+ result := database.
+ Where("check_sum = $1", checksum_).
+ Find(&pitImage)
+ return pitImage, result.Error
+}
+
+func ComputeSha256FromByteArray(arr []byte) string {
+ sum := sha256.Sum256(arr)
+ return fmt.Sprintf("%x", sum)
+}
+
func (database *Database) QueryMatchesString(teamNumber_ string) ([]TeamMatch, error) {
var matches []TeamMatch
result := database.
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index d49e649..94dce7e 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -609,6 +609,82 @@
}
}
+func TestQueryPitImages(t *testing.T) {
+ fixture := createDatabase(t)
+ defer fixture.TearDown()
+
+ testDatabase := []PitImage{
+ PitImage{
+ TeamNumber: "723", CheckSum: "8be8h9829hf98wp",
+ ImagePath: "image1.jpg", ImageData: []byte{14, 15, 32, 54},
+ },
+ PitImage{
+ TeamNumber: "237", CheckSum: "br78232b6r7iaa",
+ ImagePath: "bot.png", ImageData: []byte{32, 54, 23, 00},
+ },
+ PitImage{
+ TeamNumber: "125A", CheckSum: "b63c728bqiq8a73",
+ ImagePath: "file123.jpeg", ImageData: []byte{32, 05, 01, 28},
+ },
+ }
+
+ for i := 0; i < len(testDatabase); i++ {
+ err := fixture.db.AddPitImage(testDatabase[i])
+ check(t, err, fmt.Sprint("Failed to add pit image", i))
+ }
+
+ correct := []RequestedPitImage{
+ RequestedPitImage{
+ TeamNumber: "723", CheckSum: "8be8h9829hf98wp",
+ ImagePath: "image1.jpg",
+ },
+ }
+
+ got, err := fixture.db.QueryPitImages("723")
+ check(t, err, "Failed to query shift for team 723")
+
+ if !reflect.DeepEqual(correct, got) {
+ t.Fatalf("Got %#v,\nbut expected %#v.", got, correct)
+ }
+}
+
+func TestQueryPitImageByChecksum(t *testing.T) {
+ fixture := createDatabase(t)
+ defer fixture.TearDown()
+
+ testDatabase := []PitImage{
+ PitImage{
+ TeamNumber: "723", CheckSum: "8be8h9829hf98wp",
+ ImagePath: "image1.jpg", ImageData: []byte{05, 32, 00, 74, 28},
+ },
+ PitImage{
+ TeamNumber: "237", CheckSum: "br78232b6r7iaa",
+ ImagePath: "bot.png", ImageData: []byte{32, 54, 23, 00},
+ },
+ PitImage{
+ TeamNumber: "125A", CheckSum: "b63c728bqiq8a73",
+ ImagePath: "file123.jpeg", ImageData: []byte{32, 05, 01, 28},
+ },
+ }
+
+ for i := 0; i < len(testDatabase); i++ {
+ err := fixture.db.AddPitImage(testDatabase[i])
+ check(t, err, fmt.Sprint("Failed to add pit image", i))
+ }
+
+ correctPitImage := PitImage{
+ TeamNumber: "125A", CheckSum: "b63c728bqiq8a73",
+ ImagePath: "file123.jpeg", ImageData: []byte{32, 05, 01, 28},
+ }
+
+ got, err := fixture.db.QueryPitImageByChecksum("b63c728bqiq8a73")
+ check(t, err, "Failed to query shift for checksum 'b63c728bqiq8a73'")
+
+ if !reflect.DeepEqual(correctPitImage, got) {
+ t.Fatalf("Got %#v,\nbut expected %#v.", got, correctPitImage)
+ }
+}
+
func TestQueryRankingsDB(t *testing.T) {
fixture := createDatabase(t)
defer fixture.TearDown()
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
index c5bed2c..d655708 100644
--- a/scouting/webserver/main.go
+++ b/scouting/webserver/main.go
@@ -133,7 +133,7 @@
defer database.Delete()
scoutingServer := server.NewScoutingServer()
- static.ServePages(scoutingServer, *dirPtr)
+ static.ServePages(scoutingServer, *dirPtr, database)
requests.HandleRequests(database, scoutingServer)
scoutingServer.Start(*portPtr)
fmt.Println("Serving", *dirPtr, "on port", *portPtr)
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
}
diff --git a/scouting/webserver/static/BUILD b/scouting/webserver/static/BUILD
index 0cf22f3..4b40c76 100644
--- a/scouting/webserver/static/BUILD
+++ b/scouting/webserver/static/BUILD
@@ -6,7 +6,10 @@
importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/static",
target_compatible_with = ["@platforms//cpu:x86_64"],
visibility = ["//visibility:public"],
- deps = ["//scouting/webserver/server"],
+ deps = [
+ "//scouting/db",
+ "//scouting/webserver/server",
+ ],
)
go_test(
@@ -19,5 +22,8 @@
],
embed = [":static"],
target_compatible_with = ["@platforms//cpu:x86_64"],
- deps = ["//scouting/webserver/server"],
+ deps = [
+ "//scouting/db",
+ "//scouting/webserver/server",
+ ],
)
diff --git a/scouting/webserver/static/static.go b/scouting/webserver/static/static.go
index fbad476..8e2e400 100644
--- a/scouting/webserver/static/static.go
+++ b/scouting/webserver/static/static.go
@@ -7,12 +7,14 @@
"fmt"
"io"
"log"
+ "mime"
"net/http"
"os"
"path/filepath"
"strings"
"time"
+ "github.com/frc971/971-Robot-Code/scouting/db"
"github.com/frc971/971-Robot-Code/scouting/webserver/server"
)
@@ -28,6 +30,10 @@
"X-Accel-Expires": "0",
}
+type ScoutingDatabase interface {
+ QueryPitImageByChecksum(checksum string) (db.PitImage, error)
+}
+
func MaybeNoCache(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// We force the browser not to cache index.html so that
@@ -98,7 +104,7 @@
return shaSums
}
-func HandleShaUrl(directory string, h http.Handler) http.Handler {
+func HandleShaUrl(directory string, h http.Handler, scoutingDatabase ScoutingDatabase) http.Handler {
shaSums := findAllFileShas(directory)
fn := func(w http.ResponseWriter, r *http.Request) {
@@ -132,9 +138,20 @@
// We found a file with this checksum. Serve that file.
r.URL.Path = path
} else {
- // No file with this checksum found.
- w.WriteHeader(http.StatusNotFound)
- return
+ pitImage, err := scoutingDatabase.QueryPitImageByChecksum(hash)
+ if err == nil {
+ if parts[3] != pitImage.ImagePath {
+ log.Println("Got ", parts[3], "expected", pitImage.ImagePath)
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ w.Header().Add("Content-Type", mime.TypeByExtension(pitImage.ImagePath))
+ w.Write(pitImage.ImageData)
+ return
+ } else { // No file with this checksum found.
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
}
h.ServeHTTP(w, r)
@@ -143,11 +160,10 @@
return http.HandlerFunc(fn)
}
-// Serve pages in the specified directory.
-func ServePages(scoutingServer server.ScoutingServer, directory string) {
+func ServePages(scoutingServer server.ScoutingServer, directory string, scoutingDatabase ScoutingDatabase) {
// Serve the / endpoint given a folder of pages.
scoutingServer.Handle("/", MaybeNoCache(http.FileServer(http.Dir(directory))))
// Also serve files in a checksum-addressable manner.
- scoutingServer.Handle("/sha256/", HandleShaUrl(directory, http.FileServer(http.Dir(directory))))
+ scoutingServer.Handle("/sha256/", HandleShaUrl(directory, http.FileServer(http.Dir(directory)), scoutingDatabase))
}
diff --git a/scouting/webserver/static/static_test.go b/scouting/webserver/static/static_test.go
index 1d036a0..a68d973 100644
--- a/scouting/webserver/static/static_test.go
+++ b/scouting/webserver/static/static_test.go
@@ -1,14 +1,30 @@
package static
import (
+ "errors"
"fmt"
"io/ioutil"
"net/http"
"testing"
+ "github.com/frc971/971-Robot-Code/scouting/db"
"github.com/frc971/971-Robot-Code/scouting/webserver/server"
)
+type MockDatabase struct {
+ images []db.PitImage
+}
+
+func (database *MockDatabase) QueryPitImageByChecksum(checksum string) (db.PitImage, error) {
+ for _, data := range database.images {
+ if data.CheckSum == checksum {
+ return data, nil
+ }
+ }
+
+ return db.PitImage{}, errors.New("Could not find pit image")
+}
+
func expectEqual(t *testing.T, actual string, expected string) {
if actual != expected {
t.Error("Expected ", actual, " to equal ", expected)
@@ -16,6 +32,15 @@
}
func TestServing(t *testing.T) {
+ database := MockDatabase{
+ images: []db.PitImage{
+ {
+ TeamNumber: "971", CheckSum: "3yi32rhewd23",
+ ImagePath: "abc.png", ImageData: []byte("hello"),
+ },
+ },
+ }
+
cases := []struct {
// The path to request from the server.
path string
@@ -26,10 +51,11 @@
{"/", "<h1>This is the index</h1>\n"},
{"/root.txt", "Hello, this is the root page!"},
{"/page.txt", "Hello from a page!"},
+ {"/sha256/3yi32rhewd23/abc.png", "hello"},
}
scoutingServer := server.NewScoutingServer()
- ServePages(scoutingServer, "test_pages")
+ ServePages(scoutingServer, "test_pages", &database)
scoutingServer.Start(8080)
defer scoutingServer.Stop()
@@ -43,8 +69,9 @@
// Makes sure that requesting / sets the proper headers so it doesn't get
// cached.
func TestDisallowedCache(t *testing.T) {
+ database := MockDatabase{}
scoutingServer := server.NewScoutingServer()
- ServePages(scoutingServer, "test_pages")
+ ServePages(scoutingServer, "test_pages", &database)
scoutingServer.Start(8080)
defer scoutingServer.Stop()
@@ -61,8 +88,9 @@
// Makes sure that requesting anything other than / doesn't set the "do not
// cache" headers.
func TestAllowedCache(t *testing.T) {
+ database := MockDatabase{}
scoutingServer := server.NewScoutingServer()
- ServePages(scoutingServer, "test_pages")
+ ServePages(scoutingServer, "test_pages", &database)
scoutingServer.Start(8080)
defer scoutingServer.Stop()
@@ -77,11 +105,23 @@
}
func TestSha256(t *testing.T) {
+ database := MockDatabase{
+ images: []db.PitImage{
+ {
+ TeamNumber: "971", CheckSum: "3yi32rhewd23",
+ ImagePath: "abc.png", ImageData: []byte{32, 54, 23, 00},
+ },
+ },
+ }
scoutingServer := server.NewScoutingServer()
- ServePages(scoutingServer, "test_pages")
+ ServePages(scoutingServer, "test_pages", &database)
scoutingServer.Start(8080)
defer scoutingServer.Stop()
+ //Validate receiving the correct byte sequence from image request.
+ byteDataReceived := getData("sha256/3yi32rhewd23/abc.png", t)
+ expectEqual(t, string(byteDataReceived), string([]byte{32, 54, 23, 00}))
+
// Validate a valid checksum.
dataReceived := getData("sha256/553b9b29647a112136986cf93c57b988d1f12dc43d3b774f14a24e58d272dbff/root.txt", t)
expectEqual(t, dataReceived, "Hello, this is the root page!")
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index 6ecad54..55e9a8f 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -19,6 +19,7 @@
"//scouting/www/entry",
"//scouting/www/match_list",
"//scouting/www/notes",
+ "//scouting/www/pit_scouting",
"//scouting/www/shift_schedule",
"//scouting/www/view",
],
diff --git a/scouting/www/app/app.module.ts b/scouting/www/app/app.module.ts
index 7b200d0..bf393e4 100644
--- a/scouting/www/app/app.module.ts
+++ b/scouting/www/app/app.module.ts
@@ -9,6 +9,7 @@
import {ShiftScheduleModule} from '../shift_schedule';
import {ViewModule} from '../view';
import {DriverRankingModule} from '../driver_ranking';
+import {PitScoutingModule} from '../pit_scouting';
@NgModule({
declarations: [App],
@@ -21,6 +22,7 @@
ShiftScheduleModule,
DriverRankingModule,
ViewModule,
+ PitScoutingModule,
],
exports: [App],
bootstrap: [App],
diff --git a/scouting/www/app/app.ng.html b/scouting/www/app/app.ng.html
index b7f1873..5f7ea9f 100644
--- a/scouting/www/app/app.ng.html
+++ b/scouting/www/app/app.ng.html
@@ -64,6 +64,15 @@
View
</a>
</li>
+ <li class="nav-item">
+ <a
+ class="nav-link"
+ [class.active]="tabIs('Pit')"
+ (click)="switchTabToGuarded('Pit')"
+ >
+ Pit
+ </a>
+ </li>
</ul>
<ng-container [ngSwitch]="tab">
@@ -83,4 +92,5 @@
<app-driver-ranking *ngSwitchCase="'DriverRanking'"></app-driver-ranking>
<shift-schedule *ngSwitchCase="'ShiftSchedule'"></shift-schedule>
<app-view *ngSwitchCase="'View'"></app-view>
+ <app-pit-scouting *ngSwitchCase="'Pit'"></app-pit-scouting>
</ng-container>
diff --git a/scouting/www/app/app.ts b/scouting/www/app/app.ts
index c19895a..16e3197 100644
--- a/scouting/www/app/app.ts
+++ b/scouting/www/app/app.ts
@@ -6,7 +6,8 @@
| 'Entry'
| 'DriverRanking'
| 'ShiftSchedule'
- | 'View';
+ | 'View'
+ | 'Pit';
// Ignore the guard for tabs that don't require the user to enter any data.
const unguardedTabs: Tab[] = ['MatchList'];
diff --git a/scouting/www/pit_scouting/BUILD b/scouting/www/pit_scouting/BUILD
new file mode 100644
index 0000000..740dee1
--- /dev/null
+++ b/scouting/www/pit_scouting/BUILD
@@ -0,0 +1,18 @@
+load("@npm//:defs.bzl", "npm_link_all_packages")
+load("//tools/build_rules:js.bzl", "ng_pkg")
+
+npm_link_all_packages(name = "node_modules")
+
+ng_pkg(
+ name = "pit_scouting",
+ extra_srcs = [
+ "//scouting/www:app_common_css",
+ ],
+ deps = [
+ ":node_modules/@angular/forms",
+ "//scouting/webserver/requests/messages:error_response_ts_fbs",
+ "//scouting/webserver/requests/messages:submit_pit_image_response_ts_fbs",
+ "//scouting/webserver/requests/messages:submit_pit_image_ts_fbs",
+ "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+ ],
+)
diff --git a/scouting/www/pit_scouting/package.json b/scouting/www/pit_scouting/package.json
new file mode 100644
index 0000000..e58fc51
--- /dev/null
+++ b/scouting/www/pit_scouting/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@org_frc971/scouting/www/pit_scouting",
+ "private": true,
+ "dependencies": {
+ "@angular/forms": "15.1.5"
+ }
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.component.css b/scouting/www/pit_scouting/pit_scouting.component.css
new file mode 100644
index 0000000..b224d73
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.component.css
@@ -0,0 +1,8 @@
+* {
+ padding: 10px;
+}
+
+button {
+ touch-action: manipulation;
+ margin-top: 1vh;
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.component.ts b/scouting/www/pit_scouting/pit_scouting.component.ts
new file mode 100644
index 0000000..b74c119
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.component.ts
@@ -0,0 +1,75 @@
+import {Component} from '@angular/core';
+import {Builder, ByteBuffer} from 'flatbuffers';
+import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
+import {SubmitPitImage} from '../../webserver/requests/messages/submit_pit_image_generated';
+
+type Section = 'TeamSelection' | 'Data';
+
+interface Input {
+ teamNumber: number;
+ pitImage: HTMLImageElement;
+}
+
+@Component({
+ selector: 'app-pit-scouting',
+ templateUrl: './pit_scouting.ng.html',
+ styleUrls: ['../app/common.css', './pit_scouting.component.css'],
+})
+export class PitScoutingComponent {
+ section: Section = 'Data';
+
+ errorMessage = '';
+ teamNumber: number = 971;
+ pitImage: string = '';
+
+ async readFile(file): Promise<ArrayBuffer> {
+ return new Promise((resolve, reject) => {
+ let reader = new FileReader();
+ reader.addEventListener('loadend', (e) =>
+ resolve(e.target.result as ArrayBuffer)
+ );
+ reader.addEventListener('error', reject);
+ reader.readAsArrayBuffer(file);
+ });
+ }
+
+ async getImageData() {
+ const selectedFile = (<HTMLInputElement>document.getElementById('pitImage'))
+ .files[0];
+ return new Uint8Array(await this.readFile(selectedFile));
+ }
+
+ async submitData() {
+ const builder = new Builder();
+ const teamNumber = builder.createString(this.teamNumber.toString());
+ const pitImage = builder.createString(this.pitImage.toString());
+ const imageData = SubmitPitImage.createImageDataVector(
+ builder,
+ await this.getImageData()
+ );
+ builder.finish(
+ SubmitPitImage.createSubmitPitImage(
+ builder,
+ teamNumber,
+ pitImage,
+ imageData
+ )
+ );
+
+ const buffer = builder.asUint8Array();
+ const res = await fetch('/requests/submit/submit_pit_image', {
+ method: 'POST',
+ body: buffer,
+ });
+ if (!res.ok) {
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
+
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ }
+ this.section = 'TeamSelection';
+ this.pitImage = '';
+ }
+}
diff --git a/scouting/www/pit_scouting/pit_scouting.module.ts b/scouting/www/pit_scouting/pit_scouting.module.ts
new file mode 100644
index 0000000..7d92f6c
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.module.ts
@@ -0,0 +1,11 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {PitScoutingComponent} from './pit_scouting.component';
+
+@NgModule({
+ declarations: [PitScoutingComponent],
+ exports: [PitScoutingComponent],
+ imports: [CommonModule, FormsModule],
+})
+export class PitScoutingModule {}
diff --git a/scouting/www/pit_scouting/pit_scouting.ng.html b/scouting/www/pit_scouting/pit_scouting.ng.html
new file mode 100644
index 0000000..98aa313
--- /dev/null
+++ b/scouting/www/pit_scouting/pit_scouting.ng.html
@@ -0,0 +1,30 @@
+<div class="header">
+ <h2>Pit Scouting</h2>
+</div>
+<ng-container [ngSwitch]="section">
+ <div *ngSwitchCase="'Data'" id="pitImageSelection" class="container-fluid">
+ <label for="teamNumber">Team Number</label>
+ <input
+ [(ngModel)]="teamNumber"
+ type="string"
+ id="teamNumber"
+ min="1"
+ max="9999"
+ />
+ <form action="setPitImage()">
+ <label for="pitImage">Select pit image:</label>
+ <br />
+ <input
+ id="pitImage"
+ [(ngModel)]="pitImage"
+ type="file"
+ accept="image/*"
+ />
+ </form>
+ <button id="submit-button" class="btn btn-success" (click)="submitData()">
+ Submit
+ </button>
+ </div>
+
+ <span class="error_message" role="alert">{{ errorMessage }}</span>
+</ng-container>