Skip to content
This repository was archived by the owner on Mar 21, 2025. It is now read-only.

Commit 88ff020

Browse files
authored
Add control panel for camera settings (#216)
* Add control panel for camera ISO & shutter speed settings * Add controls to change white balance settings
1 parent 7fdc593 commit 88ff020

File tree

12 files changed

+703
-76
lines changed

12 files changed

+703
-76
lines changed

internal/app/pslive/routes/instruments/planktoscope.go

+261-54
Original file line numberDiff line numberDiff line change
@@ -80,24 +80,12 @@ func (h *Handlers) HandlePumpPub() turbostreams.HandlerFunc {
8080
t := pumpPartial
8181
h.r.MustHave(t)
8282
return func(c *turbostreams.Context) error {
83-
// Parse params
84-
id, err := parseID(c.Param("id"), "instrument")
85-
if err != nil {
86-
return err
87-
}
88-
controllerID, err := parseID(c.Param("controllerID"), "controller")
83+
// Parse params & run queries
84+
instrumentID, controllerID, pc, err := getPlanktoscopeClientForPub(c, h.pco)
8985
if err != nil {
9086
return err
9187
}
9288

93-
// Run queries
94-
pc, ok := h.pco.Get(controllerID)
95-
if !ok {
96-
return errors.Errorf(
97-
"planktoscope client for controller %d on instrument %d not found", controllerID, id,
98-
)
99-
}
100-
10189
// Publish on MQTT update
10290
for {
10391
ctx := c.Context()
@@ -111,7 +99,7 @@ func (h *Handlers) HandlePumpPub() turbostreams.HandlerFunc {
11199
}
112100
// We insert an empty Auth object because the MSG handler will add the auth object for each
113101
// client
114-
message := replacePumpStream(id, controllerID, auth.Auth{}, pc)
102+
message := replacePumpStream(instrumentID, controllerID, auth.Auth{}, pc)
115103
c.Publish(message)
116104
}
117105
}
@@ -122,10 +110,6 @@ type PlanktoscopePumpViewAuthz struct {
122110
Set bool
123111
}
124112

125-
type PlanktoscopeControllerViewAuthz struct {
126-
Pump PlanktoscopePumpViewAuthz
127-
}
128-
129113
func getPlanktoscopePumpViewAuthz(
130114
ctx context.Context, id, controllerID int64, a auth.Auth, azc *auth.AuthzChecker,
131115
) (authz PlanktoscopePumpViewAuthz, err error) {
@@ -136,45 +120,13 @@ func getPlanktoscopePumpViewAuthz(
136120
return authz, nil
137121
}
138122

139-
func getPlanktoscopeControllerViewAuthz(
140-
ctx context.Context, id, controllerID int64, a auth.Auth, azc *auth.AuthzChecker,
141-
) (authz PlanktoscopeControllerViewAuthz, err error) {
142-
if authz.Pump, err = getPlanktoscopePumpViewAuthz(ctx, id, controllerID, a, azc); err != nil {
143-
return PlanktoscopeControllerViewAuthz{}, errors.Wrap(err, "couldn't check authz for pump")
144-
}
145-
return authz, nil
146-
}
147-
148123
func (h *Handlers) ModifyPumpMsgData() handling.DataModifier {
149124
return func(
150125
ctx context.Context, a auth.Auth, data map[string]interface{},
151126
) (modifications map[string]interface{}, err error) {
152-
instrumentID, ok := data["InstrumentID"]
153-
if !ok {
154-
return nil, errors.New(
155-
"couldn't find instrument id from turbostreams message data to check authorizations",
156-
)
157-
}
158-
id, ok := instrumentID.(int64)
159-
if !ok {
160-
return nil, errors.Errorf(
161-
"instrument id has unexpected type %T in turbostreams message data for checking authorization",
162-
instrumentID,
163-
)
164-
}
165-
controllerID, ok := data["ControllerID"]
166-
if !ok {
167-
return nil, errors.Errorf(
168-
"couldn't find controller id for instrument %d from turbostreams message data to check authorizations",
169-
id,
170-
)
171-
}
172-
cid, ok := controllerID.(int64)
173-
if !ok {
174-
return nil, errors.Errorf(
175-
"controller id has unexpected type %T in turbostreams message data for checking authorization",
176-
controllerID,
177-
)
127+
id, cid, err := getIDsForModificationMiddleware(data)
128+
if err != nil {
129+
return nil, err
178130
}
179131
modifications = make(map[string]interface{})
180132
if modifications["Authorizations"], err = getPlanktoscopePumpViewAuthz(
@@ -229,3 +181,258 @@ func (h *Handlers) HandlePumpPost() auth.HTTPHandlerFunc {
229181
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/instruments/%d", id))
230182
}
231183
}
184+
185+
// Camera
186+
187+
const cameraPartial = "instruments/planktoscope/camera.partial.tmpl"
188+
189+
func replaceCameraStream(
190+
id, controllerID int64, a auth.Auth, pc *planktoscope.Client,
191+
) turbostreams.Message {
192+
state := pc.GetState()
193+
return turbostreams.Message{
194+
Action: turbostreams.ActionReplace,
195+
Target: fmt.Sprintf("/instruments/%d/controllers/%d/camera", id, controllerID),
196+
Template: cameraPartial,
197+
Data: map[string]interface{}{
198+
"InstrumentID": id,
199+
"ControllerID": controllerID,
200+
"CameraSettings": state.CameraSettings,
201+
"Auth": a,
202+
},
203+
}
204+
}
205+
206+
func handleCameraSettings(
207+
isoRaw, shutterSpeedRaw,
208+
autoWhiteBalanceRaw, whiteBalanceRedGainRaw, whiteBalanceBlueGainRaw string,
209+
pc *planktoscope.Client,
210+
) (err error) {
211+
var token mqtt.Token
212+
// TODO: use echo's request binding functionality instead of strconv.ParseFloat
213+
// TODO: perform input validation and handle invalid inputs
214+
const uintBase = 10
215+
const uintWidth = 64
216+
iso, err := strconv.ParseUint(isoRaw, uintBase, uintWidth)
217+
if err != nil {
218+
return echo.NewHTTPError(http.StatusBadRequest, errors.Wrap(err, "couldn't parse iso"))
219+
}
220+
shutterSpeed, err := strconv.ParseUint(shutterSpeedRaw, uintBase, uintWidth)
221+
if err != nil {
222+
return echo.NewHTTPError(http.StatusBadRequest, errors.Wrap(err, "couldn't parse shutter speed"))
223+
}
224+
225+
const floatWidth = 64
226+
autoWhiteBalance := strings.ToLower(autoWhiteBalanceRaw) == "true"
227+
whiteBalanceRedGain, err := strconv.ParseFloat(whiteBalanceRedGainRaw, floatWidth)
228+
if err != nil {
229+
return echo.NewHTTPError(http.StatusBadRequest, errors.Wrap(
230+
err, "couldn't parse white balance red gain",
231+
))
232+
}
233+
whiteBalanceBlueGain, err := strconv.ParseFloat(whiteBalanceBlueGainRaw, floatWidth)
234+
if err != nil {
235+
return echo.NewHTTPError(http.StatusBadRequest, errors.Wrap(
236+
err, "couldn't parse white balance blue gain",
237+
))
238+
}
239+
240+
if token, err = pc.SetCamera(
241+
iso, shutterSpeed, autoWhiteBalance, whiteBalanceRedGain, whiteBalanceBlueGain,
242+
); err != nil {
243+
return err
244+
}
245+
246+
stateUpdated := pc.CameraStateBroadcasted()
247+
// TODO: instead of waiting forever, have a timeout before redirecting and displaying a
248+
// warning message that we haven't heard any camera settings updates from the planktoscope.
249+
if token.Wait(); token.Error() != nil {
250+
return token.Error()
251+
}
252+
<-stateUpdated
253+
return nil
254+
}
255+
256+
func (h *Handlers) HandleCameraPub() turbostreams.HandlerFunc {
257+
t := cameraPartial
258+
h.r.MustHave(t)
259+
return func(c *turbostreams.Context) error {
260+
// Parse params & run queries
261+
instrumentID, controllerID, pc, err := getPlanktoscopeClientForPub(c, h.pco)
262+
if err != nil {
263+
return err
264+
}
265+
266+
// Publish on MQTT update
267+
for {
268+
ctx := c.Context()
269+
select {
270+
case <-ctx.Done():
271+
return ctx.Err()
272+
case <-pc.CameraStateBroadcasted():
273+
if err := ctx.Err(); err != nil {
274+
// Context was also canceled and it should have priority
275+
return err
276+
}
277+
// We insert an empty Auth object because the MSG handler will add the auth object for each
278+
// client
279+
message := replaceCameraStream(instrumentID, controllerID, auth.Auth{}, pc)
280+
c.Publish(message)
281+
}
282+
}
283+
}
284+
}
285+
286+
type PlanktoscopeCameraViewAuthz struct {
287+
Set bool
288+
}
289+
290+
func getPlanktoscopeCameraViewAuthz(
291+
ctx context.Context, id, controllerID int64, a auth.Auth, azc *auth.AuthzChecker,
292+
) (authz PlanktoscopeCameraViewAuthz, err error) {
293+
path := fmt.Sprintf("/instruments/%d/controllers/%d/camera", id, controllerID)
294+
if authz.Set, err = azc.Allow(ctx, a, path, http.MethodPost, nil); err != nil {
295+
return PlanktoscopeCameraViewAuthz{}, errors.Wrap(
296+
err, "couldn't check authz for setting camera",
297+
)
298+
}
299+
return authz, nil
300+
}
301+
302+
func (h *Handlers) ModifyCameraMsgData() handling.DataModifier {
303+
return func(
304+
ctx context.Context, a auth.Auth, data map[string]interface{},
305+
) (modifications map[string]interface{}, err error) {
306+
id, cid, err := getIDsForModificationMiddleware(data)
307+
if err != nil {
308+
return nil, err
309+
}
310+
modifications = make(map[string]interface{})
311+
if modifications["Authorizations"], err = getPlanktoscopeCameraViewAuthz(
312+
ctx, id, cid, a, h.azc,
313+
); err != nil {
314+
return nil, errors.Wrapf(
315+
err, "couldn't check authz for camera of controller %d of instrument %d", cid, id,
316+
)
317+
}
318+
return modifications, nil
319+
}
320+
}
321+
322+
func (h *Handlers) HandleCameraPost() auth.HTTPHandlerFunc {
323+
t := cameraPartial
324+
h.r.MustHave(t)
325+
return func(c echo.Context, a auth.Auth) error {
326+
// Parse params
327+
id, err := parseID(c.Param("id"), "instrument")
328+
if err != nil {
329+
return err
330+
}
331+
controllerID, err := parseID(c.Param("controllerID"), "controller")
332+
if err != nil {
333+
return err
334+
}
335+
336+
// Run queries
337+
pc, ok := h.pco.Get(id)
338+
if !ok {
339+
return errors.Errorf(
340+
"planktoscope client for controller %d on instrument %d not found", id, controllerID,
341+
)
342+
}
343+
if err = handleCameraSettings(
344+
c.FormValue("iso"), c.FormValue("shutter-speed"),
345+
c.FormValue("awb"), c.FormValue("wb-red"), c.FormValue("wb-blue"), pc,
346+
); err != nil {
347+
return err
348+
}
349+
350+
// We rely on Turbo Streams over websockets, so we return an empty response here to avoid a race
351+
// condition of two Turbo Stream replace messages (where the one from this POST response could
352+
// be stale and overwrite a fresher message over websockets by arriving later).
353+
// FIXME: is there a cleaner way to avoid the race condition which would work even if the
354+
// WebSocket connection is misbehaving?
355+
if turbostreams.Accepted(c.Request().Header) {
356+
return h.r.TurboStream(c.Response())
357+
}
358+
359+
// Redirect user
360+
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/instruments/%d", id))
361+
}
362+
}
363+
364+
// Controller
365+
366+
type PlanktoscopeControllerViewAuthz struct {
367+
Pump PlanktoscopePumpViewAuthz
368+
Camera PlanktoscopeCameraViewAuthz
369+
}
370+
371+
func getPlanktoscopeControllerViewAuthz(
372+
ctx context.Context, id, controllerID int64, a auth.Auth, azc *auth.AuthzChecker,
373+
) (authz PlanktoscopeControllerViewAuthz, err error) {
374+
if authz.Pump, err = getPlanktoscopePumpViewAuthz(ctx, id, controllerID, a, azc); err != nil {
375+
return PlanktoscopeControllerViewAuthz{}, errors.Wrap(err, "couldn't check authz for pump")
376+
}
377+
if authz.Camera, err = getPlanktoscopeCameraViewAuthz(ctx, id, controllerID, a, azc); err != nil {
378+
return PlanktoscopeControllerViewAuthz{}, errors.Wrap(err, "couldn't check authz for camera")
379+
}
380+
return authz, nil
381+
}
382+
383+
func getIDsForModificationMiddleware(
384+
data map[string]interface{},
385+
) (instrumentID int64, controllerID int64, err error) {
386+
rawInstrumentID, ok := data["InstrumentID"]
387+
if !ok {
388+
return 0, 0, errors.New(
389+
"couldn't find instrument id from turbostreams message data to check authorizations",
390+
)
391+
}
392+
instrumentID, ok = rawInstrumentID.(int64)
393+
if !ok {
394+
return 0, 0, errors.Errorf(
395+
"instrument id has unexpected type %T in turbostreams message data for checking authorization",
396+
rawInstrumentID,
397+
)
398+
}
399+
rawControllerID, ok := data["ControllerID"]
400+
if !ok {
401+
return 0, 0, errors.Errorf(
402+
"couldn't find controller id for instrument %d from turbostreams message data to check authorizations",
403+
instrumentID,
404+
)
405+
}
406+
controllerID, ok = rawControllerID.(int64)
407+
if !ok {
408+
return 0, 0, errors.Errorf(
409+
"controller id has unexpected type %T in turbostreams message data for checking authorization",
410+
rawControllerID,
411+
)
412+
}
413+
return instrumentID, controllerID, nil
414+
}
415+
416+
func getPlanktoscopeClientForPub(
417+
c *turbostreams.Context, pco *planktoscope.Orchestrator,
418+
) (instrumentID int64, controllerID int64, client *planktoscope.Client, err error) {
419+
// Parse params
420+
instrumentID, err = parseID(c.Param("id"), "instrument")
421+
if err != nil {
422+
return 0, 0, nil, err
423+
}
424+
controllerID, err = parseID(c.Param("controllerID"), "controller")
425+
if err != nil {
426+
return 0, 0, nil, err
427+
}
428+
429+
// Run queries
430+
pc, ok := pco.Get(controllerID)
431+
if !ok {
432+
return 0, 0, nil, errors.Errorf(
433+
"planktoscope client for controller %d on instrument %d not found",
434+
controllerID, instrumentID,
435+
)
436+
}
437+
return instrumentID, controllerID, pc, nil
438+
}

internal/app/pslive/routes/instruments/routes.go

+6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ func (h *Handlers) Register(
7979
h.r, ss, h.ModifyPumpMsgData(),
8080
))
8181
hr.POST("/instruments/:id/controllers/:controllerID/pump", h.HandlePumpPost())
82+
tsr.SUB("/instruments/:id/controllers/:controllerID/camera", turbostreams.EmptyHandler)
83+
tsr.PUB("/instruments/:id/controllers/:controllerID/camera", h.HandleCameraPub())
84+
tsr.MSG("/instruments/:id/controllers/:controllerID/camera", handling.HandleTSMsg(
85+
h.r, ss, h.ModifyCameraMsgData(),
86+
))
87+
hr.POST("/instruments/:id/controllers/:controllerID/camera", h.HandleCameraPost())
8288
tsr.SUB("/instruments/:id/chat/messages", turbostreams.EmptyHandler)
8389
tsr.MSG("/instruments/:id/chat/messages", handling.HandleTSMsg(h.r, ss))
8490
// TODO: add a paginated GET handler for chat messages to support chat history infiniscroll

0 commit comments

Comments
 (0)