Skip to content

Commit

Permalink
Add timelapse functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
TickleThePanda committed Mar 31, 2021
1 parent 07ce624 commit acbb42a
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 87 deletions.
28 changes: 0 additions & 28 deletions rpi-timelapse/src/capture.go

This file was deleted.

38 changes: 18 additions & 20 deletions rpi-timelapse/src/main.go
@@ -1,8 +1,8 @@
package main

import (
"log"
"os"
"time"
)

func main() {
Expand All @@ -11,6 +11,10 @@ func main() {
if siteRoot == "" {
siteRoot = ""
}
sharedAssets := os.Getenv("RPI_CAMERA_SHARED_ASSETS_SITE")
if siteRoot == "" {
sharedAssets = ""
}

storageDirectory := os.Getenv("RPI_CAMERA_STORAGE_DIR")
if storageDirectory == "" {
Expand All @@ -21,32 +25,26 @@ func main() {
StorageDirectory: storageDirectory,
}

capturer := &ImageCapturer{}

timelapse := &TimelapseSettings{
Name: "test",
Interval: (time.Duration(30) * time.Second),
Camera: CameraSettings{
HFlip: false,
VFlip: false,
Width: 640,
Height: 480,
},
}

timelapseCamera := &TimelapseCamera{
ImageCapturer: capturer,
Store: store,
Store: store,
}

store.Init()
store.SetCurrentTimelapse(timelapse)

timelapseCamera.StartTimelapse(timelapse)
timelapse, err := store.GetCurrentTimelapse()
if err != nil {
log.Printf("Unable to get current timelapse: %s", err.Error())
} else {
log.Printf("Starting existing timelapse %+v", timelapse)
go timelapseCamera.StartTimelapse(timelapse)
}

handleRequests(
siteRoot,
&SiteInfo{
SiteRoot: siteRoot,
SharedAssetsSite: sharedAssets,
},
store,
capturer,
timelapseCamera,
)
}
129 changes: 110 additions & 19 deletions rpi-timelapse/src/server.go
Expand Up @@ -2,20 +2,62 @@ package main

import (
"fmt"
"html/template"
"log"
"net/http"
"strings"
"strconv"
"time"

"github.com/gorilla/mux"
)

type ImageResultHander struct {
Store *TimelapseStore
Capturer *ImageCapturer
SiteInfo *SiteInfo
Store *TimelapseStore
Camera *TimelapseCamera
Templates *Templates
}

type Templates struct {
Index *template.Template
Images *template.Template
}

type SiteInfo struct {
SiteRoot string
SharedAssetsSite string
}

type ImageResponseData struct {
Timestamp string
}

type ImagesPageResponseData struct {
Images []ImageResponseData
SiteInfo *SiteInfo
}

type IndexPageResponseData struct {
SiteInfo *SiteInfo
}

var DEFAULT_CAMERA_SETTINGS CameraSettings = CameraSettings{
HFlip: false,
VFlip: false,
Width: 1600,
Height: 1080,
Rotation: 270,
}

var NOW_CAMERA_SETTINGS CameraSettings = CameraSettings{
HFlip: false,
VFlip: false,
Width: 320,
Height: 240,
Rotation: 270,
}

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

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

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

builder.WriteString("<ul>")
builder.WriteString("<li><a href=\"latest/\">latest</a>")
for _, n := range names {
fmt.Fprintf(&builder, "<li><a href=\"%v/\">%v</a>", n, n)
var images []ImageResponseData

for _, name := range names {
images = append(images, ImageResponseData{
Timestamp: name,
})
}
builder.WriteString("</ul>")

w.Write([]byte(builder.String()))
ih.Templates.Images.Execute(w, ImagesPageResponseData{
Images: images,
SiteInfo: ih.SiteInfo,
})

}

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

name := v["imageName"]

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

w.WriteHeader(http.StatusOK)

ih.Store.ImageByName(name, w)
}

func handleRequests(siteRoot string, store *TimelapseStore, capturer *ImageCapturer) {
func (ih *ImageResultHander) GetCurrentImage(w http.ResponseWriter, r *http.Request) {

w.Header().Set("Content-Type", "image/png")

w.WriteHeader(http.StatusOK)

ih.Camera.CaptureImage(&NOW_CAMERA_SETTINGS, w)
}

func handleRequests(siteInfo *SiteInfo, store *TimelapseStore, capturer *TimelapseCamera) {

templates := &Templates{
Index: template.Must(template.ParseFiles("./src/templates/index.html")),
Images: template.Must(template.ParseFiles("./src/templates/images.html")),
}

handler := &ImageResultHander{
Store: store,
Capturer: capturer,
SiteInfo: siteInfo,
Store: store,
Camera: capturer,
Templates: templates,
}

rootRoute := mux.NewRouter()
rootRoute.HandleFunc(siteRoot+"/images/", handler.GetImageNamePage)
rootRoute.HandleFunc(siteRoot+"/images/latest/", handler.GetLatestImage)
rootRoute.HandleFunc(siteRoot+"/images/{imageName}/", handler.GetImageByName)
rootRoute.HandleFunc(siteInfo.SiteRoot+"/images/", handler.GetImageNamePage)
rootRoute.HandleFunc(siteInfo.SiteRoot+"/images/latest/", handler.GetLatestImage)
rootRoute.HandleFunc(siteInfo.SiteRoot+"/images/now/", handler.GetCurrentImage)
rootRoute.HandleFunc(siteInfo.SiteRoot+"/images/{imageName}/", handler.GetImageByName)
rootRoute.
Path(siteInfo.SiteRoot + "/").
Methods("GET").
HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
templates.Index.Execute(rw, IndexPageResponseData{
SiteInfo: siteInfo,
})
})
rootRoute.
Path(siteInfo.SiteRoot + "/").
Methods("POST").
HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

interval, err := strconv.Atoi(r.FormValue("timelapse-interval"))
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(rw, "Interval was not a number")
return
}

