Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added heart beat #2

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions cmd/tc2-hat-attiny/attinyconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (
)

type AttinyConfig struct {
OnWindow *window.Window
Battery config.Battery
OnWindow *window.Window
Battery config.Battery
LowPowerMode bool
}

func ParseConfig(configDir string) (*AttinyConfig, error) {
Expand All @@ -47,6 +48,11 @@ func ParseConfig(configDir string) (*AttinyConfig, error) {
return nil, err
}

recorder := config.DefaultThermalRecorder()
if err := rawConfig.Unmarshal(config.ThermalRecorderKey, &recorder); err != nil {
return nil, err
}

w, err := window.New(
windows.PowerOn,
windows.PowerOff,
Expand All @@ -57,7 +63,8 @@ func ParseConfig(configDir string) (*AttinyConfig, error) {
}

return &AttinyConfig{
OnWindow: w,
Battery: battery,
OnWindow: w,
Battery: battery,
LowPowerMode: recorder.UseLowPowerMode,
}, nil
}
190 changes: 190 additions & 0 deletions cmd/tc2-hat-attiny/heartbeat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package main

import (
"log"
"time"

api "github.com/TheCacophonyProject/go-api"
"github.com/TheCacophonyProject/modemd/connrequester"
"github.com/TheCacophonyProject/modemd/modemlistener"
"github.com/TheCacophonyProject/window"
)

const (
heartBeatDelay = 30 * time.Minute
interval = 4 * time.Hour
attemptDelay = 5 * time.Second

connTimeout = time.Minute * 2
connRetryInterval = time.Minute * 1
connMaxRetries = 3
)

type Heartbeat struct {
api *api.CacophonyAPI
window *window.Window
validUntil time.Time
end time.Time
penultimate bool
MaxAttempts int
}

// Used to test
type Clock interface {
Sleep(d time.Duration)
Now() time.Time
After(d time.Duration) <-chan time.Time
}

type HeartBeatClock struct {
}

func (h *HeartBeatClock) Sleep(d time.Duration) {
time.Sleep(d)
}
func (h *HeartBeatClock) Now() time.Time {
return time.Now()
}

func (h *HeartBeatClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}

var clock Clock = &HeartBeatClock{}

func heartBeatLoop(window *window.Window) {
hb := NewHeartbeat(window)
sendBeats(hb, window)
}
func sendBeats(hb *Heartbeat, window *window.Window) {
modemConnectSignal, err := modemlistener.GetModemConnectedSignalListener()
if err != nil {
log.Println("Failed to get modem connected signal listener")
}
initialDelay := heartBeatDelay

if !window.Active() {
until := window.Until()
if until > initialDelay {
initialDelay = until
}
}
log.Printf("Sending initial heartbeat in %v", initialDelay)
clock.Sleep(initialDelay)
for {
done := hb.updateNextBeat()
err := sendHeartbeat(hb.validUntil, hb.MaxAttempts)
if err != nil {
log.Printf("Error sending heartbeat, skipping this beat %v", err)
}
if done {
log.Printf("Sent penultimate heartbeat")
return
}

nextEventIn := hb.validUntil.Sub(clock.Now())
if !hb.penultimate && nextEventIn >= 2*time.Hour {
nextEventIn = nextEventIn - 1*time.Hour
} else {
// 5 minutes to give a bit of leeway
nextEventIn = nextEventIn - 5*time.Minute

}
log.Printf("Heartbeat sleeping until %v", clock.Now().Add(nextEventIn))
// Empty modemConnectSignal channel so as to not trigger from old signals
emptyChannel(modemConnectSignal)
select {
case <-modemConnectSignal:
log.Println("Modem connected")
case <-clock.After(nextEventIn):
}
}
}

func emptyChannel(ch chan time.Time) {
for {
select {
case <-ch:
default:
return
}
}
}

func NewHeartbeat(window *window.Window) *Heartbeat {
var nextEnd time.Time
if !window.NoWindow {
nextEnd = window.NextEnd()
}

h := &Heartbeat{end: nextEnd, window: window, MaxAttempts: 3}
return h
}

// updates next heart beat time, returns true if will be the final event
func (h *Heartbeat) updateNextBeat() bool {
if h.penultimate {
h.validUntil = h.end
return true
}
h.validUntil = clock.Now().Add(interval)
if !h.window.NoWindow && h.validUntil.After(h.end.Add(-time.Hour)) {
// always want an event 1 hour before end if possible
h.validUntil = h.end.Add(-time.Hour)
if clock.Now().After(h.validUntil) {
// rare case of very short window
h.validUntil = h.end
return true
}
h.penultimate = true
}
return false
}

func sendHeartbeat(nextBeat time.Time, attempts int) error {
cr := connrequester.NewConnectionRequester()
cr.Start()
defer cr.Stop()
if err := cr.WaitUntilUpLoop(connTimeout, connRetryInterval, connMaxRetries); err != nil {
log.Println("unable to get an internet connection. Not reporting events")
return err
}
var apiClient *api.CacophonyAPI
var err error
attempt := 0
for {
apiClient, err = api.New()
if err != nil {
attempt += 1
if attempt < attempts {
log.Printf("Error connecting to api %v trying again in %v", err, attemptDelay)
clock.Sleep(attemptDelay)
continue
}
log.Printf("Error connecting to api %v", err)
return err
}
break
}

attempt = 0
for {
_, err := apiClient.Heartbeat(nextBeat)
if err == nil {
log.Printf("Sent heartbeat, valid until %v", nextBeat)
return nil
}
attempt += 1
if attempt > attempts {
break
}
log.Printf("Error sending heartbeat %v, trying again in %v", err, attemptDelay)
clock.Sleep(attemptDelay)
}
return err
}

func sendFinalHeartBeat(window *window.Window) error {
log.Printf("Sending final heart beat")
return sendHeartbeat(window.NextStart().Add(heartBeatDelay*2), 3)
}
123 changes: 123 additions & 0 deletions cmd/tc2-hat-attiny/heartbeat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2018 The Cacophony Project. All rights reserved.
// Use of this source code is governed by the Apache License Version 2.0;
// see the LICENSE file for further details.

package main

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/TheCacophonyProject/window"
)

