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

Commit 0dc500e

Browse files
authored
Expose interfaces for stop-flow imaging (#272)
* Add a basic GUI panel for stop-flow imaging data acquisition * Resolve linting problems * Enable automated starting of stop-flow imaging data acquisition * Upgrade dependencies
1 parent e059001 commit 0dc500e

File tree

22 files changed

+991
-226
lines changed

22 files changed

+991
-226
lines changed

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
path: dist
4848

4949
- name: Upload coverage to Codecov
50-
uses: codecov/[email protected].1
50+
uses: codecov/[email protected].2
5151
with:
5252
file: ./coverage.out
5353
flags: ${{ runner.os }}

go.mod

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/benbjohnson/hashfs v0.2.1
99
github.com/dgraph-io/ristretto v0.1.1
1010
github.com/eclipse/paho.mqtt.golang v1.4.2
11-
github.com/go-co-op/gocron v1.19.0
11+
github.com/go-co-op/gocron v1.22.2
1212
github.com/gorilla/csrf v1.7.1
1313
github.com/gorilla/sessions v1.2.1
1414
github.com/gorilla/websocket v1.5.0
@@ -21,7 +21,7 @@ require (
2121
github.com/pkg/errors v0.9.1
2222
github.com/sargassum-world/godest v0.5.1
2323
github.com/unrolled/secure v1.13.0
24-
golang.org/x/image v0.6.0
24+
golang.org/x/image v0.7.0
2525
golang.org/x/sync v0.1.0
2626
zombiezen.com/go/sqlite v0.13.0
2727
)
@@ -72,7 +72,7 @@ require (
7272
golang.org/x/net v0.8.0 // indirect
7373
golang.org/x/oauth2 v0.4.0 // indirect
7474
golang.org/x/sys v0.6.0 // indirect
75-
golang.org/x/text v0.8.0 // indirect
75+
golang.org/x/text v0.9.0 // indirect
7676
golang.org/x/time v0.3.0 // indirect
7777
google.golang.org/appengine v1.6.7 // indirect
7878
google.golang.org/protobuf v1.28.1 // indirect

go.sum

+6-7
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6
9393
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
9494
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
9595
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
96-
github.com/go-co-op/gocron v1.19.0 h1:XlPLqNnxnKblmCRLdfcWV1UgbukQaU54QdNeR1jtgak=
97-
github.com/go-co-op/gocron v1.19.0/go.mod h1:UqVyvM90I1q/R1qGEX6cBORI6WArLuEgYlbncLMvzRM=
96+
github.com/go-co-op/gocron v1.22.2 h1:5+486wUbSp2Tgodv3Fwek0OgMK/aqjcgGBcRTcT2kgs=
97+
github.com/go-co-op/gocron v1.22.2/go.mod h1:UqVyvM90I1q/R1qGEX6cBORI6WArLuEgYlbncLMvzRM=
9898
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
9999
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
100100
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -289,7 +289,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
289289
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
290290
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
291291
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
292-
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
293292
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
294293
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
295294
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -312,8 +311,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
312311
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
313312
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
314313
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
315-
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
316-
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
314+
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
315+
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
317316
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
318317
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
319318
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -440,8 +439,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
440439
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
441440
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
442441
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
443-
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
444-
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
442+
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
443+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
445444
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
446445
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
447446
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

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

+201
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"strconv"
88
"strings"
9+
"time"
910

1011
"github.com/eclipse/paho.mqtt.golang"
1112
"github.com/labstack/echo/v4"
@@ -36,6 +37,7 @@ func replacePumpStream(
3637
"ControllerID": cid,
3738
"PumpSettings": state.PumpSettings,
3839
"Pump": state.Pump,
40+
"Imaging": state.Imager.Imaging,
3941
"Auth": a,
4042
},
4143
}
@@ -367,11 +369,205 @@ func (h *Handlers) HandleCameraPost() auth.HTTPHandlerFunc {
367369
}
368370
}
369371

