Merge changes I8e9a3a44,I0e992ccf,I53881cf2

* changes:
  Recal pot after weird pot slip event
  Add ability to wait for a distance along a spline in auto
  Fix arm freakout
diff --git a/frc971/control_loops/python/graph.py b/frc971/control_loops/python/graph.py
index 1cc6f57..16c792bc 100644
--- a/frc971/control_loops/python/graph.py
+++ b/frc971/control_loops/python/graph.py
@@ -23,8 +23,10 @@
         self.canvas = FigureCanvas(fig)  # a Gtk.DrawingArea
         self.canvas.set_vexpand(True)
         self.canvas.set_size_request(800, 250)
-        self.callback_id = self.canvas.mpl_connect('motion_notify_event',
-                                                   self.on_mouse_move)
+        self.mouse_move_callback = self.canvas.mpl_connect(
+            'motion_notify_event', self.on_mouse_move)
+        self.click_callback = self.canvas.mpl_connect('button_press_event',
+                                                      self.on_click)
         self.add(self.canvas)
 
         # The current graph data
@@ -99,6 +101,24 @@
             if self.cursor_watcher is not None:
                 self.cursor_watcher.queue_draw()
 
+    def on_click(self, event):
+        """Same as on_mouse_move but also selects multisplines"""
+
+        if self.data is None:
+            return
+        total_steps_taken = self.data.shape[1]
+        total_time = self.dt * total_steps_taken
+        if event.xdata is not None:
+            # clip the position if still on the canvas, but off the graph
+            self.cursor = np.clip(event.xdata, 0, total_time)
+
+            self.redraw_cursor()
+
+            # tell the field to update too
+            if self.cursor_watcher is not None:
+                self.cursor_watcher.queue_draw()
+                self.cursor_watcher.on_graph_clicked()
+
     def redraw_cursor(self):
         """Redraws the cursor line"""
         # TODO: This redraws the entire graph and isn't very snappy
diff --git a/frc971/control_loops/python/path_edit.py b/frc971/control_loops/python/path_edit.py
index b5e8c1b..1659e55 100755
--- a/frc971/control_loops/python/path_edit.py
+++ b/frc971/control_loops/python/path_edit.py
@@ -287,7 +287,11 @@
 
                 if i == 0:
                     self.draw_robot_at_point(cr, spline, 0)
-                self.draw_robot_at_point(cr, spline, 1)
+
+                is_last_spline = spline is multispline.getLibsplines()[-1]
+
+                if multispline == self.active_multispline or is_last_spline:
+                    self.draw_robot_at_point(cr, spline, 1)
 
     def export_json(self, file_name):
         export_folder = Path(
@@ -422,6 +426,15 @@
                     prev_multispline.getSplines()[-1])
             self.queue_draw()
 
+    def on_graph_clicked(self):
+        if self.graph.cursor is not None:
+            cursor = self.graph.find_cursor()
+            if cursor is None:
+                return
+            multispline_index, x = cursor
+
+            self.active_multispline_index = multispline_index
+
     def do_button_release_event(self, event):
         self.drag_start = None
 
@@ -489,7 +502,7 @@
 
             multispline, result = Multispline.nearest_distance(
                 self.multisplines, cur_p)
-            if result and result.fun < 0.1:
+            if self.control_point_index == None and result and result.fun < 0.1:
                 self.active_multispline_index = self.multisplines.index(
                     multispline)
 
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 718711c..ca1af9e 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -48,14 +48,14 @@
 }
 
 type Action struct {
-	TeamNumber      string `gorm:"primaryKey"`
-	MatchNumber     int32  `gorm:"primaryKey"`
-	SetNumber       int32  `gorm:"primaryKey"`
-	CompLevel       string `gorm:"primaryKey"`
-	CompletedAction []byte
+	TeamNumber  string `gorm:"primaryKey"`
+	MatchNumber int32  `gorm:"primaryKey"`
+	SetNumber   int32  `gorm:"primaryKey"`
+	CompLevel   string `gorm:"primaryKey"`
 	// This contains a serialized scouting.webserver.requests.ActionType flatbuffer.
-	TimeStamp   int32 `gorm:"primaryKey"`
-	CollectedBy string
+	CompletedAction []byte
+	Timestamp       int64 `gorm:"primaryKey"`
+	CollectedBy     string
 }
 
 type NotesData struct {
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index ea83fa3..b0d34d8 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -761,27 +761,27 @@
 	correct := []Action{
 		Action{
 			TeamNumber: "1235", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0000, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0000, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1236", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0321, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0321, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1237", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0222, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0222, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1238", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0110, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0110, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1239", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0004, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0004, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1233", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0004, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0005, CollectedBy: "",
 		},
 	}
 
diff --git a/scouting/scouting_test.cy.js b/scouting/scouting_test.cy.js
index 0237c4b..8ac1880 100644
--- a/scouting/scouting_test.cy.js
+++ b/scouting/scouting_test.cy.js
@@ -105,7 +105,7 @@
   });
 
   //TODO(FILIP): Verify last action when the last action header gets added.
