Merged in raspistill (pull request #449)

device: add raspistill package housing release and testing implementations

Approved-by: Trek Hopton
This commit is contained in:
Saxon Milton 2021-01-27 03:19:39 +00:00
commit 9db5e7acb3
9 changed files with 638 additions and 101 deletions

View File

@ -98,7 +98,7 @@ func Lex(dst io.Writer, src io.Reader, delay time.Duration) error {
if nImg == 0 {
<-tick
Log.Debug("writing buf","len(buf)",len(buf))
Log.Debug("writing buf", "len(buf)", len(buf))
_, err = dst.Write(buf)
if err != nil {
return err

View File

@ -0,0 +1,138 @@
// +build !test
/*
DESCRIPTION
release.go provides implementations for the Raspistill struct for release
conditions, i.e. we're running on a raspberry pi with access to the actual
raspistill utility with a pi camera connected. The code here runs a raspistill
background process and reads captured images from the camera.
AUTHORS
Saxon Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package raspistill
import (
"errors"
"fmt"
"io"
"bufio"
"os/exec"
"strings"
"bitbucket.org/ausocean/av/revid/config"
)
type raspistill struct {
cfg config.Config
cmd *exec.Cmd
out io.ReadCloser
log config.Logger
done chan struct{}
isRunning bool
}
func new(l config.Logger) raspistill {
return raspistill{
log: l,
done: make(chan struct{}),
}
}
func (r *Raspistill) stop() error {
if r.isRunning == false {
return nil
}
close(r.done)
if r.cmd == nil || r.cmd.Process == nil {
return errors.New("raspistill process was never started")
}
err := r.cmd.Process.Kill()
if err != nil {
return fmt.Errorf("could not kill raspistill process: %w", err)
}
r.isRunning = false
return r.out.Close()
}
func (r *Raspistill) start() error {
args := []string{
"--output", "-",
"--nopreview",
"--width", fmt.Sprint(r.cfg.Width),
"--height", fmt.Sprint(r.cfg.Height),
"--rotation", fmt.Sprint(r.cfg.Rotation),
"--timeout", fmt.Sprint(r.cfg.TimelapseDuration),
"--timelapse", fmt.Sprint(r.cfg.TimelapseInterval),
"--quality", fmt.Sprint(r.cfg.JPEGQuality),
}
r.log.Info(pkg+"raspistill args", "args", strings.Join(args, " "))
r.cmd = exec.Command("raspistill", args...)
var err error
r.out, err = r.cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("could not pipe command output: %w", err)
}
stderr, err := r.cmd.StderrPipe()
if err != nil {
return fmt.Errorf("could not pipe command error: %w", err)
}
go func() {
errScnr := bufio.NewScanner(stderr)
for {
select {
case <-r.done:
r.log.Info("raspistill.Stop() called, finished checking stderr")
return
default:
}
if errScnr.Scan() {
r.log.Error("error line from raspistill stderr","error",errScnr.Text())
continue
}
err := errScnr.Err()
if err != nil {
r.log.Error("error from stderr scan","error",err)
}
}
}()
err = r.cmd.Start()
if err != nil {
return fmt.Errorf("could not start raspistill process: %w", err)
}
r.isRunning = true
return nil
}
func (r *Raspistill) read(p []byte) (int, error) {
if r.out != nil {
return r.out.Read(p)
}
return 0, errNotStarted
}

View File

@ -0,0 +1,175 @@
// +build test
/*
DESCRIPTION
test.go provides test implementations of the raspistill methods when the
"test" build tag is specified. In this mode, raspistill simply provides
specific test JPEG images when Raspistill.Read() is called.
AUTHORS
Saxon Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package raspistill
import (
"io"
"io/ioutil"
"path/filepath"
"strconv"
"sync"
"time"
"bitbucket.org/ausocean/av/revid/config"
)
const (
// TODO(Saxon): find nImages programmatically ?
nImages = 6
imgPath = "../../../test/test-data/av/input/jpeg/"
jpgExt = ".jpg"
)
type raspistill struct {
images [nImages][]byte
imgCnt int // Number of images that have been loaded thus far.
durTicker *time.Ticker // Tracks timelapse duration.
intvlTicker *time.Ticker // Tracks current interval in the timelapse.
log config.Logger
cfg config.Config
isRunning bool
buf []byte // Holds frame data to be read.
term chan struct{} // Signals termination when close() is called.
mu sync.Mutex
}
func new(l config.Logger) raspistill {
l.Debug("creating new test raspistill input")
r := raspistill{log: l}
// Load the test images into the images slice.
// We expect the 6 images test images to be named 0.jpg through to 5.jpg.
r.log.Debug("loading test JPEG images")
for i, _ := range r.images {
absPath, err := filepath.Abs(imgPath)
path := absPath + "/" + strconv.Itoa(i) + jpgExt
r.images[i], err = ioutil.ReadFile(path)
if err != nil {
r.log.Fatal("error loading test image", "imageNum", i, "error", err)
}
r.log.Debug("image loaded", "path/name", path, "size", len(r.images[i]))
}
return r
}
// stop sets isRunning flag to false, indicating no further captures. Future
// calls on Raspistill.read will return an error.
func (r *Raspistill) stop() error {
r.log.Debug("stopping test raspistill")
r.setRunning(false)
return nil
}
// start creates and starts the timelapse and duration tickers and sets
// isRunning flag to true indicating that raspistill is capturing.
func (r *Raspistill) start() error {
r.log.Debug("starting test raspistill")
r.durTicker = time.NewTicker(r.cfg.TimelapseDuration)
r.intvlTicker = time.NewTicker(r.cfg.TimelapseInterval)
r.term = make(chan struct{})
r.loadImg()
go r.capture()
r.setRunning(true)
return nil
}
func (r *Raspistill) loadImg() {
r.log.Debug("appending new image on to buffer and copying next image p", "nImg", r.imgCnt)
imgBytes := r.images[r.imgCnt%nImages]
if len(imgBytes) == 0 {
panic("length of image bytes should not be 0")
}
r.imgCnt++
r.mu.Lock()
r.buf = append(r.buf, imgBytes...)
r.log.Debug("added image to buf", "nBytes", len(imgBytes))
r.mu.Unlock()
}
func (r *Raspistill) capture() {
for {
select {
case t := <-r.intvlTicker.C:
r.log.Debug("got interval tick", "tick", t)
r.loadImg()
r.intvlTicker.Reset(r.cfg.TimelapseInterval)
case <-r.term:
r.setRunning(false)
return
case t := <-r.durTicker.C:
r.log.Debug("got duration tick, timelapse over", "tick", t)
r.buf = nil
return
}
}
}
// read blocks until either another timelapse interval has completed, in which
// case we provide the next jpeg to p, or, the timelapse duration has completed
// in which case we don't read and provide io.EOF error.
func (r *Raspistill) read(p []byte) (int, error) {
r.log.Debug("reading from test raspistill")
if !r.running() {
return 0, errNotStarted
}
r.mu.Lock()
defer r.mu.Unlock()
if r.buf == nil {
return 0, io.EOF
}
n := copy(p, r.buf)
r.buf = r.buf[n:]
return n, nil
}
func (r *Raspistill) setRunning(s bool) {
r.mu.Lock()
r.isRunning = s
r.mu.Unlock()
}
func (r *Raspistill) running() bool {
r.mu.Lock()
ir := r.isRunning
r.mu.Unlock()
return ir
}

View File

@ -0,0 +1,139 @@
/*
DESCRIPTION
raspistill.go provides an implementation of the AVDevice interface for the
raspistill raspberry pi camera interfacing utility. This allows for the
capture of single frames over time, i.e. a timelapse form of capture.
AUTHORS
Saxon Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
// Package raspistill rovides an implementation of the AVDevice interface for the
// raspistill raspberry pi camera interfacing utility. This allows for the
// capture of single frames over time, i.e. a timelapse form of capture.
package raspistill
import (
"errors"
"fmt"
"time"
"bitbucket.org/ausocean/av/device"
"bitbucket.org/ausocean/av/revid/config"
)
// To indicate package when logging.
const pkg = "raspistill: "
// Config field validation bounds.
const (
minTimelapseDuration = 30 * time.Second // s
maxTimelapseDuration = 86400 * time.Second // s = 24 hours
minTimelapseInterval = 1 * time.Second // s
maxTimelapseInterval = 86400 * time.Second // s = 24 hours
)
// Raspistill configuration defaults.
const (
defaultRotation = 0 // degrees
defaultWidth = 1280 // pixels
defaultHeight = 720 // pixels
defaultJPEGQuality = 75 // %
defaultTimelapseDuration = maxTimelapseDuration // ms
defaultTimelapseInterval = 3600 * time.Second // ms
)
// Configuration errors.
var (
errBadRotation = fmt.Errorf("Rotation bad or unset, defaulting to: %v", defaultRotation)
errBadWidth = fmt.Errorf("Width bad or unset, defaulting to: %v", defaultWidth)
errBadHeight = fmt.Errorf("Height bad or unset, defaulting to: %v", defaultHeight)
errBadJPEGQuality = fmt.Errorf("JPEGQuality bad or unset, defaulting to: %v", defaultJPEGQuality)
errBadTimelapseDuration = fmt.Errorf("TimelapseDuration bad or unset, defaulting to: %v", defaultTimelapseDuration)
errBadTimelapseInterval = fmt.Errorf("TimelapseInterval bad or unset, defaulting to: %v", defaultTimelapseInterval)
)
// Misc errors.
var errNotStarted = errors.New("cannot read, raspistill not started")
// Raspistill is an implementation of AVDevice that provides control over the
// raspistill utility for using the raspberry pi camera for the capture of
// singular images.
type Raspistill struct{ raspistill }
// New returns a new Raspistill.
func New(l config.Logger) *Raspistill { return &Raspistill{raspistill: new(l)} }
// Start will prepare the arguments for the raspistill command using the
// configuration set using the Set method then call the raspistill command,
// piping the image output from which the Read method will read from.
func (r *Raspistill) Start() error { return r.start() }
// Read implements io.Reader. Calling read before Start has been called will
// result in 0 bytes read and an error.
func (r *Raspistill) Read(p []byte) (int, error) { return r.read(p) }
// Stop will terminate the raspistill process and close the output pipe.
func (r *Raspistill) Stop() error { return r.stop() }
// IsRunning is used to determine if the pi's camera is running.
func (r *Raspistill) IsRunning() bool { return r.isRunning }
// Name returns the name of the device.
func (r *Raspistill) Name() string { return "Raspistill" }
// Set will take a Config struct, check the validity of the relevant fields
// and then performs any configuration necessary. If fields are not valid,
// an error is added to the multiError and a default value is used.
func (r *Raspistill) Set(c config.Config) error {
var errs device.MultiError
if c.Rotation > 359 || c.Rotation < 0 {
c.Rotation = defaultRotation
errs = append(errs, errBadRotation)
}
if c.Width == 0 {
c.Width = defaultWidth
errs = append(errs, errBadWidth)
}
if c.Height == 0 {
c.Height = defaultHeight
errs = append(errs, errBadHeight)
}
if c.JPEGQuality < 0 || c.JPEGQuality > 100 {
c.JPEGQuality = defaultJPEGQuality
errs = append(errs, errBadJPEGQuality)
}
if c.TimelapseDuration > maxTimelapseDuration || c.TimelapseDuration < minTimelapseDuration {
c.TimelapseDuration = defaultTimelapseDuration
errs = append(errs, errBadTimelapseDuration)
}
if c.TimelapseInterval > maxTimelapseInterval || c.TimelapseInterval < minTimelapseInterval {
c.TimelapseInterval = defaultTimelapseInterval
errs = append(errs, errBadTimelapseInterval)
}
r.cfg = c
return errs
}

View File

@ -35,6 +35,11 @@ import (
type Logger interface {
SetLevel(int8)
Log(level int8, message string, params ...interface{})
Debug(msg string, params ...interface{})
Info(msg string, params ...interface{})
Warning(msg string, params ...interface{})
Error(msg string, params ...interface{})
Fatal(msg string, params ...interface{})
}
// Enums to define inputs, outputs and codecs.
@ -45,6 +50,7 @@ const (
// Input/Output.
InputFile
InputRaspivid
InputRaspistill
InputV4L
InputRTSP
InputAudio
@ -55,10 +61,12 @@ const (
OutputHTTP
OutputMPEGTS
OutputFile
OutputFiles
// Codecs.
H264
H265
MJPEG
JPEG
)
@ -142,7 +150,9 @@ type Config struct {
//
// Valid values are defined by enums:
// InputRaspivid:
// Read data from a Raspberry Pi Camera.
// Use raspivid utility to capture video from Raspberry Pi Camera.
// InputRaspistill:
// Use raspistill utility to capture images from the Raspberry Pi Camera.
// InputV4l:
// Read from webcam.
// InputFile:
@ -198,7 +208,7 @@ type Config struct {
// Outputs define the outputs we wish to output data too.
//
// Valid outputs are defined by enums:
// OutputFile:
// OutputFile & OutputFiles:
// Location must be defined by the OutputPath field. MPEG-TS packetization
// is used.
// OutputHTTP:
@ -215,6 +225,7 @@ type Config struct {
PSITime uint // Sets the time between a packet being sent.
Quantization uint // Quantization defines the quantization level, which will determine variable bitrate quality in the case of input from the Pi Camera.
RBCapacity uint // The number of bytes the ring buffer will occupy.
RBStartElementSize uint // The starting element size of the ring buffer from which element size will increase to accomodate frames.
RBWriteTimeout uint // The ringbuffer write timeout in seconds.
RecPeriod float64 // How many seconds to record at a time.
Rotation uint // Rotation defines the video rotation angle in degrees Raspivid input.
@ -222,7 +233,22 @@ type Config struct {
RTPAddress string // RTPAddress defines the RTP output destination.
SampleRate uint // Samples a second (Hz).
Saturation int
// JPEGQuality is a value 0-100 inclusive, controlling JPEG compression of the
// timelapse snaps. 100 represents minimal compression and 0 represents the most
// compression.
JPEGQuality int
Suppress bool // Holds logger suppression state.
// TimelapseInterval defines the interval between timelapse images when using
// raspistill input.
TimelapseInterval time.Duration
// TimelapseDuration defines the duration of timelapse i.e. duration over
// which all snaps are taken, when using raspistill input.
TimelapseDuration time.Duration
VBRBitrate uint // VBRBitrate describes maximal variable bitrate.
// VBRQuality describes the general quality of video from the GeoVision camera

View File

@ -37,6 +37,11 @@ type dumbLogger struct{}
func (dl *dumbLogger) Log(l int8, m string, a ...interface{}) {}
func (dl *dumbLogger) SetLevel(l int8) {}
func (dl *dumbLogger) Debug(msg string, args ...interface{}) {}
func (dl *dumbLogger) Info(msg string, args ...interface{}) {}
func (dl *dumbLogger) Warning(msg string, args ...interface{}) {}
func (dl *dumbLogger) Error(msg string, args ...interface{}) {}
func (dl *dumbLogger) Fatal(msg string, args ...interface{}) {}
func TestValidate(t *testing.T) {
dl := &dumbLogger{}
@ -55,6 +60,7 @@ func TestValidate(t *testing.T) {
PSITime: defaultPSITime,
FileFPS: defaultFileFPS,
RBCapacity: defaultRBCapacity,
RBStartElementSize: defaultRBStartElementSize,
RBWriteTimeout: defaultRBWriteTimeout,
MinFPS: defaultMinFPS,
}

View File

@ -37,7 +37,7 @@ import (
"bitbucket.org/ausocean/utils/logger"
)
// Config map keys.
// Config map Keys.
const (
KeyAutoWhiteBalance = "AutoWhiteBalance"
KeyBitDepth = "BitDepth"
@ -78,6 +78,7 @@ const (
KeyPSITime = "PSITime"
KeyQuantization = "Quantization"
KeyRBCapacity = "RBCapacity"
KeyRBStartElementSize = "RBStartElementSize"
KeyRBWriteTimeout = "RBWriteTimeout"
KeyRecPeriod = "RecPeriod"
KeyRotation = "Rotation"
@ -85,7 +86,10 @@ const (
KeyRTPAddress = "RTPAddress"
KeySampleRate = "SampleRate"
KeySaturation = "Saturation"
KeyJPEGQuality = "JPEGQuality"
KeySuppress = "Suppress"
KeyTimelapseDuration = "TimelapseDuration"
KeyTimelapseInterval = "TimelapseInterval"
KeyVBRBitrate = "VBRBitrate"
KeyVBRQuality = "VBRQuality"
KeyVerticalFlip = "VerticalFlip"
@ -95,6 +99,7 @@ const (
// Config map parameter types.
const (
typeString = "string"
typeInt = "int"
typeUint = "uint"
typeBool = "bool"
typeFloat = "float"
@ -118,6 +123,7 @@ const (
// Ring buffer defaults.
defaultRBCapacity = 50000000 // => 50MB
defaultRBStartElementSize = 1000 // bytes
defaultRBWriteTimeout = 5 // Seconds.
// Motion filter parameter defaults.
@ -264,13 +270,14 @@ var Variables = []struct {
},
{
Name: KeyInput,
Type_: "enum:raspivid,rtsp,v4l,file,audio",
Type_: "enum:raspivid,raspistill,rtsp,v4l,file,audio",
Update: func(c *Config, v string) {
c.Input = parseEnum(
KeyInput,
v,
map[string]uint8{
"raspivid": InputRaspivid,
"raspistill": InputRaspistill,
"rtsp": InputRTSP,
"v4l": InputV4L,
"file": InputFile,
@ -281,7 +288,7 @@ var Variables = []struct {
},
Validate: func(c *Config) {
switch c.Input {
case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP:
case InputRaspivid, InputRaspistill, InputV4L, InputFile, InputAudio, InputRTSP:
default:
c.LogInvalidField(KeyInput, defaultInput)
c.Input = defaultInput
@ -290,7 +297,7 @@ var Variables = []struct {
},
{
Name: KeyInputCodec,
Type_: "enum:H264,H265,MJPEG,PCM,ADPCM",
Type_: "enum:H264,H265,MJPEG,JPEG,PCM,ADPCM",
Update: func(c *Config, v string) {
c.InputCodec = parseEnum(
KeyInputCodec,
@ -299,6 +306,7 @@ var Variables = []struct {
"h264": codecutil.H264,
"h265": codecutil.H265,
"mjpeg": codecutil.MJPEG,
"jpeg": codecutil.JPEG,
"pcm": codecutil.PCM,
"adpcm": codecutil.ADPCM,
},
@ -307,7 +315,7 @@ var Variables = []struct {
},
Validate: func(c *Config) {
switch c.InputCodec {
case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM:
case codecutil.H264, codecutil.MJPEG, codecutil.JPEG, codecutil.PCM, codecutil.ADPCM:
default:
c.LogInvalidField(KeyInputCodec, defaultInputCodec)
c.InputCodec = defaultInputCodec
@ -440,6 +448,8 @@ var Variables = []struct {
switch strings.ToLower(v) {
case "file":
c.Outputs[0] = OutputFile
case "files":
c.Outputs[0] = OutputFiles
case "http":
c.Outputs[0] = OutputHTTP
case "rtmp":
@ -466,6 +476,8 @@ var Variables = []struct {
switch strings.ToLower(output) {
case "file":
c.Outputs[i] = OutputFile
case "files":
c.Outputs[i] = OutputFiles
case "http":
c.Outputs[i] = OutputHTTP
case "rtmp":
@ -501,6 +513,14 @@ var Variables = []struct {
Update: func(c *Config, v string) { c.RBCapacity = parseUint(KeyRBCapacity, v, c) },
Validate: func(c *Config) { c.RBCapacity = lessThanOrEqual(KeyRBCapacity, c.RBCapacity, 0, c, defaultRBCapacity) },
},
{
Name: KeyRBStartElementSize,
Type_: typeUint,
Update: func(c *Config, v string) { c.RBStartElementSize = parseUint("RBStartElementSize", v, c) },
Validate: func(c *Config) {
c.RBStartElementSize = lessThanOrEqual("RBStartElementSize", c.RBStartElementSize, 0, c, defaultRBStartElementSize)
},
},
{
Name: KeyRBWriteTimeout,
Type_: typeUint,
@ -548,7 +568,7 @@ var Variables = []struct {
},
{
Name: KeySaturation,
Type_: "int",
Type_: typeInt,
Update: func(c *Config, v string) {
_v, err := strconv.Atoi(v)
if err != nil {
@ -557,6 +577,17 @@ var Variables = []struct {
c.Saturation = _v
},
},
{
Name: KeyJPEGQuality,
Type_: typeUint,
Update: func(c *Config, v string) {
_v, err := strconv.Atoi(v)
if err != nil {
c.Logger.Log(logger.Warning, "invalid JPEGQuality param", "value", v)
}
c.JPEGQuality = _v
},
},
{
Name: KeySuppress,
Type_: typeBool,
@ -565,6 +596,28 @@ var Variables = []struct {
c.Logger.(*logger.Logger).SetSuppress(c.Suppress)
},
},
{
Name: KeyTimelapseInterval,
Type_: typeUint,
Update: func(c *Config, v string) {
_v, err := strconv.Atoi(v)
if err != nil {
c.Logger.Log(logger.Warning, "invalid TimelapseInterval param", "value", v)
}
c.TimelapseInterval = time.Duration(_v) * time.Second
},
},
{
Name: KeyTimelapseDuration,
Type_: typeUint,
Update: func(c *Config, v string) {
_v, err := strconv.Atoi(v)
if err != nil {
c.Logger.Log(logger.Warning, "invalid TimelapseDuration param", "value", v)
}
c.TimelapseDuration = time.Duration(_v) * time.Second
},
},
{
Name: KeyVBRBitrate,
Type_: typeUint,