372+
// Imager
373+
374+
const imagerPartial = "instruments/planktoscope/imager.partial.tmpl"
375+
376+
func replaceImagerStream(
377+
iid instruments.InstrumentID, cid instruments.ControllerID, a auth.Auth,
378+
pc *planktoscope.Client,
379+
) turbostreams.Message {
380+
state := pc.GetState()
381+
return turbostreams.Message{
382+
Action: turbostreams.ActionReplace,
383+
Target: fmt.Sprintf("/instruments/%d/controllers/%d/imager", iid, cid),
384+
Template: imagerPartial,
385+
Data: map[string]interface{}{
386+
"InstrumentID": iid,
387+
"ControllerID": cid,
388+
"ImagerSettings": state.ImagerSettings,
389+
"Imager": state.Imager,
390+
"Auth": a,
391+
},
392+
}
393+
}
394+
395+
func handleImagerSettings(
396+
sampleID, imagingRaw, direction, stepVolumeRaw, stepDelayRaw, stepsRaw string,
397+
pc *planktoscope.Client,
398+
) (err error) {
399+
imaging := strings.ToLower(imagingRaw) == "start"
400+
var token mqtt.Token
401+
if !imaging {
402+
if token, err = pc.StopImaging(); err != nil {
403+
return err
404+
}
405+
} else {
406+
// TODO: use echo's request binding functionality instead of strconv.ParseFloat
407+
// TODO: perform input validation and handle invalid inputs
408+
forward := strings.ToLower(direction) == "forward"
409+
const floatWidth = 64
410+
stepVolume, err := strconv.ParseFloat(stepVolumeRaw, floatWidth)
411+
if err != nil {
412+
return echo.NewHTTPError(http.StatusBadRequest, errors.Wrap(
413+
err, "couldn't parse step volume",
414+
))
415+
}
416+
stepDelay, err := strconv.ParseFloat(stepDelayRaw, floatWidth)
417+
if err != nil {
418+
return echo.NewHTTPError(http.StatusBadRequest, errors.Wrap(
419+
err, "couldn't parse step delay",
420+
))
421+
}
422+
const base = 10
423+
const bitSize = 64
424+
steps, err := strconv.ParseUint(stepsRaw, base, bitSize)
425+
if err != nil {
426+
return echo.NewHTTPError(http.StatusBadRequest, errors.Wrap(err, "couldn't parse steps"))
427+
}
428+
if token, err = pc.SetMetadata(sampleID, time.Now()); err != nil {
429+
return err
430+
}
431+
// TODO: instead of waiting forever, have a timeout before redirecting and displaying a
432+
// warning message that we couldn't update the imaging metadata
433+
if token.Wait(); token.Error() != nil {
434+
return token.Error()
435+
}
436+
if token, err = pc.StartImaging(forward, stepVolume, stepDelay, steps); err != nil {
437+
return err
438+
}
439+
}
440+
441+
stateUpdated := pc.ImagerStateBroadcasted()
442+
// TODO: instead of waiting forever, have a timeout before redirecting and displaying a
443+
// warning message that we haven't heard any imager state updates from the planktoscope.
444+
if token.Wait(); token.Error() != nil {
445+
return token.Error()
446+
}
447+
<-stateUpdated
448+
return nil
449+
}
450+
451+
func (h *Handlers) HandleImagerPub() turbostreams.HandlerFunc {
452+
t := imagerPartial
453+
h.r.MustHave(t)
454+
return func(c *turbostreams.Context) error {
455+
// Parse params & run queries
456+
iid, cid, pc, err := getPlanktoscopeClientForPub(c, h.pco)
457+
if err != nil {
458+
return err
459+
}
460+
461+
// Publish on MQTT update
462+
for {
463+
ctx := c.Context()
464+
select {
465+
case <-ctx.Done():
466+
return ctx.Err()
467+
case <-pc.ImagerStateBroadcasted():
468+
if err := ctx.Err(); err != nil {
469+
// Context was also canceled and it should have priority
470+
return err
471+
}
472+
// We insert an empty Auth object because the MSG handler will add the auth object for each
473+
// client
474+
message := replaceImagerStream(iid, cid, auth.Auth{}, pc)
475+
c.Publish(message)
476+
}
477+
}
478+
}
479+
}
480+
481+
type PlanktoscopeImagerViewAuthz struct {
482+
Set bool
483+
}
484+
485+
func getPlanktoscopeImagerViewAuthz(
486+
ctx context.Context, iid instruments.InstrumentID, cid instruments.ControllerID,
487+
a auth.Auth, azc *auth.AuthzChecker,
488+
) (authz PlanktoscopeImagerViewAuthz, err error) {
489+
path := fmt.Sprintf("/instruments/%d/controllers/%d/imager", iid, cid)
490+
if authz.Set, err = azc.Allow(ctx, a, path, http.MethodPost, nil); err != nil {
491+
return PlanktoscopeImagerViewAuthz{}, errors.Wrap(
492+
err, "couldn't check authz for setting imager",
493+
)
494+
}
495+
return authz, nil
496+
}
497+
498+
func (h *Handlers) ModifyImagerMsgData() handling.DataModifier {
499+
return func(
500+
ctx context.Context, a auth.Auth, data map[string]interface{},
501+
) (modifications map[string]interface{}, err error) {
502+
iid, cid, err := getIDsForModificationMiddleware(data)
503+
if err != nil {
504+
return nil, err
505+
}
506+
modifications = make(map[string]interface{})
507+
if modifications["Authorizations"], err = getPlanktoscopeImagerViewAuthz(
508+
ctx, iid, cid, a, h.azc,
509+
); err != nil {
510+
return nil, errors.Wrapf(
511+
err, "couldn't check authz for imager of controller %d of instrument %d", cid, iid,
512+
)
513+
}
514+
return modifications, nil
515+
}
516+
}
517+
518+
func (h *Handlers) HandleImagerPost() auth.HTTPHandlerFunc {
519+
t := imagerPartial
520+
h.r.MustHave(t)
521+
return func(c echo.Context, a auth.Auth) error {
522+
// Parse params
523+
iid, err := parseID[instruments.InstrumentID](c.Param("id"), "instrument")
524+
if err != nil {
525+
return err
526+
}
527+
cid, err := parseID[instruments.ControllerID](c.Param("controllerID"), "controller")
528+
if err != nil {
529+
return err
530+
}
531+
532+
// Run queries
533+
instrument, err := h.is.GetInstrument(c.Request().Context(), iid)
534+
if err != nil {
535+
return err
536+
}
537+
pc, ok := h.pco.Get(planktoscope.ClientID(cid))
538+
if !ok {
539+
return errors.Errorf(
540+
"planktoscope client for controller %d on instrument %d not found for imager post",
541+
cid, iid,
542+
)
543+
}
544+
if err = handleImagerSettings(
545+
instrument.Name, c.FormValue("imaging"), c.FormValue("direction"),
546+
c.FormValue("step-volume"), c.FormValue("step-delay"), c.FormValue("steps"), pc,
547+
); err != nil {
548+
return err
549+
}
550+
551+
// We rely on Turbo Streams over websockets, so we return an empty response here to avoid a race
552+
// condition of two Turbo Stream replace messages (where the one from this POST response could
553+
// be stale and overwrite a fresher message over websockets by arriving later).
554+
// FIXME: is there a cleaner way to avoid the race condition which would work even if the
555+
// WebSocket connection is misbehaving?
556+
if turbostreams.Accepted(c.Request().Header) {
557+
return h.r.TurboStream(c.Response())
558+
}
559+
560+
// Redirect user
561+
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/instruments/%d", iid))
562+
}
563+
}
564+
370565
// Controller
371566