-  it('should: be able to get to submit screen in data scouting.', () => {
+  it('should: be able to submit data scouting.', () => {
     switchToTab('Data Entry');
     headerShouldBe('Team Selection');
     clickButton('Next');
@@ -131,11 +131,11 @@
     clickButton('Endgame');
     cy.get('[type="checkbox"]').check();
 
-    // Should be on submit screen.
-    // TODO(FILIP): Verify that submitting works once we add it.
-
     clickButton('End Match');
     headerShouldBe('Review and Submit');
+
+    clickButton('Submit');
+    headerShouldBe('Success');
   });
 
   it('should: be able to return to correct screen with undo for pick and place.', () => {
diff --git a/scouting/webserver/requests/debug/BUILD b/scouting/webserver/requests/debug/BUILD
index f3f4a72..d9bb030 100644
--- a/scouting/webserver/requests/debug/BUILD
+++ b/scouting/webserver/requests/debug/BUILD
@@ -14,6 +14,7 @@
         "//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_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_shift_schedule_response_go_fbs",
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index eb3a1ca..acb9dd4 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -16,6 +16,7 @@
 	"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_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_shift_schedule_response"
@@ -157,3 +158,9 @@
 		server+"/requests/submit/submit_driver_ranking", requestBytes,
 		submit_driver_ranking_response.GetRootAsSubmitDriverRankingResponse)
 }
+
+func SubmitActions(server string, requestBytes []byte) (*submit_actions_response.SubmitActionsResponseT, error) {
+	return sendMessage[submit_actions_response.SubmitActionsResponseT](
+		server+"/requests/submit/submit_actions", requestBytes,
+		submit_actions_response.GetRootAsSubmitActionsResponse)
+}
diff --git a/scouting/webserver/requests/messages/submit_actions.fbs b/scouting/webserver/requests/messages/submit_actions.fbs
index 8c79097..d8aa98d 100644
--- a/scouting/webserver/requests/messages/submit_actions.fbs
+++ b/scouting/webserver/requests/messages/submit_actions.fbs
@@ -62,5 +62,6 @@
     set_number:int (id: 2);
     comp_level:string (id: 3);
     actions_list:[Action] (id:4);
+    //TODO: delete this field
     collected_by:string (id: 5);
 }
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 9305274..8646a30 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -56,6 +56,7 @@
 type SubmitDriverRanking = submit_driver_ranking.SubmitDriverRanking
 type SubmitDriverRankingResponseT = submit_driver_ranking_response.SubmitDriverRankingResponseT
 type SubmitActions = submit_actions.SubmitActions
+type Action = submit_actions.Action
 type SubmitActionsResponseT = submit_actions_response.SubmitActionsResponseT
 
 // The interface we expect the database abstraction to conform to.
@@ -74,6 +75,7 @@
 	QueryNotes(int32) ([]string, error)
 	AddNotes(db.NotesData) error
 	AddDriverRanking(db.DriverRankingData) error
+	AddAction(db.Action) error
 }
 
 // Handles unknown requests. Just returns a 404.
@@ -785,6 +787,62 @@
 	w.Write(builder.FinishedBytes())
 }
 