http.Redirect(rw, r, siteInfo.SiteRoot+"/", http.StatusFound)

timelapse := &TimelapseSettings{
Name: r.FormValue("timelapse-name"),
Interval: time.Duration(interval) * time.Second,
Camera: DEFAULT_CAMERA_SETTINGS,
}

store.SetCurrentTimelapse(timelapse)
capturer.StartTimelapse(timelapse)
})

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

Expand Down
24 changes: 24 additions & 0 deletions rpi-timelapse/src/templates/images.html
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<title>Raspberry Pi Timelapse Camera - Images</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ .SiteInfo.SiteRoot }}/static/speed-test.css">
<link rel="stylesheet" href="{{ .SiteInfo.SharedAssetsSite }}/main.css">
</head>
<body>
<main class="content">
<a href="{{ .SiteInfo.SiteRoot }}/">Back</a>
<h1>Images</h1>

<a href="{{ .SiteInfo.SiteRoot }}/images/latest/">Latest</a>
<ul>
{{range .Images}}
<li><a href="{{$.SiteInfo.SiteRoot}}/images/{{.Timestamp}}/">{{.Timestamp}}</a>
{{end}}
</ul>

</main>
</body>
</html>
29 changes: 29 additions & 0 deletions rpi-timelapse/src/templates/index.html
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<title>Raspberry Pi Timelapse Camera</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ .SiteInfo.SiteRoot }}/static/speed-test.css">
<link rel="stylesheet" href="{{ .SiteInfo.SharedAssetsSite }}/main.css">
</head>
<body>
<main class="content">
<h1>Raspberry Pi Timelapse Camera</h1>
<h2>New timelapse</h2>
<form action="." method="post">
<label for="timelapse-name">Name</label>
<input type="text" name="timelapse-name" id="timelapse-name"></input>
<label for="timelapse-interval">Interval (seconds)</label>
<input type="number" step="1" min="0" id="timelapse-interval" name="timelapse-interval"></input>
<button type="submit">Create</button>
</form>

<h2>Current timelapse</h2>
<p><a href="{{ .SiteInfo.SiteRoot }}/images/">Current timelapse images</a>

<h2>Now</h2>
<img src="{{ .SiteInfo.SiteRoot }}/images/now/">
</main>
</body>
</html>
45 changes: 41 additions & 4 deletions rpi-timelapse/src/timelapse-camera.go
@@ -1,28 +1,65 @@
package main

import (
"fmt"
"io"
"log"
"os"
"sync"
"time"

"github.com/dhowden/raspicam"
)

type TimelapseCamera struct {
Store *TimelapseStore
ImageCapturer *ImageCapturer
CurrentTicker *time.Ticker
Mutex sync.Mutex
}

func (tt *TimelapseCamera) StartTimelapse(t *TimelapseSettings) {

if tt.CurrentTicker != nil {
tt.CurrentTicker.Stop()
tt.CurrentTicker = nil
}

log.Printf("Starting timelapse ticker with interval %v", t.Interval)
tt.CurrentTicker = time.NewTicker(t.Interval)
go func() {
for range tt.CurrentTicker.C {
log.Printf("Taking image at triggered interval")
err := tt.Store.StoreImage(tt.ImageCapturer.CaptureImage)
err := tt.Store.StoreImage(tt.CaptureImage)
if err != nil {
log.Printf("Error storing image")
log.Printf("Error storing image: %s", err.Error())
}
}
}()

tt.Store.StoreImage(tt.ImageCapturer.CaptureImage)
err := tt.Store.StoreImage(tt.CaptureImage)
if err != nil {
log.Printf("Error storing image: %s", err.Error())
}

}

func (c *TimelapseCamera) CaptureImage(cameraSettings *CameraSettings, w io.Writer) {
log.Printf("Camera settings for capture: %+v", cameraSettings)
s := raspicam.NewStill()
s.Camera.VFlip = cameraSettings.VFlip
s.Camera.HFlip = cameraSettings.HFlip
s.Camera.Rotation = cameraSettings.Rotation
s.Width = cameraSettings.Width
s.Height = cameraSettings.Height
s.Encoding = raspicam.EncodingPNG

errCh := make(chan error)
go func() {
for x := range errCh {
fmt.Fprintf(os.Stderr, "%v\n", x)
}
}()
c.Mutex.Lock()
raspicam.Capture(s, w, errCh)
c.Mutex.Unlock()
}

0 comments on commit acbb42a

Please sign in to comment.