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

Commit 0d31d87

Browse files
authored
Add toggles to disable instrument camera/controller (#219)
* Add "enabled" flag to set whether instrument camera/controller is shown * Don't log an error when MJPEG stream receiving is context-canceled * Address linter complaint * Show an error message frame when a stream can't be loaded
1 parent d792182 commit 0d31d87

25 files changed

+300
-147
lines changed

db/embeds.go

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var MigrationFiles []database.MigrationFile = []database.MigrationFile{
3131
{Domain: "instruments", File: instruments.MigrationFiles[1]},
3232
{Domain: "instruments", File: instruments.MigrationFiles[2]},
3333
{Domain: "sessions", File: sessions.MigrationFiles[0]},
34+
{Domain: "instruments", File: instruments.MigrationFiles[3]},
3435
}
3536

3637
// Queries

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ func newErrorJPEG(width, height int, message string) []byte {
7777
return frame.Im
7878
}
7979

80-
var jpegError = newErrorJPEG(errorWidth, errorHeight, "stream failed")
80+
var (
81+
frameError = newErrorFrame(errorWidth, errorHeight, "stream failed")
82+
jpegError = newErrorJPEG(errorWidth, errorHeight, "stream failed")
83+
)
8184

8285
// Sending helpers
8386

@@ -129,11 +132,14 @@ func externalSourceFrameSender(
129132
func (h *Handlers) HandleInstrumentCameraPost() auth.HTTPHandlerFunc {
130133
return handleInstrumentComponentPost(
131134
"camera",
132-
func(ctx context.Context, componentID instruments.CameraID, url, protocol string) error {
135+
func(
136+
ctx context.Context, componentID instruments.CameraID, url, protocol string, enabled bool,
137+
) error {
133138
return h.is.UpdateCamera(ctx, instruments.Camera{
134139
ID: componentID,
135140
URL: url,
136141
Protocol: protocol,
142+
Enabled: enabled,
137143
})
138144
},
139145
h.is.DeleteCamera,
@@ -203,7 +209,7 @@ func (h *Handlers) HandleInstrumentCameraStreamGet() echo.HandlerFunc {
203209
if err != nil {
204210
return err
205211
}
206-
annotated := c.QueryParam("annotated") == "true"
212+
annotated := c.QueryParam("annotated") == flagChecked
207213
// TODO: implement a max framerate
208214

209215
// Run queries
@@ -292,6 +298,7 @@ func (h *Handlers) HandleInstrumentCameraStreamPub() videostreams.HandlerFunc {
292298
}),
293299
context.Canceled,
294300
); err != nil {
301+
c.Publish(frameError)
295302
c.Logger().Error(errors.Wrapf(err, "failed to proxy stream %s", sourceURL))
296303
}
297304
return nil

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import (
99

1010
func (h *Handlers) HandleInstrumentCamerasPost() auth.HTTPHandlerFunc {
1111
return handleInstrumentComponentsPost(
12-
func(ctx context.Context, id instruments.InstrumentID, url, protocol string) error {
12+
func(
13+
ctx context.Context, id instruments.InstrumentID, url, protocol string, enabled bool,
14+
) error {
1315
_, err := h.is.AddCamera(ctx, instruments.Camera{
1416
InstrumentID: id,
1517
URL: url,
1618
Protocol: protocol,
19+
Enabled: enabled,
1720
})
1821
return err
1922
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package instruments
2+
3+
import (
4+
"context"
5+
6+
"github.com/sargassum-world/pslive/internal/app/pslive/auth"
7+
"github.com/sargassum-world/pslive/internal/clients/instruments"
8+
"github.com/sargassum-world/pslive/internal/clients/planktoscope"
9+
)
10+
11+
func (h *Handlers) HandleInstrumentControllerPost() auth.HTTPHandlerFunc {
12+
return handleInstrumentComponentPost(
13+
"controller",
14+
func(
15+
ctx context.Context, controllerID instruments.ControllerID, url, protocol string, enabled bool,
16+
) error {
17+
if err := h.is.UpdateController(ctx, instruments.Controller{
18+
ID: controllerID,
19+
URL: url,
20+
Protocol: protocol,
21+
Enabled: enabled,
22+
}); err != nil {
23+
return err
24+
}
25+
// Note: when we have other controllers, we'll need to generalize this
26+
if !enabled {
27+
return h.pco.Remove(ctx, planktoscope.ClientID(controllerID))
28+
}
29+
return h.pco.Update(ctx, planktoscope.ClientID(controllerID), url)
30+
},
31+
func(ctx context.Context, controllerID instruments.ControllerID) error {
32+
if err := h.is.DeleteController(ctx, controllerID); err != nil {
33+
return err
34+
}
35+
if err := h.pco.Remove(ctx, planktoscope.ClientID(controllerID)); err != nil {
36+
return err
37+
}
38+
return nil
39+
},
40+
)
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package instruments
2+
3+
import (
4+
"context"
5+
6+
"github.com/sargassum-world/pslive/internal/app/pslive/auth"
7+
"github.com/sargassum-world/pslive/internal/clients/instruments"
8+
"github.com/sargassum-world/pslive/internal/clients/planktoscope"
9+
)
10+
11+
func (h *Handlers) HandleInstrumentControllersPost() auth.HTTPHandlerFunc {
12+
return handleInstrumentComponentsPost(
13+
func(
14+
ctx context.Context, iid instruments.InstrumentID, url, protocol string, enabled bool,
15+
) error {
16+
controllerID, err := h.is.AddController(ctx, instruments.Controller{
17+
InstrumentID: iid,
18+
URL: url,
19+
Protocol: protocol,
20+
Enabled: enabled,
21+
})
22+
if err != nil {
23+
return err
24+
}
25+
if !enabled {
26+
return nil
27+
}
28+
return h.pco.Add(planktoscope.ClientID(controllerID), url)
29+
},
30+
)
31+
}

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

+85-123
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/http"
77
"strconv"
8+
"strings"
89

910
"github.com/labstack/echo/v4"
1011
"github.com/pkg/errors"
@@ -19,6 +20,8 @@ import (
1920
"github.com/sargassum-world/pslive/internal/clients/presence"
2021
)
2122

23+
const flagChecked = "true"
24+
2225
func parseID[ID ~int64](raw string, typeName string) (ID, error) {
2326
const intBase = 10
2427
const intWidth = 64
@@ -29,6 +32,85 @@ func parseID[ID ~int64](raw string, typeName string) (ID, error) {
2932
return ID(id), err
3033
}
3134

35+
// Components (common across cameras & controllers)
36+
37+
func handleInstrumentComponentsPost(
38+
storeAdder func(
39+
ctx context.Context, iid instruments.InstrumentID, url, protocol string, enabled bool,
40+
) error,
41+
) auth.HTTPHandlerFunc {
42+
return func(c echo.Context, a auth.Auth) error {
43+
// Parse params
44+
iid, err := parseID[instruments.InstrumentID](c.Param("id"), "instrument")
45+
if err != nil {
46+
return err
47+
}
48+
url := c.FormValue("url")
49+
protocol := c.FormValue("protocol")
50+
enabled := strings.ToLower(c.FormValue("enabled")) == flagChecked
51+
52+
// Run queries
53+
// FIXME: there needs to be an authorization check to ensure that the user attempting to
54+
// delete the instrument is an administrator of the instrument!
55+
if err := storeAdder(c.Request().Context(), iid, url, protocol, enabled); err != nil {
56+
return err
57+
}
58+
59+
// TODO: return turbo stream, broadcast updates
60+
61+
// Redirect user
62+
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/instruments/%d", iid))
63+
}
64+
}
65+
66+
func handleInstrumentComponentPost[ComponentID ~int64](
67+
typeName string,
68+
componentUpdater func(
69+
ctx context.Context, componentID ComponentID, url, protocol string, enabled bool,
70+
) error,
71+
componentDeleter func(ctx context.Context, componentID ComponentID) error,
72+
) auth.HTTPHandlerFunc {
73+
return func(c echo.Context, a auth.Auth) error {
74+
// Parse params
75+
iid, err := parseID[instruments.InstrumentID](c.Param("id"), "instrument")
76+
if err != nil {
77+
return err
78+
}
79+
componentID, err := parseID[ComponentID](c.Param(typeName+"ID"), typeName)
80+
if err != nil {
81+
return err
82+
}
83+
state := c.FormValue("state")
84+
85+
// Run queries
86+
ctx := c.Request().Context()
87+
switch state {
88+
default:
89+
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf(
90+
"invalid %s state %s", typeName, state,
91+
))
92+
case "updated":
93+
protocol := c.FormValue("protocol")
94+
url := c.FormValue("url")
95+
enabled := strings.ToLower(c.FormValue("enabled")) == flagChecked
96+
// FIXME: needs authorization check!
97+
if err = componentUpdater(ctx, componentID, url, protocol, enabled); err != nil {
98+
return err
99+
}
100+
// TODO: deal with turbo streams
101+
case "deleted":
102+
// FIXME: needs authorization check!
103+
if err = componentDeleter(ctx, componentID); err != nil {
104+
return err
105+
}
106+
// TODO: deal with turbo streams
107+
}
108+
109+
// Redirect user
110+
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/instruments/%d", iid))
111+
}
112+
}
113+
32114
// Instrument
33115

34116
type InstrumentViewData struct {
@@ -56,6 +138,9 @@ func getInstrumentViewData(
56138
vd.ControllerIDs = make([]instruments.ControllerID, 0, len(vd.Instrument.Controllers))
57139
vd.Controllers = make(map[instruments.ControllerID]planktoscope.Planktoscope)
58140
for _, controller := range vd.Instrument.Controllers {
141+
if !controller.Enabled {
142+
continue
143+
}
59144
pc, ok := pco.Get(planktoscope.ClientID(controller.ID))
60145
if !ok {
61146
return InstrumentViewData{}, errors.Errorf(
@@ -249,126 +334,3 @@ func (h *Handlers) HandleInstrumentDescriptionPost() auth.HTTPHandlerFunc {
249334
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/instruments/%d", iid))
250335
}
251336
}
252-
253-
// Components
254-
255-
func handleInstrumentComponentsPost(
256-
storeAdder func(ctx context.Context, iid instruments.InstrumentID, url, protocol string) error,
257-
) auth.HTTPHandlerFunc {
258-
return func(c echo.Context, a auth.Auth) error {
259-
// Parse params
260-
iid, err := parseID[instruments.InstrumentID](c.Param("id"), "instrument")
261-
if err != nil {
262-
return err
263-
}
264-
url := c.FormValue("url")
265-
protocol := c.FormValue("protocol")
266-
267-
// Run queries
268-
// FIXME: there needs to be an authorization check to ensure that the user attempting to
269-
// delete the instrument is an administrator of the instrument!
270-
if err := storeAdder(c.Request().Context(), iid, url, protocol); err != nil {
271-
return err
272-
}
273-
274-
// TODO: return turbo stream, broadcast updates
275-
276-
// Redirect user
277-
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/instruments/%d", iid))
278-
}
279-
}
280-
281-
func handleInstrumentComponentPost[ComponentID ~int64](
282-
typeName string,
283-
componentUpdater func(ctx context.Context, componentID ComponentID, url, protocol string) error,
284-
componentDeleter func(ctx context.Context, componentID ComponentID) error,
285-
) auth.HTTPHandlerFunc {
286-
return func(c echo.Context, a auth.Auth) error {
287-
// Parse params
288-
iid, err := parseID[instruments.InstrumentID](c.Param("id"), "instrument")
289-
if err != nil {
290-
return err
291-
}
292-
componentID, err := parseID[ComponentID](c.Param(typeName+"ID"), typeName)
293-
if err != nil {
294-
return err
295-
}
296-
state := c.FormValue("state")
297-
298-
// Run queries
299-
ctx := c.Request().Context()
300-
switch state {
301-
default:
302-
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf(
303-
"invalid %s state %s", typeName, state,
304-
))
305-
case "updated":
306-
protocol := c.FormValue("protocol")
307-
url := c.FormValue("url")
308-
// FIXME: needs authorization check!
309-
if err = componentUpdater(ctx, componentID, url, protocol); err != nil {
310-
return err
311-
}
312-
// TODO: deal with turbo streams
313-
case "deleted":
314-
// FIXME: needs authorization check!
315-
if err = componentDeleter(ctx, componentID); err != nil {
316-
return err
317-
}
318-
// TODO: deal with turbo streams
319-
}
320-
321-
// Redirect user
322-
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/instruments/%d", iid))
323-
}
324-
}
325-
326-
// Controllers
327-
328-
func (h *Handlers) HandleInstrumentControllersPost() auth.HTTPHandlerFunc {
329-
return handleInstrumentComponentsPost(
330-
func(ctx context.Context, iid instruments.InstrumentID, url, protocol string) error {
331-
controllerID, err := h.is.AddController(ctx, instruments.Controller{
332-
InstrumentID: iid,
333-
URL: url,
334-
Protocol: protocol,
335-
})
336-
if err != nil {
337-
return err
338-
}
339-
if err := h.pco.Add(planktoscope.ClientID(controllerID), url); err != nil {
340-
return err
341-
}
342-
return nil
343-
},
344-
)
345-
}
346-
347-
func (h *Handlers) HandleInstrumentControllerPost() auth.HTTPHandlerFunc {
348-
return handleInstrumentComponentPost(
349-
"controller",
350-
func(ctx context.Context, controllerID instruments.ControllerID, url, protocol string) error {
351-
if err := h.is.UpdateController(ctx, instruments.Controller{
352-
ID: controllerID,
353-
URL: url,
354-
Protocol: protocol,
355-
}); err != nil {
356-
return err
357-
}
358-
// Note: when we have other controllers, we'll need to generalize this
359-
if err := h.pco.Update(ctx, planktoscope.ClientID(controllerID), url); err != nil {
360-
return err
361-
}
362-
return nil
363-
},
364-
func(ctx context.Context, controllerID instruments.ControllerID) error {
365-
if err := h.is.DeleteController(ctx, controllerID); err != nil {
366-
return err
367-
}
368-
if err := h.pco.Remove(ctx, planktoscope.ClientID(controllerID)); err != nil {
369-
return err
370-
}
371-
return nil
372-
},
373-
)
374-
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func handleCameraSettings(
227227
}
228228

229229
const floatWidth = 64
230-
autoWhiteBalance := strings.ToLower(autoWhiteBalanceRaw) == "true"
230+
autoWhiteBalance := strings.ToLower(autoWhiteBalanceRaw) == flagChecked
231231
whiteBalanceRedGain, err := strconv.ParseFloat(whiteBalanceRedGainRaw, floatWidth)
232232
if err != nil {
233233
return echo.NewHTTPError(http.StatusBadRequest, errors.Wrap(

0 commit comments

Comments
 (0)