diff options
-rw-r--r-- | README.md | 47 | ||||
-rw-r--r-- | config/default.json | 8 | ||||
-rw-r--r-- | src/camera.go | 7 | ||||
-rw-r--r-- | src/main.go | 72 | ||||
-rw-r--r-- | src/runtime_config.go | 62 | ||||
-rw-r--r-- | src/transport.go | 125 | ||||
-rw-r--r-- | src/web.go | 8 |
7 files changed, 266 insertions, 63 deletions
@@ -3,3 +3,50 @@ A software to take beautiful pictures of birds with a Raspberry Pi Camera. +# Usage + +## Prepare the Target Fileserver + +TODO + +Some notes: +``` +ssh-keygen -y -f ~/.ssh/id_rsa # generates and prints public key of this private one +sudo mkdir /srv/birdscan +sudo chown -R birdscan:birdscan /srv/birdscan +sudo chmod -R 700 /srv/birdscan +sudo setfacl -m u:myuser:rwx /srv/birdscan +``` + +## Install and configure birdscan on main Server + +Install and configure birdscan in these few steps on an Arch Linux system: + +``` +mkdir -p ~/ABS # create a directory for the arch build system +cd ~/ABS +git clone https://aur.archlinux.org/python-picamera.git # download picamera from AUR +cd picamera +makepkg -si # build and install picamera dependency +mkdir ../birdscan +cd ../birdscan +# (download PKGBUILD for birdscan to current directory FIXME: insert link here) +makepkg -si # build and install birdscan +sudo ssh-keyscan -t rsa <fileserver_ip_or_host> >> /etc/ssh/ssh_known_hosts # add host key of fileserver +sudo vim /etc/birdscan/config.json # edit config file +sudo systemctl enable --now birdscan # start and enable birdscan +``` + +Change the configuration like this: + +``` +sudo vim /etc/birdscan/config.json +sudo systemctl restart birdscan +``` + +If something does not work you can get a live log of birdscan like this: + +``` +journalctl -fu birdscan +``` + diff --git a/config/default.json b/config/default.json index 6288daf..be86cfb 100644 --- a/config/default.json +++ b/config/default.json @@ -2,5 +2,13 @@ "webserver":{ "bind_address":"127.0.0.1", "bind_port":"8080" + }, + "file_transport":{ + "enabled":false, + "local_path_to_ssh_private_key":"/dev/null", + "fileserver_host_or_ip":"127.0.0.1", + "fileserver_ssh_port":"22", + "remote_user":"birdscan", + "remote_path_to_target_directory":"/srv/birdscan/" } } diff --git a/src/camera.go b/src/camera.go index cc29f54..e836cf3 100644 --- a/src/camera.go +++ b/src/camera.go @@ -50,7 +50,7 @@ func singlePicture(m *Machine) { // create command var cmd *exec.Cmd - if !debug { + if !config.Flag.Debug { cmd = exec.Command("python3", "/usr/lib/python3.9/site-packages/birdscan/") } else { cmd = exec.Command("python3", "../python/birdscan", "--debug") @@ -68,7 +68,7 @@ func singlePicture(m *Machine) { if err != nil { log.Print(err) } - data,err := io.ReadAll(stdout) + _,err = io.ReadAll(stdout) if err != nil { log.Print(err) } @@ -78,8 +78,9 @@ func singlePicture(m *Machine) { } // process result - log.Println(string(data)) m.SendEvent("single_picture_taken") + log.Println("Picture taken. Sending transport_request ...") + transporter.statemachine.SendEvent("transport_request") } func (cam *Camera) run() { diff --git a/src/main.go b/src/main.go index 01581ff..572e5f9 100644 --- a/src/main.go +++ b/src/main.go @@ -3,77 +3,37 @@ package main import ( - "flag" "log" - "os" - "io/ioutil" - "encoding/json" ) var ( + config RuntimeConfig camera Camera - debug bool + transporter Transporter ) -type config struct { - WebConfig webConfig `json:"webserver"` -} - func main() { - // read command line arguments - configPath := readFlags() + // disable log timestamp because systemd takes care of that + log.SetFlags(0) - // set up log and print startup message - log.SetFlags(0) // disable timestamp because systemd takes care of that - log.Println("Starting birdscan") + // parse flags and read config + config = GetRuntimeConfig() - // read config file - cfg := readConfig(configPath) + // print startup message + log.Println("Starting birdscan") - // setup camera state machine + // create camera camera = NewCamera() + // setup transporter + transporter = NewTransporter() + go transporter.Run() // daemon to copy files via rsync + // start goroutines - server := NewWebServer(&cfg.WebConfig) - go server.run() // http server + server := NewWebServer() + go server.run() // http server / user interface - // run camera state machine + // run camera camera.run() } - -func readFlags() string { - var retval string - flag.StringVar(&retval, "c", "/etc/birdscan/config.json", "Path to birdscan configuration file") - flag.BoolVar(&debug, "d", false, "A debug flag to be used by source repository Makefile") - flag.Parse() - return retval -} - -func readConfig(path string) config { - - log.Printf("Reading config file %s", path) - var retval config - - // open the config file - configFile, err := os.Open(path) - defer configFile.Close() - if err != nil { - log.Fatalf("Could not open configuration file %s", path) - } - - // read byte content - byteData, err := ioutil.ReadAll(configFile) - if err != nil { - log.Fatalf("Could not read configuration file %s", path) - } - - // parse content to config structs - err = json.Unmarshal(byteData, &retval) - if err != nil { - log.Fatalf("Could not parse configuration file %s", path) - } - - return retval -} - diff --git a/src/runtime_config.go b/src/runtime_config.go new file mode 100644 index 0000000..07e4a7c --- /dev/null +++ b/src/runtime_config.go @@ -0,0 +1,62 @@ +// vim: shiftwidth=4 tabstop=4 noexpandtab + +package main + +import ( + "log" + "io/ioutil" + "os" + "flag" + "encoding/json" +) + +type RuntimeConfig struct { + Flag FlagConfig + Web WebConfig `json:"webserver"` + Transport TransportConfig `json:"file_transport"` +} + +func GetRuntimeConfig() RuntimeConfig { + + retval := RuntimeConfig{} + + // read CLI parameters + retval.Flag.read() + + // open the config file + configFile, err := os.Open(retval.Flag.ConfigPath) + defer configFile.Close() + if err != nil { + log.Fatalf("Could not open configuration file %s", retval.Flag.ConfigPath) + } + + // read byte content + byteData, err := ioutil.ReadAll(configFile) + if err != nil { + log.Fatalf("Could not read configuration file %s", retval.Flag.ConfigPath) + } + + // parse content to config structs + err = json.Unmarshal(byteData, &retval) + if err != nil { + log.Fatalf("Could not parse configuration file %s", retval.Flag.ConfigPath) + } + + // patch default config in case of debugging + if retval.Flag.Debug { + retval.Transport.Enabled = true + } + + return retval +} + +type FlagConfig struct { + ConfigPath string + Debug bool +} + +func (flagConfig *FlagConfig) read() { + flag.StringVar(&(flagConfig.ConfigPath), "c", "/etc/birdscan/config.json", "Path to birdscan configuration file") + flag.BoolVar(&(flagConfig.Debug), "d", false, "A debug flag to be used by source repository Makefile") + flag.Parse() +} diff --git a/src/transport.go b/src/transport.go new file mode 100644 index 0000000..3e91355 --- /dev/null +++ b/src/transport.go @@ -0,0 +1,125 @@ +// vim: shiftwidth=4 tabstop=4 noexpandtab + +package main + +import ( + "time" + "log" + "os/exec" + "fmt" + "io" +) + +type Transporter struct { + statemachine Machine +} + +type TransportConfig struct { + Enabled bool `json:"enabled"` + LocalPathToSshPrivateKey string `json:"local_path_to_ssh_private_key"` + FileserverHostOrIp string `json:"fileserver_host_or_ip"` + FileserverSshPort string `json:"fileserver_ssh_port"` + RemoteUser string `json:"remote_user"` + RemotePathToTargetDirectory string `json:"remote_path_to_target_directory"` +} + +func NewTransporter() Transporter { + return Transporter{ + statemachine: Machine{ + name: "transporter", + initial: "idle", + states: StateMap{ + "idle": MachineState{ // nothing to transport + on: TransitionMap{ + "transport_request": MachineTransition{ + to: "transport", + }, + }, + }, + "transport": MachineState{ // rsync process for transport is running + on: TransitionMap{ + "transport_request": MachineTransition{ + to: "transport_queue", + }, + "transport_finished": MachineTransition{ + to: "idle", + }, + }, + }, + "transport_queue": MachineState{ // like transport but also a pending transport request + on: TransitionMap{ + "transport_finished": MachineTransition{ + to: "transport", + }, + }, + }, + }, + api: make(chan string), + state_listeners: make([]*(chan string), 0), + hook: runTransporterHooks, + }, + } +} + +func (transp *Transporter) Run() { + transp.statemachine.Run() +} + +func runTransporterHooks(last string, next string, m *Machine) { + if last == "idle" && next == "transport" { + go transportData(m) + } + if last == "transport_queue" && next == "transport" { + go transportData(m) + } +} + +func transportData(m *Machine) { + if config.Transport.Enabled { + if !config.Flag.Debug { + + // generate command string from config + cmdString := fmt.Sprintf( + "\"rsync --remove-source-files -rltgoDv -e 'ssh -p %s -i %s' /var/lib/birdscan/ %s@%s:%s\"", + config.Transport.FileserverSshPort, + config.Transport.LocalPathToSshPrivateKey, + config.Transport.RemoteUser, + config.Transport.FileserverHostOrIp, + config.Transport.RemotePathToTargetDirectory, + ) + + // create command + cmd := exec.Command( + "/bin/bash", + "-c", + cmdString, + ) + log.Printf("Executing: '%s'", cmd.String()) + + // get stderr + stderr,err := cmd.StderrPipe() + if err != nil { + log.Print(err) + } + defer stderr.Close() + + // execute command and fetch stderr + err = cmd.Start() + if err != nil { + log.Print(err) + } + stderrData,err := io.ReadAll(stderr) + if err != nil { + log.Print(err) + } + err = cmd.Wait() + if err != nil { + log.Print(err) + log.Println(string(stderrData)) + } + } else { + time.Sleep(8 * time.Second) + } + } + m.SendEvent("transport_finished") +} @@ -16,17 +16,17 @@ const ( ) type WebServer struct { - config *webConfig + config *WebConfig } -type webConfig struct { +type WebConfig struct { BindAddress string `json:"bind_address"` BindPort string `json:"bind_port"` } -func NewWebServer(cfg *webConfig) WebServer { +func NewWebServer() WebServer { server := WebServer{} - server.config = cfg + server.config = &(config.Web) return server } |