diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cdeb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +server.crt +server.key diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..17a0c79 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 + +# Build the application from source +FROM golang:1.21 AS build-stage + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY cmd ./cmd +COPY internal ./internal +COPY pkg ./pkg +COPY gen ./gen + +RUN CGO_ENABLED=0 GOOS=linux go build -o /signaler ./cmd/signaler + +# Run the tests in the container +FROM build-stage AS run-test-stage +RUN go test -v ./... + +# Deploy the application binary into a lean image +FROM golang:1.21 AS build-release-stage + +WORKDIR / +COPY --from=build-stage /signaler / +COPY server.crt /server.crt +COPY server.key /server.key + +EXPOSE 8080 + +ENTRYPOINT ["/signaler"] \ No newline at end of file diff --git a/cmd/signaler/main.go b/cmd/signaler/main.go index 889a1d9..2c1bccc 100644 --- a/cmd/signaler/main.go +++ b/cmd/signaler/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "log" "net/http" @@ -10,8 +9,6 @@ import ( "github.com/chathaway-codes/home-sensors/v2/pkg/signaler" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/cors" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" ) func main() { @@ -19,15 +16,8 @@ func main() { reflector := grpcreflect.NewStaticReflector( servicepb.SignalerServiceName, ) - path, _ := grpcreflect.NewHandlerV1(reflector) - fmt.Printf("Got path %s\n", path) mux.Handle(grpcreflect.NewHandlerV1(reflector)) - path, _ = grpcreflect.NewHandlerV1Alpha(reflector) - fmt.Printf("Got path %s\n", path) mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector)) - - path, _ = servicepb.NewSignalerServiceHandler(signaler.New()) - fmt.Printf("Got path %s\n", path) mux.Handle(servicepb.NewSignalerServiceHandler(signaler.New())) corsHandler := cors.New(cors.Options{ @@ -35,7 +25,7 @@ func main() { http.MethodGet, http.MethodPost, }, - AllowedOrigins: []string{"example.com"}, + AllowedOrigins: []string{"*"}, AllowedHeaders: []string{ "Accept-Encoding", "Authorization", @@ -61,10 +51,12 @@ func main() { server := &http.Server{ Addr: "0.0.0.0:8080", - Handler: h2c.NewHandler(handler, &http2.Server{}), + Handler: handler, + //Handler: h2c.NewHandler(handler, &http2.Server{}), + } - if err := server.ListenAndServe(); err != nil { + if err := server.ListenAndServeTLS("server.crt", "server.key"); err != nil { log.Fatalf("Failed to listen for HTTP traffic: %v", err) } } diff --git a/cmd/watcher/watcher.go b/cmd/watcher/watcher.go index 456d88e..233e25d 100644 --- a/cmd/watcher/watcher.go +++ b/cmd/watcher/watcher.go @@ -8,7 +8,6 @@ import ( "flag" "fmt" "net/http" - "os" "time" "connectrpc.com/connect" @@ -16,13 +15,17 @@ import ( servicepb "github.com/chathaway-codes/home-sensors/v2/gen/genconnect" "github.com/chathaway-codes/home-sensors/v2/internal/sensors" "github.com/chathaway-codes/home-sensors/v2/internal/video" + "github.com/chathaway-codes/home-sensors/v2/internal/watcher/config" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/pkg/media" - "github.com/rs/zerolog" "github.com/rs/zerolog/log" "google.golang.org/protobuf/proto" ) +var ( + signalerServer = flag.String("signaler_address", "home.chathaway.codes", "address of the signaler") +) + func withAuth[T any](token string, v *T) *connect.Request[T] { req := connect.NewRequest[T](v) req.Header().Add("Authorization", "Bearer "+token) @@ -30,10 +33,32 @@ func withAuth[T any](token string, v *T) *connect.Request[T] { } func main() { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) flag.Parse() ctx := context.Background() + cfg, err := config.Default.Get() + if err != nil { + log.Fatal().Err(err).Msg("failed to get config") + } + + client := servicepb.NewSignalerServiceClient( + http.DefaultClient, + fmt.Sprintf("https://%s/", *signalerServer), + connect.WithGRPC(), + ) + authToken, err := client.CreateAuthToken(ctx, connect.NewRequest(&pb.CreateAuthTokenRequest{ + Home: cfg.HomeName, + Type: &pb.CreateAuthTokenRequest_Camera_{ + Camera: &pb.CreateAuthTokenRequest_Camera{ + Id: cfg.CameraName, + }, + }, + })) + if err != nil { + log.Fatal().Err(err).Msg("failed to get auth token") + } + token := authToken.Msg.GetToken() + vid, err := video.Default.Get() if err != nil { log.Fatal().Err(err).Msg("failed to get default video") @@ -43,19 +68,6 @@ func main() { if err != nil { log.Fatal().Err(err).Msg("failed to get default sensor") } - client := servicepb.NewSignalerServiceClient(http.DefaultClient, "http://192.168.0.65:8080/") - authToken, err := client.CreateAuthToken(ctx, connect.NewRequest(&pb.CreateAuthTokenRequest{ - Home: "home1234", - Type: &pb.CreateAuthTokenRequest_Camera_{ - Camera: &pb.CreateAuthTokenRequest_Camera{ - Id: "movie", - }, - }, - })) - if err != nil { - log.Fatal().Err(err).Msg("failed to get auth token") - } - token := authToken.Msg.GetToken() go vid.Run() defer vid.Done() @@ -75,7 +87,8 @@ func main() { // Wait for a session request session, err := client.PopSession(ctx, withAuth(token, &pb.PopSessionRequest{})) if err != nil { - log.Fatal().Err(err).Msg("error creating session") + log.Error().Err(err).Msg("error creating session") + continue } go handleSession(ctx, client, token, session, vid) } @@ -174,6 +187,7 @@ func handleSession(ctx context.Context, client servicepb.SignalerServiceClient, // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected + exitCh := make(chan struct{}) peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { log.Debug().Msgf("Peer Connection State has changed: %s\n", s.String()) @@ -181,9 +195,12 @@ func handleSession(ctx context.Context, client servicepb.SignalerServiceClient, // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - fmt.Println("Peer Connection has gone to failed exiting") + close(exitCh) return } + if s == webrtc.PeerConnectionStateDisconnected { + close(exitCh) + } }) peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { @@ -223,6 +240,12 @@ func handleSession(ctx context.Context, client servicepb.SignalerServiceClient, // Add ICE candidates from remote for { + select { + case <-exitCh: + return + default: + // check for another message + } msg, err := client.PopIceMessage(ctx, withAuth(token, &pb.PopIceMessageRequest{ SessionIdentifier: session.Msg.GetId(), })) diff --git a/internal/watcher/config/config.go b/internal/watcher/config/config.go index d8d19b2..95bfedd 100644 --- a/internal/watcher/config/config.go +++ b/internal/watcher/config/config.go @@ -17,10 +17,12 @@ type Cmd struct { } type Config struct { - H264Cmd *Cmd `yaml:"h264"` - IVFCmd *Cmd `yaml:"ivf"` - SensorCmd *Cmd `yaml:"sensor"` - SensorRateMS int64 `yaml:"sensor_rate_ms"` + HomeName string `yaml:"home"` + CameraName string `yaml:"name"` + H264Cmd *Cmd `yaml:"h264"` + IVFCmd *Cmd `yaml:"ivf"` + SensorCmd *Cmd `yaml:"sensor"` + SensorRateMS int64 `yaml:"sensor_rate_ms"` } func New(source []byte) (*Config, error) { diff --git a/kube/prod.yaml b/kube/prod.yaml new file mode 100644 index 0000000..2af534e --- /dev/null +++ b/kube/prod.yaml @@ -0,0 +1,90 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: signaler-deployment + labels: + app: signaler +spec: + replicas: 1 + selector: + matchLabels: + app: signaler + template: + metadata: + labels: + app: signaler + app.kubernetes.io/name: signaler-pods + spec: + containers: + - name: signaler + image: us-central1-docker.pkg.dev/home-sensors-400805/signaler/image:$VERSION + command: + - /signaler + ports: + - containerPort: 8080 + livenessProbe: + httpGet: + path: /metrics + port: 8080 + scheme: HTTPS + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /metrics + port: 8080 + scheme: HTTPS + initialDelaySeconds: 3 + periodSeconds: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: signaler-service + annotations: + cloud.google.com/app-protocols: '{"my-port":"HTTP2"}' +spec: + selector: + app.kubernetes.io/name: signaler-pods + ports: + - name: my-port + protocol: TCP + port: 8080 + targetPort: 8080 + appProtocol: HTTP2 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: signaler-ingress + annotations: + kubernetes.io/ingress.global-static-ip-name: signaler + networking.gke.io/managed-certificates: managed-cert + kubernetes.io/ingress.class: "gce" +spec: + rules: + - http: + paths: + - path: /signaler.SignalerService + pathType: Prefix + backend: + service: + name: signaler-service + port: + number: 8080 +--- +apiVersion: networking.gke.io/v1 +kind: ManagedCertificate +metadata: + name: managed-cert +spec: + domains: + - home.chathaway.codes + - www.home.chathaway.codes +--- +apiVersion: cloud.google.com/v1 +kind: BackendConfig +metadata: + name: my-bsc-backendconfig +spec: + timeoutSec: 40 \ No newline at end of file diff --git a/prod.yaml b/prod.yaml new file mode 100644 index 0000000..74a30b3 --- /dev/null +++ b/prod.yaml @@ -0,0 +1,83 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: signaler-deployment + labels: + app: signaler +spec: + replicas: 1 + selector: + matchLabels: + app: signaler + template: + metadata: + labels: + app: signaler + app.kubernetes.io/name: signaler-pods + spec: + containers: + - name: signaler + image: us-central1-docker.pkg.dev/home-sensors-400805/signaler/image:20231003-0032 + command: + - /signaler + ports: + - containerPort: 8080 + livenessProbe: + httpGet: + path: /metrics + port: 8080 + scheme: HTTPS + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /metrics + port: 8080 + scheme: HTTPS + initialDelaySeconds: 3 + periodSeconds: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: signaler-service + annotations: + cloud.google.com/app-protocols: '{"my-port":"HTTP2"}' +spec: + selector: + app.kubernetes.io/name: signaler-pods + ports: + - name: my-port + protocol: TCP + port: 8080 + targetPort: 8080 + appProtocol: HTTP2 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: signaler-ingress + annotations: + kubernetes.io/ingress.global-static-ip-name: signaler + networking.gke.io/managed-certificates: managed-cert + kubernetes.io/ingress.class: "gce" +spec: + rules: + - http: + paths: + - path: /signaler.SignalerService + pathType: Prefix + backend: + service: + name: signaler-service + port: + number: 8080 +--- +apiVersion: networking.gke.io/v1 +kind: ManagedCertificate +metadata: + name: managed-cert +spec: + domains: + - home.chathaway.codes + - www.home.chathaway.codes diff --git a/rpi_camera.yaml b/rpi_camera.yaml index c175d5d..c93b0a8 100644 --- a/rpi_camera.yaml +++ b/rpi_camera.yaml @@ -1,3 +1,5 @@ +home: Sunnyvale +name: Office h264: binary: "/usr/bin/libcamera-vid" arguments: diff --git a/sample.yaml b/sample.yaml index c075dc4..b9c7adb 100644 --- a/sample.yaml +++ b/sample.yaml @@ -1,3 +1,5 @@ +home: Sunnyvale +name: Movie h264: binary: "/usr/bin/cat" arguments: diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..ff78b24 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +ROOT=$(pwd) +export VERSION=$(date +%Y%m%d-%H%M) +IMAGE=us-central1-docker.pkg.dev/home-sensors-400805/signaler/image:$VERSION + +docker build -f Dockerfile . -t $IMAGE + +echo "Done building $IMAGE" + +echo -n "Hit enter to deploy" + +read + +docker push $IMAGE + +envsubst < kube/prod.yaml > prod.yaml + +kubectl apply -f prod.yaml diff --git a/temperature.py b/temperature.py new file mode 100644 index 0000000..27fa4ba --- /dev/null +++ b/temperature.py @@ -0,0 +1,17 @@ +import bme280 +import smbus2 +from time import sleep + +port = 1 +address = 0x77 # Adafruit BME280 address. Other BME280s may be different +bus = smbus2.SMBus(port) + +bme280.load_calibration_params(bus,address) + +while True: + bme280_data = bme280.sample(bus,address) + humidity = bme280_data.humidity + pressure = bme280_data.pressure + ambient_temperature = bme280_data.temperature + print(humidity, pressure, ambient_temperature) + sleep(1) diff --git a/ui/lib/main.dart b/ui/lib/main.dart index b9d4133..231a6b4 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -10,9 +10,9 @@ void main() async { Logger logger = Logger(); logger.i("Establishing connection..."); final channel = ClientChannel( - '192.168.0.65', - port: 8080, - options: const ChannelOptions(credentials: ChannelCredentials.insecure()), + 'home.chathaway.codes', + //port: 80, + //options: const ChannelOptions(credentials: ChannelCredentials.insecure()), channelShutdownHandler: () { logger.e("Channel shutdown unexpectedly"); }, @@ -54,7 +54,7 @@ class MyApp extends StatelessWidget { client, sessionService, title: 'Home Sensors', - home: "home1234", + home: "Sunnyvale", ), ); } @@ -85,12 +85,14 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String topMessage = "Creating session..."; List camerasToRender = []; + List samples = []; @override void initState() { super.initState(); _getSession(); _listCameras(); + _listSensors(); } _getSession() async { @@ -99,6 +101,20 @@ class _MyHomePageState extends State { setState(() {}); } + _listSensors() async { + var callOptions = CallOptions(metadata: { + 'Authorization': await widget.sessionService.getAuthToken(widget.home) + }); + var resp = await widget.client + .listSamples(ListSamplesRequest(), options: callOptions); + + for (var sample in resp.samples) { + samples + .add(Text("${sample.type}: ${sample.reading} on ${sample.cameraId}")); + } + setState(() {}); + } + _listCameras() async { var callOptions = CallOptions(metadata: { 'Authorization': await widget.sessionService.getAuthToken(widget.home) @@ -126,24 +142,26 @@ class _MyHomePageState extends State { // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Column( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. + appBar: AppBar( + // TRY THIS: Try changing the color here to a specific color (to + // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar + // change color while the other colors stay the same. + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: SingleChildScrollView( + child: Column( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. - children: [ - Text(topMessage), - ] + - camerasToRender, - ), - ); + children: [ + Text(topMessage), + ] + + samples + + camerasToRender, + ), + )); } }