Handle webserver requests from the scouting web page
Right now we don't actually serve a whole lot. It's mostly the
infrastructure. I added a simple data scouting submission as an
example. We can build up from there.
Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I0572a214039cdb61e5bddf6f7256955a06147099
diff --git a/scouting/webserver/BUILD b/scouting/webserver/BUILD
index ff91aa8..7d42bf9 100644
--- a/scouting/webserver/BUILD
+++ b/scouting/webserver/BUILD
@@ -7,6 +7,7 @@
target_compatible_with = ["@platforms//cpu:x86_64"],
visibility = ["//visibility:private"],
deps = [
+ "//scouting/webserver/requests",
"//scouting/webserver/server",
"//scouting/webserver/static",
],
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
index aebeef9..2baf36d 100644
--- a/scouting/webserver/main.go
+++ b/scouting/webserver/main.go
@@ -7,6 +7,7 @@
"os/signal"
"syscall"
+ "github.com/frc971/971-Robot-Code/scouting/webserver/requests"
"github.com/frc971/971-Robot-Code/scouting/webserver/server"
"github.com/frc971/971-Robot-Code/scouting/webserver/static"
)
@@ -18,6 +19,7 @@
scoutingServer := server.NewScoutingServer()
static.ServePages(scoutingServer, *dirPtr)
+ requests.HandleRequests(scoutingServer)
scoutingServer.Start(*portPtr)
fmt.Println("Serving", *dirPtr, "on port", *portPtr)
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
new file mode 100644
index 0000000..456730e
--- /dev/null
+++ b/scouting/webserver/requests/BUILD
@@ -0,0 +1,30 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "requests",
+ srcs = ["requests.go"],
+ importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/requests",
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//scouting/webserver/requests/messages:error_response_go_fbs",
+ "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
+ "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
+ "//scouting/webserver/server",
+ "@com_github_google_flatbuffers//go:go_default_library",
+ ],
+)
+
+go_test(
+ name = "requests_test",
+ srcs = ["requests_test.go"],
+ embed = [":requests"],
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ deps = [
+ "//scouting/webserver/requests/messages:error_response_go_fbs",
+ "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
+ "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
+ "//scouting/webserver/server",
+ "@com_github_google_flatbuffers//go:go_default_library",
+ ],
+)
diff --git a/scouting/webserver/requests/messages/BUILD b/scouting/webserver/requests/messages/BUILD
new file mode 100644
index 0000000..a32ca57
--- /dev/null
+++ b/scouting/webserver/requests/messages/BUILD
@@ -0,0 +1,21 @@
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_go_library", "flatbuffer_ts_library")
+
+[(
+ flatbuffer_go_library(
+ name = name + "_go_fbs",
+ srcs = [name + ".fbs"],
+ importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/" + name,
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:public"],
+ ),
+ flatbuffer_ts_library(
+ name = name + "_ts_fbs",
+ srcs = [name + ".fbs"],
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:public"],
+ ),
+) for name in (
+ "error_response",
+ "submit_data_scouting",
+ "submit_data_scouting_response",
+)]
diff --git a/scouting/webserver/requests/messages/error_response.fbs b/scouting/webserver/requests/messages/error_response.fbs
new file mode 100644
index 0000000..4371cb5
--- /dev/null
+++ b/scouting/webserver/requests/messages/error_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table ErrorResponse {
+ error_message:string (id: 0);
+}
+
+root_type ErrorResponse;
diff --git a/scouting/webserver/requests/messages/submit_data_scouting.fbs b/scouting/webserver/requests/messages/submit_data_scouting.fbs
new file mode 100644
index 0000000..63bba7a
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_data_scouting.fbs
@@ -0,0 +1,11 @@
+namespace scouting.webserver.requests;
+
+table SubmitDataScouting {
+ team:int (id: 0);
+ match:int (id: 1);
+
+ upper_goal_hits:int (id: 2);
+ // TODO: Implement the rest of this.
+}
+
+root_type SubmitDataScouting;
diff --git a/scouting/webserver/requests/messages/submit_data_scouting_response.fbs b/scouting/webserver/requests/messages/submit_data_scouting_response.fbs
new file mode 100644
index 0000000..fb7f765
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_data_scouting_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table SubmitDataScoutingResponse {
+ // TODO: Implement this.
+}
+
+root_type SubmitDataScoutingResponse;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
new file mode 100644
index 0000000..4839675
--- /dev/null
+++ b/scouting/webserver/requests/requests.go
@@ -0,0 +1,67 @@
+package requests
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
+ _ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+ "github.com/frc971/971-Robot-Code/scouting/webserver/server"
+ flatbuffers "github.com/google/flatbuffers/go"
+)
+
+// Handles unknown requests. Just returns a 404.
+func unknown(w http.ResponseWriter, req *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+}
+
+func respondWithError(w http.ResponseWriter, statusCode int, errorMessage string) {
+ builder := flatbuffers.NewBuilder(1024)
+ builder.Finish((&error_response.ErrorResponseT{
+ ErrorMessage: errorMessage,
+ }).Pack(builder))
+ w.WriteHeader(statusCode)
+ w.Write(builder.FinishedBytes())
+}
+
+func respondNotImplemented(w http.ResponseWriter) {
+ respondWithError(w, http.StatusNotImplemented, "")
+}
+
+// TODO(phil): Can we turn this into a generic?
+func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*submit_data_scouting.SubmitDataScouting, bool) {
+ success := true
+ defer func() {
+ if r := recover(); r != nil {
+ respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
+ success = false
+ }
+ }()
+ result := submit_data_scouting.GetRootAsSubmitDataScouting(buf, 0)
+ return result, success
+}
+
+// Handles a SubmitDataScouting request.
+func submitDataScouting(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
+ }
+
+ _, success := parseSubmitDataScouting(w, requestBytes)
+ if !success {
+ return
+ }
+
+ // TODO(phil): Actually handle the request.
+
+ respondNotImplemented(w)
+}
+
+func HandleRequests(scoutingServer server.ScoutingServer) {
+ scoutingServer.HandleFunc("/requests", unknown)
+ scoutingServer.HandleFunc("/requests/submit/data_scouting", submitDataScouting)
+}
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
new file mode 100644
index 0000000..11d728c
--- /dev/null
+++ b/scouting/webserver/requests/requests_test.go
@@ -0,0 +1,81 @@
+package requests
+
+import (
+ "bytes"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
+ _ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+ "github.com/frc971/971-Robot-Code/scouting/webserver/server"
+ flatbuffers "github.com/google/flatbuffers/go"
+)
+
+// Validates that an unhandled address results in a 404.
+func Test404(t *testing.T) {
+ scoutingServer := server.NewScoutingServer()
+ HandleRequests(scoutingServer)
+ scoutingServer.Start(8080)
+ defer scoutingServer.Stop()
+
+ resp, err := http.Get("http://localhost:8080/requests/foo")
+ if err != nil {
+ t.Fatalf("Failed to get data: %v", err)
+ }
+ if resp.StatusCode != http.StatusNotFound {
+ t.Fatalf("Expected error code 404, but got %d instead", resp.Status)
+ }
+}
+
+// Validates that we can submit new data scouting data.
+func TestSubmitDataScoutingError(t *testing.T) {
+ scoutingServer := server.NewScoutingServer()
+ HandleRequests(scoutingServer)
+ scoutingServer.Start(8080)
+ defer scoutingServer.Stop()
+
+ resp, err := http.Post("http://localhost:8080/requests/submit/data_scouting", "application/octet-stream", bytes.NewReader([]byte("")))
+ if err != nil {
+ t.Fatalf("Failed to send request: %v", err)
+ }
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Fatal("Unexpected status code. Got", resp.Status)
+ }
+
+ responseBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal("Failed to read response bytes:", err)
+ }
+ errorResponse := error_response.GetRootAsErrorResponse(responseBytes, 0)
+
+ errorMessage := string(errorResponse.ErrorMessage())
+ if errorMessage != "Failed to parse SubmitDataScouting: runtime error: index out of range [3] with length 0" {
+ t.Fatal("Got mismatched error message:", errorMessage)
+ }
+}
+
+// Validates that we can submit new data scouting data.
+func TestSubmitDataScouting(t *testing.T) {
+ scoutingServer := server.NewScoutingServer()
+ HandleRequests(scoutingServer)
+ scoutingServer.Start(8080)
+ defer scoutingServer.Stop()
+
+ builder := flatbuffers.NewBuilder(1024)
+ builder.Finish((&submit_data_scouting.SubmitDataScoutingT{
+ Team: 971,
+ Match: 1,
+ UpperGoalHits: 9971,
+ }).Pack(builder))
+
+ resp, err := http.Post("http://localhost:8080/requests/submit/data_scouting", "application/octet-stream", bytes.NewReader(builder.FinishedBytes()))
+ if err != nil {
+ t.Fatalf("Failed to send request: %v", err)
+ }
+ if resp.StatusCode != http.StatusNotImplemented {
+ t.Fatal("Unexpected status code. Got", resp.Status)
+ }
+ // TODO(phil): We have nothing to validate yet. Fix that.
+}
diff --git a/scouting/webserver/server/server.go b/scouting/webserver/server/server.go
index ff85794..c21e2a1 100644
--- a/scouting/webserver/server/server.go
+++ b/scouting/webserver/server/server.go
@@ -17,6 +17,9 @@
// Add a handler for a particular path. See upstream docs for this:
// https://pkg.go.dev/net/http#ServeMux.Handle
Handle(string, http.Handler)
+ // Add a handler function for a particular path. See upstream docs:
+ // https://pkg.go.dev/net/http#ServeMux.HandleFunc
+ HandleFunc(string, func(http.ResponseWriter, *http.Request))
// Starts the server on the specified port. Handlers cannot be added
// once this function is called.
Start(int)
@@ -54,6 +57,13 @@
server.mux.Handle(path, handler)
}
+func (server *scoutingServer) HandleFunc(path string, handler func(http.ResponseWriter, *http.Request)) {
+ if server.started {
+ log.Fatal("Cannot add handlers once server has started.")
+ }
+ server.mux.HandleFunc(path, handler)
+}
+
func (server *scoutingServer) Start(port int) {
if server.started {
log.Fatal("Cannot Start() a server a second time.")
diff --git a/scouting/webserver/static/BUILD b/scouting/webserver/static/BUILD
index 008d719..3166853 100644
--- a/scouting/webserver/static/BUILD
+++ b/scouting/webserver/static/BUILD
@@ -17,5 +17,6 @@
"test_pages/root.txt",
],
embed = [":static"],
+ target_compatible_with = ["@platforms//cpu:x86_64"],
deps = ["//scouting/webserver/server"],
)