Skip to content

Commit 4f92f47

Browse files
authored
Merge pull request #7 from carverauto/updates/axis
Updates/axis
2 parents 720eda9 + 2f1435c commit 4f92f47

21 files changed

Lines changed: 1765 additions & 38 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# If you prefer the allow list template instead of the deny list, see community template:
22
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
33
#
4+
5+
.DS_Store
6+
47
# Binaries for programs and plugins
58
*.exe
69
*.exe~

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Context variants exist for host I/O to match Go expectations:
120120
- HTTP: `HTTP.DoContext`, `HTTP.GetContext`, `HTTP.PostContext`
121121
- TCP: `TCPDialContext`, `(*TCPConn).ReadContext`, `(*TCPConn).WriteContext`
122122
- UDP: `UDPSendToContext`
123-
- WebSocket: `WebSocketDialContext`, `(*WebSocketConn).ReadContext`, `(*WebSocketConn).WriteContext`
123+
- WebSocket: `WebSocketDialContext`, `(*WebSocketConn).SendContext`, `(*WebSocketConn).RecvContext`
124124

125125
These currently check `ctx.Err()` before the host call (TinyGo/Wasm is synchronous), but give you a stable API if cancellation support is added later.
126126

@@ -129,22 +129,24 @@ The SDK provides WebSocket client capabilities for plugins that need to communic
129129

130130
```go
131131
// Dial a WebSocket endpoint
132-
conn, err := sdk.WebSocketDialContext(ctx, "ws://localhost:8080/ws")
132+
conn, err := sdk.WebSocketDialContext(ctx, "ws://localhost:8080/ws", 10*time.Second)
133133
if err != nil {
134134
return nil, fmt.Errorf("websocket dial failed: %w", err)
135135
}
136136
defer conn.Close()
137137

138138
// Send a message
139-
if err := conn.WriteContext(ctx, []byte(`{"method": "getInfo"}`)); err != nil {
140-
return nil, fmt.Errorf("websocket write failed: %w", err)
139+
if err := conn.SendContext(ctx, []byte(`{"method": "getInfo"}`), 10*time.Second); err != nil {
140+
return nil, fmt.Errorf("websocket send failed: %w", err)
141141
}
142142

143143
// Read response
144-
data, err := conn.ReadContext(ctx)
144+
buf := make([]byte, 4096)
145+
n, err := conn.RecvContext(ctx, buf, 10*time.Second)
145146
if err != nil {
146-
return nil, fmt.Errorf("websocket read failed: %w", err)
147+
return nil, fmt.Errorf("websocket recv failed: %w", err)
147148
}
149+
data := buf[:n]
148150
```
149151

150152
WebSocket connections are mediated by the host runtime, which enforces:
@@ -228,6 +230,7 @@ The agent imports host functions from the `env` module:
228230
- `tcp_connect` / `tcp_read` / `tcp_write` / `tcp_close`
229231
- `udp_sendto`
230232
- `websocket_connect` / `websocket_send` / `websocket_recv` / `websocket_close`
233+
- `camera_media_open` / `camera_media_write` / `camera_media_heartbeat` / `camera_media_close`
231234

232235
The SDK wraps these functions and exports `alloc`/`dealloc` for host memory access.
233236

examples/.DS_Store

6 KB
Binary file not shown.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
id: http-check
2+
name: HTTP Check
3+
version: 0.1.0
4+
entrypoint: run_check
5+
outputs: serviceradar.plugin_result.v1
6+
capabilities:
7+
- get_config
8+
- log
9+
- submit_result
10+
- http_request
11+
resources:
12+
requested_memory_mb: 64
13+
requested_cpu_ms: 2000
14+
permissions:
15+
allowed_domains:
16+
- serviceradar.cloud
17+
allowed_ports:
18+
- 443

examples/http-check/plugin.wasm

100755100644
1.35 MB
Binary file not shown.