+type submitActionsHandler struct {
+	db Database
+}
+
+func (handler submitActionsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	// Get the username of the person submitting the data.
+	username := parseUsername(req)
+
+	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, "SubmitActions", submit_actions.GetRootAsSubmitActions)
+	if !success {
+		return
+	}
+
+	log.Println("Got actions for match", request.MatchNumber(), "team", request.TeamNumber(), "from", username)
+
+	for i := 0; i < request.ActionsListLength(); i++ {
+
+		var action Action
+		request.ActionsList(&action, i)
+
+		dbAction := db.Action{
+			TeamNumber:  string(request.TeamNumber()),
+			MatchNumber: request.MatchNumber(),
+			SetNumber:   request.SetNumber(),
+			CompLevel:   string(request.CompLevel()),
+			//TODO: Serialize CompletedAction
+			CompletedAction: []byte{},
+			Timestamp:       action.Timestamp(),
+			CollectedBy:     username,
+		}
+
+		// Do some error checking.
+		if action.Timestamp() < 0 {
+			respondWithError(w, http.StatusBadRequest, fmt.Sprint(
+				"Invalid timestamp field value of ", action.Timestamp()))
+			return
+		}
+
+		err = handler.db.AddAction(dbAction)
+		if err != nil {
+			respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to add action to database: ", err))
+			return
+		}
+	}
+
+	builder := flatbuffers.NewBuilder(50 * 1024)
+	builder.Finish((&SubmitActionsResponseT{}).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 func HandleRequests(db Database, scoutingServer server.ScoutingServer) {
 	scoutingServer.HandleFunc("/requests", unknown)
 	scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
@@ -796,4 +854,5 @@
 	scoutingServer.Handle("/requests/submit/shift_schedule", submitShiftScheduleHandler{db})
 	scoutingServer.Handle("/requests/request/shift_schedule", requestShiftScheduleHandler{db})
 	scoutingServer.Handle("/requests/submit/submit_driver_ranking", SubmitDriverRankingHandler{db})
+	scoutingServer.Handle("/requests/submit/submit_actions", submitActionsHandler{db})
 }
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index dab3174..ac644ea 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -752,6 +752,109 @@
 	}
 }
 
+func packAction(action *submit_actions.ActionT) []byte {
+	builder := flatbuffers.NewBuilder(50 * 1024)
+	builder.Finish((action).Pack(builder))
+	return (builder.FinishedBytes())
+}
+
+func TestAddingActions(t *testing.T) {
+	database := MockDatabase{}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&database, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&submit_actions.SubmitActionsT{
+		TeamNumber:  "1234",
+		MatchNumber: 4,
+		SetNumber:   1,
+		CompLevel:   "qual",
+		ActionsList: []*submit_actions.ActionT{
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePickupObjectAction,
+					Value: &submit_actions.PickupObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCube,
+						Auto:       true,
+					},
+				},
+				Timestamp: 2400,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePlaceObjectAction,
+					Value: &submit_actions.PlaceObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCube,
+						ScoreLevel: submit_actions.ScoreLevelkLow,
+						Auto:       false,
+					},
+				},
+				Timestamp: 1009,
+			},
+		},
+	}).Pack(builder))
+
+	_, err := debug.SubmitActions("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to submit actions: ", err)
+	}
+
+	// Make sure that the data made it into the database.
+	// TODO: Add this back when we figure out how to add the serialized action into the database.
+
+	/* expectedActionsT := []*submit_actions.ActionT{
+		{
+			ActionTaken: &submit_actions.ActionTypeT{
+				Type:	submit_actions.ActionTypePickupObjectAction,
+				Value:	&submit_actions.PickupObjectActionT{
+					ObjectType: submit_actions.ObjectTypekCube,
+					Auto: true,
+				},
+			},
+			Timestamp:       2400,
+		},
+		{
+			ActionTaken: &submit_actions.ActionTypeT{
+				Type:	submit_actions.ActionTypePlaceObjectAction,
+				Value:	&submit_actions.PlaceObjectActionT{
+					ObjectType: submit_actions.ObjectTypekCube,
+					ScoreLevel: submit_actions.ScoreLevelkLow,
+					Auto: false,
+				},
+			},
+			Timestamp:       1009,
+		},
+	} */
+
+	expectedActions := []db.Action{
+		{
+			TeamNumber:      "1234",
+			MatchNumber:     4,
+			SetNumber:       1,
+			CompLevel:       "qual",
+			CollectedBy:     "debug_cli",
+			CompletedAction: []byte{},
+			Timestamp:       2400,
+		},
+		{
+			TeamNumber:      "1234",
+			MatchNumber:     4,
+			SetNumber:       1,
+			CompLevel:       "qual",
+			CollectedBy:     "debug_cli",
+			CompletedAction: []byte{},
+			Timestamp:       1009,
+		},
+	}
+
+	if !reflect.DeepEqual(expectedActions, database.actions) {
+		t.Fatal("Expected ", expectedActions, ", but got:", database.actions)
+	}
+
+}
+
 // A mocked database we can use for testing. Add functionality to this as
 // needed for your tests.
 
