Skip to content

Commit acbb42a

Browse files
Add timelapse functionality
1 parent 07ce624 commit acbb42a

File tree

7 files changed

+282
-87
lines changed

7 files changed

+282
-87
lines changed

rpi-timelapse/src/capture.go

Lines changed: 0 additions & 28 deletions
This file was deleted.

rpi-timelapse/src/main.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package main
22

33
import (
4+
"log"
45
"os"
5-
"time"
66
)
77

88
func main() {
@@ -11,6 +11,10 @@ func main() {
1111
if siteRoot == "" {
1212
siteRoot = ""
1313
}
14+
sharedAssets := os.Getenv("RPI_CAMERA_SHARED_ASSETS_SITE")
15+
if siteRoot == "" {
16+
sharedAssets = ""
17+
}
1418

1519
storageDirectory := os.Getenv("RPI_CAMERA_STORAGE_DIR")
1620
if storageDirectory == "" {
@@ -21,32 +25,26 @@ func main() {
2125
StorageDirectory: storageDirectory,
2226
}
2327

24-
capturer := &ImageCapturer{}
25-
26-
timelapse := &TimelapseSettings{
27-
Name: "test",
28-
Interval: (time.Duration(30) * time.Second),
29-
Camera: CameraSettings{
30-
HFlip: false,
31-
VFlip: false,
32-
Width: 640,
33-
Height: 480,
34-
},
35-
}
36-
3728
timelapseCamera := &TimelapseCamera{
38-
ImageCapturer: capturer,
39-
Store: store,
29+
Store: store,
4030
}
4131

4232
store.Init()
43-
store.SetCurrentTimelapse(timelapse)
4433

45-
timelapseCamera.StartTimelapse(timelapse)
34+
timelapse, err := store.GetCurrentTimelapse()
35+
if err != nil {
36+
log.Printf("Unable to get current timelapse: %s", err.Error())
37+
} else {
38+
log.Printf("Starting existing timelapse %+v", timelapse)
39+
go timelapseCamera.StartTimelapse(timelapse)
40+
}
4641

4742
handleRequests(
48-
siteRoot,
43+
&SiteInfo{
44+
SiteRoot: siteRoot,
45+
SharedAssetsSite: sharedAssets,
46+
},
4947
store,
50-
capturer,
48+
timelapseCamera,
5149
)
5250
}

rpi-timelapse/src/server.go

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,62 @@ package main
22

33
import (
44
"fmt"
5+
"html/template"
56
"log"
67
"net/http"
7-
"strings"
8+
"strconv"
9+
"time"
810

911
"github.com/gorilla/mux"
1012
)
1113

1214
type ImageResultHander struct {
13-
Store *TimelapseStore
14-
Capturer *ImageCapturer
15+
SiteInfo *SiteInfo
16+
Store *TimelapseStore
17+
Camera *TimelapseCamera
18+
Templates *Templates
19+
}
20+
21+
type Templates struct {
22+
Index *template.Template
23+
Images *template.Template
24+
}
25+
26+
type SiteInfo struct {
27+
SiteRoot string
28+
SharedAssetsSite string
29+
}
30+
31+
type ImageResponseData struct {
32+
Timestamp string
33+
}
34+
35+
type ImagesPageResponseData struct {
36+
Images []ImageResponseData
37+
SiteInfo *SiteInfo
38+
}
39+
40+
type IndexPageResponseData struct {
41+
SiteInfo *SiteInfo
42+
}
43+
44+
var DEFAULT_CAMERA_SETTINGS CameraSettings = CameraSettings{
45+
HFlip: false,
46+
VFlip: false,
47+
Width: 1600,
48+
Height: 1080,
49+
Rotation: 270,
50+
}
51+
52+
var NOW_CAMERA_SETTINGS CameraSettings = CameraSettings{
53+
HFlip: false,
54+
VFlip: false,
55+
Width: 320,
56+
Height: 240,
57+
Rotation: 270,
1558
}
1659

1760
func (ih *ImageResultHander) GetLatestImage(w http.ResponseWriter, r *http.Request) {
18-
log.Println(r.URL)
1961

2062
w.Header().Set("Content-Type", "image/png")
2163
w.WriteHeader(http.StatusOK)
@@ -24,20 +66,22 @@ func (ih *ImageResultHander) GetLatestImage(w http.ResponseWriter, r *http.Reque
2466
}
2567

2668
func (ih *ImageResultHander) GetImageNamePage(w http.ResponseWriter, r *http.Request) {
27-
log.Println(r.URL)
2869
w.Header().Set("Content-Type", "text/html")
2970
w.WriteHeader(http.StatusOK)
3071
names, _ := ih.Store.ImageNames()
31-
var builder strings.Builder
3272

33-
builder.WriteString("<ul>")
34-
builder.WriteString("<li><a href=\"latest/\">latest</a>")
35-
for _, n := range names {
36-
fmt.Fprintf(&builder, "<li><a href=\"%v/\">%v</a>", n, n)
73+
var images []ImageResponseData
74+
75+
for _, name := range names {
76+
images = append(images, ImageResponseData{
77+
Timestamp: name,
78+
})
3779
}
38-
builder.WriteString("</ul>")
3980

40-
w.Write([]byte(builder.String()))
81+
ih.Templates.Images.Execute(w, ImagesPageResponseData{
82+
Images: images,
83+
SiteInfo: ih.SiteInfo,
84+
})
4185

4286
}
4387

@@ -46,25 +90,72 @@ func (ih *ImageResultHander) GetImageByName(w http.ResponseWriter, r *http.Reque
4690

4791
name := v["imageName"]
4892

49-
log.Println(r.URL)
5093
w.Header().Set("Content-Type", "image/png")
5194

5295
w.WriteHeader(http.StatusOK)
5396

5497
ih.Store.ImageByName(name, w)
5598
}
5699

57-
func handleRequests(siteRoot string, store *TimelapseStore, capturer *ImageCapturer) {
100+
func (ih *ImageResultHander) GetCurrentImage(w http.ResponseWriter, r *http.Request) {
101+
102+
w.Header().Set("Content-Type", "image/png")
103+
104+
w.WriteHeader(http.StatusOK)
105+
106+
ih.Camera.CaptureImage(&NOW_CAMERA_SETTINGS, w)
107+
}
108+
109+
func handleRequests(siteInfo *SiteInfo, store *TimelapseStore, capturer *TimelapseCamera) {
110+
111+
templates := &Templates{
112+
Index: template.Must(template.ParseFiles("./src/templates/index.html")),
113+
Images: template.Must(template.ParseFiles("./src/templates/images.html")),
114+
}
58115

59116
handler := &ImageResultHander{
60-
Store: store,
61-
Capturer: capturer,
117+
SiteInfo: siteInfo,
118+
Store: store,
119+
Camera: capturer,
120+
Templates: templates,
62121
}
63122

64123
rootRoute := mux.NewRouter()
65-
rootRoute.HandleFunc(siteRoot+"/images/", handler.GetImageNamePage)
66-
rootRoute.HandleFunc(siteRoot+"/images/latest/", handler.GetLatestImage)
67-
rootRoute.HandleFunc(siteRoot+"/images/{imageName}/", handler.GetImageByName)
124+
rootRoute.HandleFunc(siteInfo.SiteRoot+"/images/", handler.GetImageNamePage)
125+
rootRoute.HandleFunc(siteInfo.SiteRoot+"/images/latest/", handler.GetLatestImage)
126+
rootRoute.HandleFunc(siteInfo.SiteRoot+"/images/now/", handler.GetCurrentImage)
127+
rootRoute.HandleFunc(siteInfo.SiteRoot+"/images/{imageName}/", handler.GetImageByName)
128+
rootRoute.
129+
Path(siteInfo.SiteRoot + "/").
130+
Methods("GET").
131+
HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
132+
templates.Index.Execute(rw, IndexPageResponseData{
133+
SiteInfo: siteInfo,
134+
})
135+
})
136+
rootRoute.
137+
Path(siteInfo.SiteRoot + "/").
138+
Methods("POST").
139+
HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
140+
141+
interval, err := strconv.Atoi(r.FormValue("timelapse-interval"))
142+
if err != nil {
143+
rw.WriteHeader(http.StatusBadRequest)
144+
fmt.Fprintf(rw, "Interval was not a number")
145+
return
146+
}
147+
148+
http.Redirect(rw, r, siteInfo.SiteRoot+"/", http.StatusFound)
149+
150+
timelapse := &TimelapseSettings{
151+
Name: r.FormValue("timelapse-name"),
152+
Interval: time.Duration(interval) * time.Second,
153+
Camera: DEFAULT_CAMERA_SETTINGS,
154+
}
155+
156+
store.SetCurrentTimelapse(timelapse)
157+
capturer.StartTimelapse(timelapse)
158+
})
68159

69160
log.Println("Starting server on port 10000")
70161

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en-GB">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Raspberry Pi Timelapse Camera - Images</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<link rel="stylesheet" href="{{ .SiteInfo.SiteRoot }}/static/speed-test.css">
8+
<link rel="stylesheet" href="{{ .SiteInfo.SharedAssetsSite }}/main.css">
9+
</head>
10+
<body>
11+
<main class="content">
12+
<a href="{{ .SiteInfo.SiteRoot }}/">Back</a>
13+
<h1>Images</h1>
14+
15+
<a href="{{ .SiteInfo.SiteRoot }}/images/latest/">Latest</a>
16+
<ul>
17+
{{range .Images}}
18+
<li><a href="{{$.SiteInfo.SiteRoot}}/images/{{.Timestamp}}/">{{.Timestamp}}</a>
19+
{{end}}
20+
</ul>
21+
22+
</main>
23+
</body>
24+
</html>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE html>
2+
<html lang="en-GB">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Raspberry Pi Timelapse Camera</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<link rel="stylesheet" href="{{ .SiteInfo.SiteRoot }}/static/speed-test.css">
8+
<link rel="stylesheet" href="{{ .SiteInfo.SharedAssetsSite }}/main.css">
9+
</head>
10+
<body>
11+
<main class="content">
12+
<h1>Raspberry Pi Timelapse Camera</h1>
13+
<h2>New timelapse</h2>
14+
<form action="." method="post">
15+
<label for="timelapse-name">Name</label>
16+
<input type="text" name="timelapse-name" id="timelapse-name"></input>
17+
<label for="timelapse-interval">Interval (seconds)</label>
18+
<input type="number" step="1" min="0" id="timelapse-interval" name="timelapse-interval"></input>
19+
<button type="submit">Create</button>
20+
</form>
21+
22+
<h2>Current timelapse</h2>
23+
<p><a href="{{ .SiteInfo.SiteRoot }}/images/">Current timelapse images</a>
24+
25+
<h2>Now</h2>
26+
<img src="{{ .SiteInfo.SiteRoot }}/images/now/">
27+
</main>
28+
</body>
29+
</html>

rpi-timelapse/src/timelapse-camera.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,65 @@
11
package main
22

33
import (
4+
"fmt"
5+
"io"
46
"log"
7+
"os"
8+
"sync"
59
"time"
10+
11+
"github.com/dhowden/raspicam"
612
)
713

814
type TimelapseCamera struct {
915
Store *TimelapseStore
10-
ImageCapturer *ImageCapturer
1116
CurrentTicker *time.Ticker
17+
Mutex sync.Mutex
1218
}
1319

1420
func (tt *TimelapseCamera) StartTimelapse(t *TimelapseSettings) {
21+
22+
if tt.CurrentTicker != nil {
23+
tt.CurrentTicker.Stop()
24+
tt.CurrentTicker = nil
25+
}
26+
1527
log.Printf("Starting timelapse ticker with interval %v", t.Interval)
1628
tt.CurrentTicker = time.NewTicker(t.Interval)
1729
go func() {
1830
for range tt.CurrentTicker.C {
1931
log.Printf("Taking image at triggered interval")
20-
err := tt.Store.StoreImage(tt.ImageCapturer.CaptureImage)
32+
err := tt.Store.StoreImage(tt.CaptureImage)
2133
if err != nil {
22-
log.Printf("Error storing image")
34+
log.Printf("Error storing image: %s", err.Error())
2335
}
2436
}
2537
}()
2638

27-
tt.Store.StoreImage(tt.ImageCapturer.CaptureImage)
39+
err := tt.Store.StoreImage(tt.CaptureImage)
40+
if err != nil {
41+
log.Printf("Error storing image: %s", err.Error())
42+
}
43+
44+
}
45+
46+
func (c *TimelapseCamera) CaptureImage(cameraSettings *CameraSettings, w io.Writer) {
47+
log.Printf("Camera settings for capture: %+v", cameraSettings)
48+
s := raspicam.NewStill()
49+
s.Camera.VFlip = cameraSettings.VFlip
50+
s.Camera.HFlip = cameraSettings.HFlip
51+
s.Camera.Rotation = cameraSettings.Rotation
52+
s.Width = cameraSettings.Width
53+
s.Height = cameraSettings.Height
54+
s.Encoding = raspicam.EncodingPNG
55+
56+
errCh := make(chan error)
57+
go func() {
58+
for x := range errCh {
59+
fmt.Fprintf(os.Stderr, "%v\n", x)
60+
}
61+
}()
62+
c.Mutex.Lock()
63+
raspicam.Capture(s, w, errCh)
64+
c.Mutex.Unlock()
2865
}

0 commit comments

Comments
 (0)