sdk/camera_http.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package sdk
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
"time"
9+
)
10+
11+
var errCameraHostRequired = errors.New("host is required")
12+
13+
// CameraHTTPClient wraps the shared HTTP request behavior for camera plugins.
14+
type CameraHTTPClient struct {
15+
BaseURL string
16+
Timeout time.Duration
17+
AuthHeader string
18+
InsecureSkipVerify bool
19+
}
20+
21+
// NewCameraHTTPClient builds a shared HTTP client from camera plugin config.
22+
func NewCameraHTTPClient(cfg CameraPluginConfig, fallbackTimeout time.Duration) (*CameraHTTPClient, error) {
23+
host := strings.TrimSpace(cfg.Host)
24+
if host == "" {
25+
return nil, errCameraHostRequired
26+
}
27+
28+
scheme, err := cfg.NormalizedScheme()
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
return &CameraHTTPClient{
34+
BaseURL: fmt.Sprintf("%s://%s", scheme, host),
35+
Timeout: cfg.ParsedTimeout(fallbackTimeout),
36+
AuthHeader: cfg.BasicAuthHeader(),
37+
InsecureSkipVerify: cfg.InsecureSkipVerify,
38+
}, nil
39+
}
40+
41+
// URL resolves a relative path against the camera base URL.
42+
func (c *CameraHTTPClient) URL(path string) string {
43+
if c == nil {
44+
return path
45+
}
46+
47+
return c.BaseURL + path
48+
}
49+
50+
// DoContext performs an HTTP request with the shared auth and timeout settings.
51+
func (c *CameraHTTPClient) DoContext(ctx context.Context, req HTTPRequest) (*HTTPResponse, error) {
52+
if c == nil {
53+
return nil, errCameraHostRequired
54+
}
55+
56+
if req.URL == "" {
57+
req.URL = c.BaseURL
58+
}
59+
if req.TimeoutMS == 0 {
60+
req.TimeoutMS = int(c.Timeout.Milliseconds())
61+
}
62+
if c.InsecureSkipVerify {
63+
req.InsecureSkipVerify = true
64+
}
65+
if c.AuthHeader != "" {
66+
if req.Headers == nil {
67+
req.Headers = map[string]string{}
68+
}
69+
if _, ok := req.Headers["Authorization"]; !ok {
70+
req.Headers["Authorization"] = c.AuthHeader
71+
}
72+
}
73+
74+
return HTTP.DoContext(ctx, req)
75+
}
76+
77+
// GetContext performs a GET request against a relative path.
78+
func (c *CameraHTTPClient) GetContext(ctx context.Context, path string) (*HTTPResponse, error) {
79+
return c.DoContext(ctx, HTTPRequest{
80+
Method: "GET",
81+
URL: c.URL(path),
82+
})
83+
}

sdk/camera_http_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package sdk
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestNewCameraHTTPClient(t *testing.T) {
9+
t.Parallel()
10+
11+
client, err := NewCameraHTTPClient(CameraPluginConfig{
12+
Host: " camera.local ",
13+
Scheme: "HTTPS",
14+
Timeout: "15s",
15+
Username: "root",
16+
Password: "secret",
17+
}, 3*time.Second)
18+
if err != nil {
19+
t.Fatalf("expected client, got error %v", err)
20+
}
21+
22+
if client.BaseURL != "https://camera.local" {
23+
t.Fatalf("unexpected base URL: %q", client.BaseURL)
24+
}
25+
if client.Timeout != 15*time.Second {
26+
t.Fatalf("unexpected timeout: %s", client.Timeout)
27+
}
28+
if client.AuthHeader != "Basic cm9vdDpzZWNyZXQ=" {
29+
t.Fatalf("unexpected auth header: %q", client.AuthHeader)
30+
}
31+
if client.InsecureSkipVerify {
32+
t.Fatalf("expected insecure TLS to default false")
33+
}
34+
}
35+
36+
func TestNewCameraHTTPClientPropagatesInsecureSkipVerify(t *testing.T) {
37+
t.Parallel()
38+
39+
client, err := NewCameraHTTPClient(CameraPluginConfig{
40+
Host: "camera.local",
41+
Scheme: "https",
42+
InsecureSkipVerify: true,
43+
}, 3*time.Second)
44+
if err != nil {
45+
t.Fatalf("expected client, got error %v", err)
46+
}
47+
if !client.InsecureSkipVerify {
48+
t.Fatalf("expected insecure TLS flag to propagate")
49+
}
50+
}
51+
52+
func TestCameraHTTPClientURL(t *testing.T) {
53+
t.Parallel()
54+
55+
client := &CameraHTTPClient{BaseURL: "https://camera.local"}
56+
if got := client.URL("/axis-cgi/basicdeviceinfo.cgi"); got != "https://camera.local/axis-cgi/basicdeviceinfo.cgi" {
57+
t.Fatalf("unexpected camera URL: %q", got)
58+
}
59+
}

