add: debian file, perform various fixes
This commit is contained in:
+202
-131
@@ -8,6 +8,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
@@ -19,6 +20,8 @@ import (
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v3/pkg/media"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -46,18 +49,6 @@ func main() {
|
||||
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 := h264video.Default.Get()
|
||||
if err != nil {
|
||||
@@ -78,6 +69,11 @@ func main() {
|
||||
sensorCh, sensorDone := sensors.Join()
|
||||
defer sensorDone()
|
||||
|
||||
token, err := getAuthToken(ctx, client, cfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to get auth token")
|
||||
}
|
||||
|
||||
go handleSensor(ctx, client, token, sensorCh)
|
||||
|
||||
// Create a new RTCPeerConnection
|
||||
@@ -87,6 +83,14 @@ func main() {
|
||||
// Wait for a session request
|
||||
session, err := client.PopSession(ctx, withAuth(token, &pb.PopSessionRequest{}))
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||
// try getting a new token
|
||||
token, err = getAuthToken(ctx, client, cfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to recreate auth token")
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Error().Err(err).Msg("error creating session")
|
||||
continue
|
||||
}
|
||||
@@ -94,6 +98,22 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func getAuthToken(ctx context.Context, client servicepb.SignalerServiceClient, cfg *config.Config) (string, error) {
|
||||
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 {
|
||||
return "", fmt.Errorf("CreateAuthToken failed: %w", err)
|
||||
}
|
||||
token := authToken.Msg.GetToken()
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func handleSensor(ctx context.Context, client servicepb.SignalerServiceClient, token string, ch <-chan *pb.Sample) {
|
||||
for {
|
||||
var sample *pb.Sample
|
||||
@@ -111,76 +131,12 @@ func handleSensor(ctx context.Context, client servicepb.SignalerServiceClient, t
|
||||
}
|
||||
}
|
||||
|
||||
func handleSession(ctx context.Context, client servicepb.SignalerServiceClient, token string, session *connect.Response[pb.Session], vid *h264video.Video) {
|
||||
var err error
|
||||
log.Debug().Msg("new session")
|
||||
|
||||
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("failed to get a connection")
|
||||
return
|
||||
}
|
||||
|
||||
// connect to the video stream; the cleanup is done in the goroutine which
|
||||
// consumes the framess
|
||||
ch, trackCodec, cleanUp := vid.Join()
|
||||
// Create a video track
|
||||
videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: trackCodec}, "video", "pion")
|
||||
if videoTrackErr != nil {
|
||||
log.Info().Err(videoTrackErr).Msg("Failed to create video track")
|
||||
return
|
||||
}
|
||||
|
||||
rtpSender, err := peerConnection.AddTrack(videoTrack)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("Failed to add track to connection")
|
||||
}
|
||||
|
||||
func getIceConnection(ctx context.Context, peerConnection *webrtc.PeerConnection, client servicepb.SignalerServiceClient, token string, session *connect.Response[pb.Session]) (context.Context, error) {
|
||||
// We use the cancel func to signal that the stream is ready
|
||||
iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background())
|
||||
defer func() {
|
||||
if err := peerConnection.Close(); err != nil {
|
||||
log.Debug().Err(err).Msg("cannot close peerConnection")
|
||||
}
|
||||
}()
|
||||
|
||||
// Read incoming RTCP packets
|
||||
// Before these packets are returned they are processed by interceptors. For things
|
||||
// like NACK this needs to be called.
|
||||
go func() {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
for {
|
||||
if _, _, err := rtpSender.Read(rtcpBuf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer cleanUp()
|
||||
readyToSend := false
|
||||
for frame := range ch {
|
||||
select {
|
||||
case <-iceConnectedCtx.Done():
|
||||
readyToSend = true
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
if !readyToSend {
|
||||
continue
|
||||
}
|
||||
if err := videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Millisecond * 33}); err != nil {
|
||||
log.Err(err).Msg("failed to write sample")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
iceDisconnectedCtx, iceDeconnectedCtxCancel := context.WithCancel(ctx)
|
||||
readyToSend := sync.WaitGroup{}
|
||||
readyToSend.Add(1)
|
||||
|
||||
// Set the handler for ICE connection state
|
||||
// This will notify you when the peer has connected/disconnected
|
||||
@@ -193,7 +149,6 @@ 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())
|
||||
|
||||
@@ -201,15 +156,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.
|
||||
close(exitCh)
|
||||
return
|
||||
}
|
||||
if s == webrtc.PeerConnectionStateDisconnected {
|
||||
close(exitCh)
|
||||
iceDeconnectedCtxCancel()
|
||||
}
|
||||
})
|
||||
|
||||
peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) {
|
||||
readyToSend.Wait()
|
||||
if i == nil {
|
||||
if _, err := client.CreateIceMessage(ctx, withAuth(token, &pb.CreateIceMessageRequest{
|
||||
SessionIdentifier: session.Msg.GetId(),
|
||||
@@ -226,6 +178,7 @@ func handleSession(ctx context.Context, client servicepb.SignalerServiceClient,
|
||||
if c.UsernameFragment != nil {
|
||||
usernameFragment = proto.String(*c.UsernameFragment)
|
||||
}
|
||||
log.Info().Msgf("got candidate %+v", c)
|
||||
client.CreateIceMessage(ctx, withAuth(token, &pb.CreateIceMessageRequest{
|
||||
SessionIdentifier: session.Msg.GetId(),
|
||||
IceMessage: &pb.IceMessage{
|
||||
@@ -240,19 +193,71 @@ func handleSession(ctx context.Context, client servicepb.SignalerServiceClient,
|
||||
},
|
||||
}))
|
||||
})
|
||||
log.Info().Msg("Spawning helper")
|
||||
|
||||
// helper which sends answers, waits for
|
||||
// Get the offer from the other
|
||||
msg, err := client.PopIceMessage(iceDisconnectedCtx, withAuth(token, &pb.PopIceMessageRequest{
|
||||
SessionIdentifier: session.Msg.GetId(),
|
||||
}))
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("failed to pop ice message")
|
||||
}
|
||||
iceSession := msg.Msg.GetSession()
|
||||
|
||||
switch iceSession.SdpType {
|
||||
case int64(webrtc.SDPTypeOffer):
|
||||
offer := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPType(iceSession.SdpType),
|
||||
SDP: iceSession.Sdp,
|
||||
}
|
||||
|
||||
if err := peerConnection.SetRemoteDescription(offer); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to set remote description")
|
||||
}
|
||||
default:
|
||||
log.Info().Msgf("unexpected sdp type: %v", webrtc.SDPType(iceSession.SdpType).String())
|
||||
}
|
||||
log.Info().Msg("Accepted promise!")
|
||||
|
||||
// Send back an answer
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("Candidate failed")
|
||||
}
|
||||
|
||||
if err := peerConnection.SetLocalDescription(answer); err != nil {
|
||||
log.Info().Err(err).Msg("Failed to set local description")
|
||||
}
|
||||
|
||||
_, err = client.CreateIceMessage(iceDisconnectedCtx, withAuth(token, &pb.CreateIceMessageRequest{
|
||||
SessionIdentifier: session.Msg.GetId(),
|
||||
IceMessage: &pb.IceMessage{
|
||||
Type: &pb.IceMessage_Session{
|
||||
Session: &pb.IceSessionDescription{
|
||||
SdpType: int64(answer.Type),
|
||||
Sdp: answer.SDP,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("Failed to send answer")
|
||||
}
|
||||
readyToSend.Done()
|
||||
|
||||
// TODO: do we add the video right here?
|
||||
|
||||
// Go into a loop processing ice candidates
|
||||
|
||||
// Add ICE candidates from remote
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-exitCh:
|
||||
case <-iceDisconnectedCtx.Done():
|
||||
return
|
||||
default:
|
||||
// check for another message
|
||||
}
|
||||
msg, err := client.PopIceMessage(ctx, withAuth(token, &pb.PopIceMessageRequest{
|
||||
msg, err := client.PopIceMessage(iceDisconnectedCtx, withAuth(token, &pb.PopIceMessageRequest{
|
||||
SessionIdentifier: session.Msg.GetId(),
|
||||
}))
|
||||
if err != nil {
|
||||
@@ -274,51 +279,117 @@ func handleSession(ctx context.Context, client servicepb.SignalerServiceClient,
|
||||
}); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to add ice candidate")
|
||||
}
|
||||
|
||||
// Send back an answer
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
log.Debug().Msg("Candidate failed")
|
||||
continue
|
||||
}
|
||||
|
||||
if err := peerConnection.SetLocalDescription(answer); err != nil {
|
||||
log.Info().Err(err).Msg("Failed to set local description")
|
||||
}
|
||||
|
||||
_, err = client.CreateIceMessage(ctx, withAuth(token, &pb.CreateIceMessageRequest{
|
||||
SessionIdentifier: session.Msg.GetId(),
|
||||
IceMessage: &pb.IceMessage{
|
||||
Type: &pb.IceMessage_Session{
|
||||
Session: &pb.IceSessionDescription{
|
||||
SdpType: int64(answer.Type),
|
||||
Sdp: answer.SDP,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("Failed to send answer")
|
||||
}
|
||||
case *pb.IceMessage_Session:
|
||||
iceSession := msg.Msg.GetSession()
|
||||
|
||||
switch iceSession.SdpType {
|
||||
case int64(webrtc.SDPTypeOffer):
|
||||
offer := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPType(iceSession.SdpType),
|
||||
SDP: iceSession.Sdp,
|
||||
}
|
||||
|
||||
if err := peerConnection.SetRemoteDescription(offer); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to set remote description")
|
||||
}
|
||||
default:
|
||||
log.Info().Msgf("unexpected sdp type: %v", webrtc.SDPType(iceSession.SdpType).String())
|
||||
}
|
||||
log.Info().Msg("Accepted promise!")
|
||||
case *pb.IceMessage_NoMoreCandidates:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-iceConnectedCtx.Done():
|
||||
// Success; return the connection
|
||||
return iceDisconnectedCtx, nil
|
||||
case <-iceDisconnectedCtx.Done():
|
||||
// No connection; return error
|
||||
return ctx, fmt.Errorf("failed to create connection: ICE disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
func handleSession(ctx context.Context, client servicepb.SignalerServiceClient, token string, session *connect.Response[pb.Session], vid *h264video.Video) {
|
||||
log.Debug().Msg("new session")
|
||||
|
||||
// connect to the video stream; the cleanup is done in the goroutine which
|
||||
// consumes the framess
|
||||
ch, trackCodec, cleanUp := vid.Join()
|
||||
defer cleanUp()
|
||||
|
||||
// Create a video track
|
||||
videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: trackCodec}, "video", "pion")
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("Failed to create video track")
|
||||
return
|
||||
}
|
||||
|
||||
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to make connection")
|
||||
}
|
||||
|
||||
rtpSender, err := peerConnection.AddTrack(videoTrack)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("Failed to add track to connection")
|
||||
return
|
||||
}
|
||||
|
||||
disconnectedCtx, err := getIceConnection(ctx, peerConnection, client, token, session)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed creating ice connection")
|
||||
}
|
||||
|
||||
log.Info().Msgf("State is %+v", rtpSender.Transport().State())
|
||||
|
||||
// Read incoming RTCP packets
|
||||
// Before these packets are returned they are processed by interceptors. For things
|
||||
// like NACK this needs to be called.
|
||||
go func() {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
for {
|
||||
select {
|
||||
case <-disconnectedCtx.Done():
|
||||
return
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
if _, _, err := rtpSender.Read(rtcpBuf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start a routine to send frames from a buffer
|
||||
var frame []byte
|
||||
var frameLock sync.Mutex
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 30)
|
||||
for {
|
||||
select {
|
||||
case <-disconnectedCtx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// do nothing
|
||||
}
|
||||
frameLock.Lock()
|
||||
myFrame := frame
|
||||
frame = nil
|
||||
frameLock.Unlock()
|
||||
if myFrame == nil {
|
||||
continue
|
||||
}
|
||||
if err := videoTrack.WriteSample(media.Sample{Data: myFrame, Duration: time.Millisecond * 33}); err != nil {
|
||||
log.Err(err).Msg("failed to write sample")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for myFrame := range ch {
|
||||
select {
|
||||
case <-disconnectedCtx.Done():
|
||||
return
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
frameLock.Lock()
|
||||
frame = myFrame
|
||||
frameLock.Unlock()
|
||||
}
|
||||
// TODO: Video ended; close the connection
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Package: watcher
|
||||
Version: 0.2
|
||||
Maintainer: Charles
|
||||
Architecture: all
|
||||
Description: Watches cameras and temp sensors
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
systemctl daemon-reload
|
||||
# Make sure its enabled
|
||||
systemctl enable --now watcher
|
||||
# Restart it; it might have already been installed and running
|
||||
systemctl restart watcher
|
||||
@@ -0,0 +1,14 @@
|
||||
# Install to /etc/systemd/system/watcher.service
|
||||
[Unit]
|
||||
Description=Run watcher
|
||||
After=network-online.target
|
||||
Wants=network-online.target systemd-networkd-wait-online.service
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/local/bin/watcher --watcher_config /etc/watcher_config.yaml --watcher_name /etc/watcher_name.txt
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,14 @@
|
||||
# Install to /etc/systemd/system/watcher.service
|
||||
[Unit]
|
||||
Description=Run watcher
|
||||
After=network-online.target
|
||||
Wants=network-online.target systemd-networkd-wait-online.service
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/local/bin --watcher_config /etc/watcher.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,20 @@
|
||||
home: Sunnyvale
|
||||
name: Office
|
||||
h264:
|
||||
binary: "/usr/bin/libcamera-vid"
|
||||
arguments:
|
||||
- "-n"
|
||||
- "-t"
|
||||
- "0"
|
||||
- "--codec"
|
||||
- "h264"
|
||||
- "--mode"
|
||||
- "1640:1232"
|
||||
- "--inline"
|
||||
- "-o"
|
||||
- "-"
|
||||
sensor:
|
||||
binary: "/usr/bin/python3"
|
||||
arguments:
|
||||
- "/usr/local/bin/temperature.py"
|
||||
sensor_rate_ms: 10000
|
||||
@@ -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)
|
||||
Executable
BIN
Binary file not shown.
@@ -25,20 +25,23 @@ type Config struct {
|
||||
SensorRateMS int64 `yaml:"sensor_rate_ms"`
|
||||
}
|
||||
|
||||
func New(source []byte) (*Config, error) {
|
||||
func New(source []byte, name string) (*Config, error) {
|
||||
config := &Config{}
|
||||
if err := yaml.Unmarshal(source, config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
config.CameraName = name
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type Mod struct {
|
||||
filePath string
|
||||
namePath string
|
||||
}
|
||||
|
||||
func (m *Mod) RegisterFlags(fs *flag.FlagSet) {
|
||||
fs.StringVar(&m.filePath, "watcher_config", "", "path to the watcher configuration")
|
||||
fs.StringVar(&m.namePath, "watcher_name", "/var/lib/dbus/machine-id", "location of the file to pull name from")
|
||||
}
|
||||
|
||||
func (m *Mod) Get() (*Config, error) {
|
||||
@@ -47,7 +50,13 @@ func (m *Mod) Get() (*Config, error) {
|
||||
return nil, fmt.Errorf("failed to read file %q: %w", m.filePath, err)
|
||||
}
|
||||
|
||||
config, err := New(bytes)
|
||||
// Override name using a unique ID; we can let users name things in the app
|
||||
myID, err := os.ReadFile(m.namePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read machine ID from %q", m.namePath)
|
||||
}
|
||||
|
||||
config, err := New(bytes, string(myID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -223,7 +223,16 @@ func (s *Server) PopSession(ctx context.Context, request *connect.Request[pb.Pop
|
||||
ch := s.sessionsByCamera[authToken.Uid]
|
||||
s.mu.Unlock()
|
||||
|
||||
sess := <-ch
|
||||
var sess *session
|
||||
tick := time.NewTicker(time.Second * 30)
|
||||
defer tick.Stop()
|
||||
select {
|
||||
case sess = <-ch:
|
||||
// OK
|
||||
case <-tick.C:
|
||||
// have them retry
|
||||
return nil, connect.NewError(connect.CodeDeadlineExceeded, fmt.Errorf("try again"))
|
||||
}
|
||||
|
||||
if sess == nil {
|
||||
return nil, status.Errorf(codes.DataLoss, "someone else stole the session")
|
||||
|
||||
@@ -17,7 +17,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: signaler
|
||||
image: us-central1-docker.pkg.dev/home-sensors-400805/signaler/image:20231003-0032
|
||||
image: us-central1-docker.pkg.dev/home-sensors-400805/signaler/image:20240110-2245
|
||||
command:
|
||||
- /signaler
|
||||
ports:
|
||||
@@ -81,3 +81,10 @@ spec:
|
||||
domains:
|
||||
- home.chathaway.codes
|
||||
- www.home.chathaway.codes
|
||||
---
|
||||
apiVersion: cloud.google.com/v1
|
||||
kind: BackendConfig
|
||||
metadata:
|
||||
name: my-bsc-backendconfig
|
||||
spec:
|
||||
timeoutSec: 40
|
||||
+1
-15
@@ -10,25 +10,11 @@ h264:
|
||||
- "h264"
|
||||
- "--mode"
|
||||
- "1640:1232"
|
||||
- "--denoise"
|
||||
- "off"
|
||||
- "--inline"
|
||||
- "-o"
|
||||
- "-"
|
||||
ivf:
|
||||
binary: "/usr/bin/ffmpeg"
|
||||
arguments:
|
||||
- "-i"
|
||||
- "-"
|
||||
- "-g"
|
||||
- "30"
|
||||
- "-b:v"
|
||||
- "2M"
|
||||
- "-f"
|
||||
- "ivf"
|
||||
- "-"
|
||||
sensor:
|
||||
binary: "/usr/bin/python3"
|
||||
arguments:
|
||||
- "/home/charles/temperature.py"
|
||||
- "/usr/local/bin/temperature.py"
|
||||
sensor_rate_ms: 10000
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
mkdir -p dbuild/usr/local/bin
|
||||
mkdir -p dbuild/etc/
|
||||
mkdir -p dbuild/etc/systemd/system/
|
||||
mkdir -p dbuild/DEBIAN
|
||||
|
||||
cat <<EOF > dbuild/DEBIAN/control
|
||||
Package: watcher
|
||||
Version: 0.2
|
||||
Maintainer: Charles
|
||||
Architecture: all
|
||||
Description: Watches cameras and temp sensors
|
||||
EOF
|
||||
|
||||
cat <<EOF > dbuild/DEBIAN/postinst
|
||||
#!/bin/bash
|
||||
|
||||
systemctl daemon-reload
|
||||
# Make sure its enabled
|
||||
systemctl enable --now watcher
|
||||
# Restart it; it might have already been installed and running
|
||||
systemctl restart watcher
|
||||
EOF
|
||||
chmod +x dbuild/DEBIAN/postinst
|
||||
|
||||
GOOS=linux GOARCH=arm64 go build -o dbuild/usr/local/bin/watcher ./cmd/watcher
|
||||
|
||||
cp ./rpi_camera.yaml dbuild/etc/watcher_config.yaml
|
||||
cp ./watcher.systemd dbuild/etc/systemd/system/watcher.service
|
||||
cp ./temperature.py dbuild/usr/local/bin/
|
||||
|
||||
|
||||
dpkg-deb --build dbuild
|
||||
@@ -31,5 +31,6 @@
|
||||
android:value="2" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
</manifest>
|
||||
|
||||
+24
-13
@@ -38,15 +38,16 @@ class CallState extends State<Call> {
|
||||
logger.i("Init remote renderer");
|
||||
await _remoteRenderer.initialize();
|
||||
logger.i("Creating session");
|
||||
await _createSesson();
|
||||
await _createSession();
|
||||
}
|
||||
|
||||
_createSesson() async {
|
||||
_createSession() async {
|
||||
var callOptions = CallOptions(metadata: {
|
||||
'Authorization': await widget.sessionService.getAuthToken(widget.home)
|
||||
});
|
||||
|
||||
var cancelCreate = Completer();
|
||||
var sendIceCandidates = Completer();
|
||||
|
||||
var clientSession = await widget.client.createSession(
|
||||
pb.CreateSessionRequest(
|
||||
@@ -73,6 +74,8 @@ class CallState extends State<Call> {
|
||||
};
|
||||
|
||||
peerConnection.onIceCandidate = (candidate) async {
|
||||
await sendIceCandidates.future;
|
||||
logger.i("Sending ICE candidate");
|
||||
if (candidate.candidate == null) {
|
||||
await widget.client.createIceMessage(
|
||||
CreateIceMessageRequest(
|
||||
@@ -99,16 +102,15 @@ class CallState extends State<Call> {
|
||||
};
|
||||
|
||||
peerConnection.onIceConnectionState = (state) {
|
||||
statusLine = "Ice state now $state";
|
||||
statusLine = "$state";
|
||||
setState(() {});
|
||||
logger.i("Ice state now $state");
|
||||
|
||||
switch (state) {
|
||||
case RTCIceConnectionState.RTCIceConnectionStateClosed:
|
||||
case RTCIceConnectionState.RTCIceConnectionStateDisconnected:
|
||||
//case RTCIceConnectionState.RTCIceConnectionStateClosed:
|
||||
//case RTCIceConnectionState.RTCIceConnectionStateDisconnected:
|
||||
case RTCIceConnectionState.RTCIceConnectionStateFailed:
|
||||
cancelCreate.complete(CallCancelled());
|
||||
_connect();
|
||||
//_connect();
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
@@ -136,7 +138,6 @@ class CallState extends State<Call> {
|
||||
var offer = await peerConnection.createOffer();
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
// Send offer through signaling server
|
||||
logger.i("Offer is $offer");
|
||||
await widget.client.createIceMessage(
|
||||
pb.CreateIceMessageRequest(
|
||||
sessionIdentifier: clientSession.id,
|
||||
@@ -149,6 +150,20 @@ class CallState extends State<Call> {
|
||||
),
|
||||
options: callOptions);
|
||||
|
||||
// Expect back a response
|
||||
var someResponse = await Future.any([
|
||||
widget.client.popIceMessage(
|
||||
pb.PopIceMessageRequest(sessionIdentifier: clientSession.id),
|
||||
options: callOptions),
|
||||
cancelCreate.future,
|
||||
]);
|
||||
var resp = someResponse as pb.IceMessage;
|
||||
var session = resp.session;
|
||||
await peerConnection
|
||||
.setRemoteDescription(RTCSessionDescription(session.sdp, "answer"));
|
||||
|
||||
sendIceCandidates.complete();
|
||||
|
||||
// Get candidates from remote
|
||||
while (true) {
|
||||
var someResponse = await Future.any([
|
||||
@@ -168,10 +183,6 @@ class CallState extends State<Call> {
|
||||
resp.candidate.sdpLineIndex));
|
||||
} else if (resp.hasNoMoreCandidates()) {
|
||||
logger.i("No more candidates from remote");
|
||||
} else if (resp.hasSession()) {
|
||||
var session = resp.session;
|
||||
await peerConnection
|
||||
.setRemoteDescription(RTCSessionDescription(session.sdp, "answer"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -183,7 +194,7 @@ class CallState extends State<Call> {
|
||||
Text(widget.cameraID.id),
|
||||
Text(statusLine),
|
||||
SizedBox(
|
||||
height: 480,
|
||||
height: 320,
|
||||
child: _ready
|
||||
? RTCVideoView(_remoteRenderer)
|
||||
: const Text("Loading...")),
|
||||
|
||||
+23
-14
@@ -1,3 +1,5 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
//import 'package:grpc/grpc_web.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
@@ -36,9 +38,9 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
title: 'Home Sensors',
|
||||
theme: ThemeData.dark(
|
||||
// colorScheme: ColorScheme.fromSeed(seedColor: Colors.black87),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: MyHomePage(
|
||||
@@ -75,8 +77,8 @@ class MyHomePage extends StatefulWidget {
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
String topMessage = "Creating session...";
|
||||
List<Call> camerasToRender = [];
|
||||
List<Widget> samples = [];
|
||||
List<Widget> camerasToRender = [];
|
||||
Map<String, Widget> cameraSamples = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -100,8 +102,11 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
.listSamples(ListSamplesRequest(), options: callOptions);
|
||||
|
||||
for (var sample in resp.samples) {
|
||||
samples
|
||||
.add(Text("${sample.type}: ${sample.reading} on ${sample.cameraId}"));
|
||||
if (sample.type == Sample_Type.TEMPERATURE_C) {
|
||||
var reading = (sample.reading * 9.0 / 5.0) + 32;
|
||||
cameraSamples[sample.cameraId.id] =
|
||||
Text("${reading.toStringAsFixed(2)} f");
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
@@ -113,12 +118,22 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
var cameras = await widget.client
|
||||
.listCameras(ListCamerasRequest(), options: callOptions);
|
||||
|
||||
cameras.cameras.sort((a, b) => a.identifier.id.compareTo(b.identifier.id));
|
||||
camerasToRender = [];
|
||||
for (var camera in cameras.cameras) {
|
||||
camerasToRender.add(Call(
|
||||
List<Widget> children = [
|
||||
Call(
|
||||
widget.client,
|
||||
widget.sessionService,
|
||||
cameraID: camera.identifier,
|
||||
home: widget.home,
|
||||
),
|
||||
];
|
||||
if (cameraSamples.containsKey(camera.identifier.id)) {
|
||||
children.add(cameraSamples[camera.identifier.id]!);
|
||||
}
|
||||
camerasToRender.add(Column(
|
||||
children: children,
|
||||
));
|
||||
}
|
||||
setState(() {});
|
||||
@@ -134,12 +149,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
// 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: SingleChildScrollView(
|
||||
@@ -150,7 +160,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
children: <Widget>[
|
||||
Text(topMessage),
|
||||
] +
|
||||
samples +
|
||||
camerasToRender,
|
||||
),
|
||||
));
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Install to /etc/systemd/system/watcher.service
|
||||
[Unit]
|
||||
Description=Run watcher
|
||||
After=network-online.target
|
||||
Wants=network-online.target systemd-networkd-wait-online.service
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/local/bin/watcher --watcher_config /etc/watcher_config.yaml --watcher_name /etc/watcher_name.txt
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user