const dateFormat = "15:04"

type TestClock struct {
now time.Time
expectedSleeps []time.Time

sleepCount int
t *testing.T
hb *Heartbeat
}

func (h *TestClock) Sleep(d time.Duration) {
// nextBeat gets updated after sleep skip first
if h.sleepCount > 0 {
if h.sleepCount == len(h.expectedSleeps)-1 {
// penultimate event is only valid for 5 minutes after sleep
assert.Equal(h.t, h.expectedSleeps[h.sleepCount].Add(5*time.Minute).Format(dateFormat), h.hb.validUntil.Format(dateFormat))

} else {
assert.Equal(h.t, h.expectedSleeps[h.sleepCount].Add(1*time.Hour).Format(dateFormat), h.hb.validUntil.Format(dateFormat))
}
}
h.now = h.now.Add(d)
assert.Equal(h.t, h.now.Format(dateFormat), h.expectedSleeps[h.sleepCount].Format(dateFormat))
h.sleepCount += 1
}
func (h *TestClock) Now() time.Time {
return h.now
}
func (h *TestClock) After(d time.Duration) <-chan time.Time {
// nextBeat gets updated after sleep skip first
if h.sleepCount > 0 {
if h.sleepCount == len(h.expectedSleeps)-1 {
// penultimate event is only valid for 5 minutes after sleep
assert.Equal(h.t, h.expectedSleeps[h.sleepCount].Add(5*time.Minute).Format(dateFormat), h.hb.validUntil.Format(dateFormat))

} else {
assert.Equal(h.t, h.expectedSleeps[h.sleepCount].Add(1*time.Hour).Format(dateFormat), h.hb.validUntil.Format(dateFormat))
}
}
h.now = h.now.Add(d)
assert.Equal(h.t, h.now.Format(dateFormat), h.expectedSleeps[h.sleepCount].Format(dateFormat))
h.sleepCount += 1
s := make(chan time.Time, 10)
s <- h.now
return s
}