sdk/camera_media.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package sdk
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
)
8+
9+
var errCameraMediaStreamNotInitialized = errors.New("camera media stream not initialized")
10+
11+
// CameraMediaOpenRequest defines metadata needed to open a host camera media stream.
12+
type CameraMediaOpenRequest struct {
13+
TrackID string `json:"track_id,omitempty"`
14+
Codec string `json:"codec,omitempty"`
15+
PayloadFormat string `json:"payload_format,omitempty"`
16+
}
17+
18+
// CameraMediaChunkMetadata describes an uploaded camera media chunk.
19+
type CameraMediaChunkMetadata struct {
20+
TrackID string `json:"track_id,omitempty"`
21+
Sequence uint64 `json:"sequence,omitempty"`
22+
PTS int64 `json:"pts,omitempty"`
23+
DTS int64 `json:"dts,omitempty"`
24+
Keyframe bool `json:"keyframe,omitempty"`
25+
IsFinal bool `json:"is_final,omitempty"`
26+
Codec string `json:"codec,omitempty"`
27+
PayloadFormat string `json:"payload_format,omitempty"`
28+
}
29+
30+
// CameraMediaHeartbeat keeps a host camera media stream lease alive.
31+
type CameraMediaHeartbeat struct {
32+
Sequence uint64 `json:"sequence,omitempty"`
33+
TimestampUnix int64 `json:"timestamp_unix,omitempty"`
34+
}
35+
36+
// CameraMediaStream wraps a host camera media stream handle.
37+
type CameraMediaStream struct {
38+
handle uint32
39+
}
40+
41+
// OpenCameraMediaStream opens a host camera media stream.
42+
func OpenCameraMediaStream(req CameraMediaOpenRequest) (*CameraMediaStream, error) {
43+
return OpenCameraMediaStreamContext(context.Background(), req)
44+
}
45+
46+
// OpenCameraMediaStreamContext opens a host camera media stream with a context.
47+
func OpenCameraMediaStreamContext(ctx context.Context, req CameraMediaOpenRequest) (*CameraMediaStream, error) {
48+
if ctx != nil {
49+
if err := ctx.Err(); err != nil {
50+
return nil, err
51+
}
52+
}
53+
54+
payload, err := json.Marshal(req)
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
res := hostCameraMediaOpen(ptrFromBytes(payload), uint32(len(payload)))
60+
if res < 0 {
61+
return nil, hostErr(res, "camera_media_open")
62+
}
63+
64+
return &CameraMediaStream{handle: uint32(res)}, nil
65+
}
66+
67+
// Write uploads a camera media chunk to the host stream.
68+
func (s *CameraMediaStream) Write(meta CameraMediaChunkMetadata, payload []byte) error {
69+
return s.WriteContext(context.Background(), meta, payload)
70+
}
71+
72+
// WriteContext uploads a camera media chunk to the host stream with a context.
73+
func (s *CameraMediaStream) WriteContext(ctx context.Context, meta CameraMediaChunkMetadata, payload []byte) error {
74+
if ctx != nil {
75+
if err := ctx.Err(); err != nil {
76+
return err
77+
}
78+
}
79+
80+
if s == nil || s.handle == 0 {
81+
return errCameraMediaStreamNotInitialized
82+
}
83+
84+
metaJSON, err := json.Marshal(meta)
85+
if err != nil {
86+
return err
87+
}
88+
89+
res := hostCameraMediaWrite(
90+
s.handle,
91+
ptrFromBytes(metaJSON),
92+
uint32(len(metaJSON)),
93+
ptrFromBytes(payload),
94+
uint32(len(payload)),
95+
)
96+
97+
return hostErr(res, "camera_media_write")
98+
}
99+
100+
// Heartbeat renews the host stream lease.
101+
func (s *CameraMediaStream) Heartbeat(meta CameraMediaHeartbeat) error {
102+
return s.HeartbeatContext(context.Background(), meta)
103+
}
104+
105+
// HeartbeatContext renews the host stream lease with a context.
106+
func (s *CameraMediaStream) HeartbeatContext(ctx context.Context, meta CameraMediaHeartbeat) error {
107+
if ctx != nil {
108+
if err := ctx.Err(); err != nil {
109+
return err
110+
}
111+
}
112+
113+
if s == nil || s.handle == 0 {
114+
return errCameraMediaStreamNotInitialized
115+
}
116+
117+
metaJSON, err := json.Marshal(meta)
118+
if err != nil {
119+
return err
120+
}
121+
122+
res := hostCameraMediaHeartbeat(s.handle, ptrFromBytes(metaJSON), uint32(len(metaJSON)))
123+
return hostErr(res, "camera_media_heartbeat")
124+
}
125+
126+
// Close closes the host camera media stream.
127+
func (s *CameraMediaStream) Close(reason string) error {
128+
if s == nil || s.handle == 0 {
129+
return nil
130+
}
131+
132+
reasonBytes := []byte(reason)
133+
res := hostCameraMediaClose(s.handle, ptrFromBytes(reasonBytes), uint32(len(reasonBytes)))
134+
s.handle = 0
135+
136+
return hostErr(res, "camera_media_close")
137+
}

sdk/camera_media_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package sdk
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestCameraMediaStreamRequiresHandle(t *testing.T) {
9+
t.Parallel()
10+
11+
var stream *CameraMediaStream
12+
if err := stream.Write(CameraMediaChunkMetadata{Sequence: 1}, []byte("frame")); !errors.Is(err, errCameraMediaStreamNotInitialized) {
13+
t.Fatalf("expected camera media handle error, got %v", err)
14+
}
15+
16+
if err := stream.Heartbeat(CameraMediaHeartbeat{Sequence: 1}); !errors.Is(err, errCameraMediaStreamNotInitialized) {
17+
t.Fatalf("expected camera media heartbeat handle error, got %v", err)
18+
}
19+
}

0 commit comments

Comments
 (0)