6
6
"net/http"
7
7
"strconv"
8
8
"strings"
9
+ "time"
9
10
10
11
"github.com/eclipse/paho.mqtt.golang"
11
12
"github.com/labstack/echo/v4"
@@ -36,6 +37,7 @@ func replacePumpStream(
36
37
"ControllerID" : cid ,
37
38
"PumpSettings" : state .PumpSettings ,
38
39
"Pump" : state .Pump ,
40
+ "Imaging" : state .Imager .Imaging ,
39
41
"Auth" : a ,
40
42
},
41
43
}
@@ -367,11 +369,205 @@ func (h *Handlers) HandleCameraPost() auth.HTTPHandlerFunc {
367
369
}
368
370
}
369
371
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
+
370
565
// Controller
371
566
372
567
type PlanktoscopeControllerViewAuthz struct {
373
568
Pump PlanktoscopePumpViewAuthz
374
569
Camera PlanktoscopeCameraViewAuthz
570
+ Imager PlanktoscopeImagerViewAuthz
375
571
}
376
572
377
573
func getPlanktoscopeControllerViewAuthz (
@@ -388,6 +584,11 @@ func getPlanktoscopeControllerViewAuthz(
388
584
); err != nil {
389
585
return PlanktoscopeControllerViewAuthz {}, errors .Wrap (err , "couldn't check authz for camera" )
390
586
}
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
+ }
391
592
return authz , nil
392
593
}
393
594
0 commit comments