func TestSmallWindow(t *testing.T) {
clock := &TestClock{now: time.Now(), t: t}
w, err := window.New(clock.Now().Format(dateFormat), clock.Now().Add(time.Hour).Format(dateFormat), 0, 0)
sleeps := make([]time.Time, 1)
sleeps[0] = clock.now.Add(30 * time.Minute)

clock.expectedSleeps = sleeps
require.NoError(t, err)
heartBeatTestLoop(w, clock)
}
func TestShortDelay(t *testing.T) {
clock := &TestClock{now: time.Now(), t: t}
w, err := window.New(clock.Now().Add(10*time.Minute).Format(dateFormat), clock.Now().Add(4*time.Hour).Format(dateFormat), 0, 0)
sleeps := make([]time.Time, 2, 2)
sleeps[0] = clock.now.Add(30 * time.Minute)
sleeps[1] = w.NextEnd().Add(-65 * time.Minute)

clock.expectedSleeps = sleeps
require.NoError(t, err)
heartBeatTestLoop(w, clock)
}

func TestLongDelay(t *testing.T) {
clock := &TestClock{now: time.Now(), t: t}
w, err := window.New(clock.Now().Add(time.Hour).Format(dateFormat), clock.Now().Add(4*time.Hour).Format(dateFormat), 0, 0)
sleeps := make([]time.Time, 2, 2)
// expect delay until window starts if further than 30 minutes
sleeps[0] = clock.Now().Add(w.Until())
sleeps[1] = w.NextEnd().Add(-65 * time.Minute)

clock.expectedSleeps = sleeps
require.NoError(t, err)
heartBeatTestLoop(w, clock)
}

func TestWindow(t *testing.T) {
clock := &TestClock{now: time.Now(), t: t}
w, err := window.New(clock.Now().Format(dateFormat), clock.Now().Add(9*time.Hour).Format(dateFormat), 0, 0)
sleeps := make([]time.Time, 4, 4)
sleeps[0] = clock.now.Add(30 * time.Minute)
sleeps[1] = sleeps[0].Add(3 * time.Hour)
sleeps[2] = sleeps[1].Add(3 * time.Hour)
sleeps[3] = w.NextEnd().Add(-65 * time.Minute)

clock.expectedSleeps = sleeps
require.NoError(t, err)
heartBeatTestLoop(w, clock)
}

func heartBeatTestLoop(window *window.Window, timer *TestClock) {
clock = timer
hb := NewHeartbeat(window)
hb.MaxAttempts = 1
timer.hb = hb
sendBeats(hb, window)
assert.Equal(timer.t, timer.sleepCount, len(timer.expectedSleeps), "Missing sleep events")
// assert last beat is at end
assert.Equal(timer.t, window.NextEnd().Format(dateFormat), hb.validUntil.Format(dateFormat))
}
14 changes: 13 additions & 1 deletion cmd/tc2-hat-attiny/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ func runMain() error {
}

log.Printf("Running version: %s", version)
conf, err := ParseConfig(args.ConfigDir)

_, err := host.Init()
_, err = host.Init()
if err != nil {
return err
}
Expand All @@ -126,6 +127,10 @@ func runMain() error {
go monitorVoltageLoop(attiny)
go checkATtinySignalLoop(attiny)

nextEnd := conf.OnWindow.NextEnd()
if !conf.LowPowerMode {
go heartBeatLoop(conf.OnWindow)
}
/*
go func() {
for {
Expand Down Expand Up @@ -159,6 +164,13 @@ func runMain() error {
}

for {
if !conf.LowPowerMode && time.Now().After(nextEnd) {
//support for non stop cameras
go heartBeatLoop(conf.OnWindow)
nextEnd = conf.OnWindow.NextEnd()

}

stayOnUntilDuration := time.Until(stayOnUntil)
if stayOnUntilDuration > waitDuration {
waitDuration = stayOnUntilDuration
Expand Down
Loading