@@ -761,6 +864,7 @@
 	shiftSchedule  []db.Shift
 	driver_ranking []db.DriverRankingData
 	stats2023      []db.Stats2023
+	actions        []db.Action
 }
 
 func (database *MockDatabase) AddToMatch(match db.TeamMatch) error {
@@ -830,3 +934,12 @@
 func (database *MockDatabase) ReturnAllDriverRankings() ([]db.DriverRankingData, error) {
 	return database.driver_ranking, nil
 }
+
+func (database *MockDatabase) AddAction(action db.Action) error {
+	database.actions = append(database.actions, action)
+	return nil
+}
+
+func (database *MockDatabase) ReturnActions() ([]db.Action, error) {
+	return database.actions, nil
+}
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 9ab5b7a..71c8de2 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -301,7 +301,7 @@
     builder.finish(SubmitActions.endSubmitActions(builder));
 
     const buffer = builder.asUint8Array();
-    const res = await fetch('/requests/submit/actions', {
+    const res = await fetch('/requests/submit/submit_actions', {
       method: 'POST',
       body: buffer,
     });
diff --git a/y2023/control_loops/python/graph_edit.py b/y2023/control_loops/python/graph_edit.py
index bbc7a1b..99b09af 100644
--- a/y2023/control_loops/python/graph_edit.py
+++ b/y2023/control_loops/python/graph_edit.py
@@ -244,6 +244,7 @@
         self.set_size_request(ARM_AREA_WIDTH, ARM_AREA_HEIGHT)
         self.center = (0, 0)
         self.shape = (ARM_AREA_WIDTH, ARM_AREA_HEIGHT)
+        self.window_shape = (ARM_AREA_WIDTH, ARM_AREA_HEIGHT)
         self.theta_version = False
 
         self.init_extents()
@@ -326,9 +327,6 @@
     def on_draw(self, widget, event):
         cr = self.get_window().cairo_create()
 
-        self.window_shape = (self.get_window().get_geometry().width,
-                             self.get_window().get_geometry().height)
-
         cr.save()
         cr.set_font_size(20)
         cr.translate(self.window_shape[0] / 2, self.window_shape[1] / 2)
diff --git a/y2023/www/field.html b/y2023/www/field.html
index cc89bb9..6bd2fc0 100644
--- a/y2023/www/field.html
+++ b/y2023/www/field.html
@@ -56,6 +56,10 @@
       <td>Game Piece Held</td>
       <td id="game_piece"> NA </td>
     </tr>
+    <tr>
+      <td>Game Piece Position (+ = left, 0 = empty)</td>
+      <td id="game_piece_position"> NA </td>
+    </tr>
   </table>
 
   <table>
diff --git a/y2023/www/field_handler.ts b/y2023/www/field_handler.ts
index 2f62c7d..24a55fa 100644
--- a/y2023/www/field_handler.ts
+++ b/y2023/www/field_handler.ts
@@ -56,6 +56,8 @@
       (document.getElementById('arm_state') as HTMLElement);
   private gamePiece: HTMLElement =
       (document.getElementById('game_piece') as HTMLElement);
+  private gamePiecePosition: HTMLElement =
+      (document.getElementById('game_piece_position') as HTMLElement);
   private armX: HTMLElement = (document.getElementById('arm_x') as HTMLElement);
   private armY: HTMLElement = (document.getElementById('arm_y') as HTMLElement);
   private circularIndex: HTMLElement =
@@ -387,6 +389,8 @@
       this.armState.innerHTML =
           ArmState[this.superstructureStatus.arm().state()];
       this.gamePiece.innerHTML = Class[this.superstructureStatus.gamePiece()];
+      this.gamePiecePosition.innerHTML =
+          this.superstructureStatus.gamePiecePosition().toFixed(4);
       this.armX.innerHTML = this.superstructureStatus.arm().armX().toFixed(2);
       this.armY.innerHTML = this.superstructureStatus.arm().armY().toFixed(2);
       this.circularIndex.innerHTML =