372567
type PlanktoscopeControllerViewAuthz struct {
373568
Pump PlanktoscopePumpViewAuthz
374569
Camera PlanktoscopeCameraViewAuthz
570+
Imager PlanktoscopeImagerViewAuthz
375571
}
376572

377573
func getPlanktoscopeControllerViewAuthz(
@@ -388,6 +584,11 @@ func getPlanktoscopeControllerViewAuthz(
388584
); err != nil {
389585
return PlanktoscopeControllerViewAuthz{}, errors.Wrap(err, "couldn't check authz for camera")
390586
}
587+
if authz.Imager, err = getPlanktoscopeImagerViewAuthz(
588+
ctx, iid, cid, a, azc,
589+
); err != nil {
590+
return PlanktoscopeControllerViewAuthz{}, errors.Wrap(err, "couldn't check authz for imager")
591+
}
391592
return authz, nil
392593
}
393594

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

+6
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ func (h *Handlers) Register(
8787
h.r, ss, h.ModifyCameraMsgData(),
8888
))
8989
hr.POST("/instruments/:id/controllers/:controllerID/camera", h.HandleCameraPost())
90+
tsr.SUB("/instruments/:id/controllers/:controllerID/imager", turbostreams.EmptyHandler)
91+
tsr.PUB("/instruments/:id/controllers/:controllerID/imager", h.HandleImagerPub())
92+
tsr.MSG("/instruments/:id/controllers/:controllerID/imager", handling.HandleTSMsg(
93+
h.r, ss, h.ModifyImagerMsgData(),
94+
))
95+
hr.POST("/instruments/:id/controllers/:controllerID/imager", h.HandleImagerPost())
9096
hr.POST("/instruments/:id/automation-jobs", h.HandleInstrumentAutomationJobsPost())
9197
hr.POST(
9298
"/instruments/:id/automation-jobs/:automationJobID", h.HandleInstrumentAutomationJobPost(),

0 commit comments

Comments
 (0)