diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1ce67e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ + + +configure: + + +build: + + +run-audio-recorder: + + +run-gps-recorder: + + +run-depth-recorder: + + diff --git a/README.md b/README.md index e69de29..5d8140c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,31 @@ +# Hydrophonitor + +A software package to record audio and related metadata from a configuration +of hydrophones. + +## Overview + +Module | Description +-----------------|- +audio-logger | Receive an audio signal from the DAC and write it on disk in `.wav` format. +gps-logger | Record position and time of the device in `.csv` format. +depth-logger | Record depth of the device and save it in `.csv` format. +*lcd-display | Provide information on the device's LCD screen +*device-controls | Provide device control using physical buttons. + +## Data Formats + +Type | Output file format | Output file name | Output structure | Content +------------|--------------------|--------------------------------------|------------------| +Audio Data | .wav | _audio.wav | Each recorded chunk will be written to its own file in `audio` folder | Wav audio data, configuration defined in XXX +GPS Data | .csv | _gps.wav | All data written to a single file | Csv data with following fields: GPS time UTC, latitude, longitude, speed, satellites in view +Depth data | .csv | _depth.wav | All data written to a single file | Csv data with following fields: date and time, voltage of depth sensor (V), depth (m) +Log data | .txt | _log.txt | All data written to a single file | Text file where each entry contains the following: date and time, process that writes the entry, logged information + +## Output Locations + +The base location/path for the output directories is defined by a configurable value BASE_DIR_PATH. If directories along this path do not exist, they will be created. If an error occurs or the location is not writable, output will be written to the default location () instead. + + + +A recording session starts when the Raspberry Pi is turned on or booted, and ends on shutdown. Each session will have its output written in its own directory that will be named _recordings. \ No newline at end of file diff --git a/audio-logger/.gitignore b/audio-logger/.gitignore new file mode 100644 index 0000000..0cd831f --- /dev/null +++ b/audio-logger/.gitignore @@ -0,0 +1,3 @@ +taget +*.wav +recordings \ No newline at end of file diff --git a/audio-logger/Cargo.lock b/audio-logger/Cargo.lock index a7a1b2f..86808a2 100644 --- a/audio-logger/Cargo.lock +++ b/audio-logger/Cargo.lock @@ -51,20 +51,21 @@ dependencies = [ ] [[package]] -name = "audio-logger" +name = "audio" version = "0.1.0" dependencies = [ "alsa", "anyhow", "chrono", "clap", - "cpal", + "cpal 0.13.5", "ctrlc", "hound", "jack 0.9.2", "libc", "nix 0.23.1", "parking_lot 0.12.1", + "rodio", ] [[package]] @@ -104,6 +105,12 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.2.1" @@ -205,6 +212,12 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + [[package]] name = "combine" version = "4.6.6" @@ -255,7 +268,7 @@ dependencies = [ "lazy_static", "libc", "mach", - "ndk", + "ndk 0.6.0", "ndk-glue", "nix 0.23.1", "oboe", @@ -266,6 +279,31 @@ dependencies = [ "winapi", ] +[[package]] +name = "cpal" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d466b47cf0ea4100186a7c12d7d0166813dda7cf648553554c9c39c6324841b" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "jni", + "js-sys", + "libc", + "mach", + "ndk 0.7.0", + "ndk-context", + "nix 0.23.1", + "oboe", + "once_cell", + "parking_lot 0.12.1", + "stdweb", + "thiserror", + "web-sys", + "windows", +] + [[package]] name = "ctrlc" version = "3.2.3" @@ -276,6 +314,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + [[package]] name = "darling" version = "0.13.4" @@ -523,6 +567,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "libc" version = "0.2.133" @@ -598,6 +653,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minimp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" +dependencies = [ + "minimp3-sys", + "slice-deque", + "thiserror", +] + +[[package]] +name = "minimp3-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" +dependencies = [ + "cc", +] + [[package]] name = "ndk" version = "0.6.0" @@ -606,11 +681,25 @@ checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ "bitflags", "jni-sys", - "ndk-sys", + "ndk-sys 0.3.0", "num_enum", "thiserror", ] +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys 0.4.0", + "num_enum", + "raw-window-handle", + "thiserror", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -626,10 +715,10 @@ dependencies = [ "lazy_static", "libc", "log", - "ndk", + "ndk 0.6.0", "ndk-context", "ndk-macro", - "ndk-sys", + "ndk-sys 0.3.0", ] [[package]] @@ -654,6 +743,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "ndk-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d83ec9c63ec5bf950200a8e508bdad6659972187b625469f58ef8c08e29046" +dependencies = [ + "jni-sys", +] + [[package]] name = "nix" version = "0.23.1" @@ -747,7 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ "jni", - "ndk", + "ndk 0.6.0", "ndk-context", "num-derive", "num-traits", @@ -763,6 +861,15 @@ dependencies = [ "cc", ] +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.15.0" @@ -906,6 +1013,15 @@ dependencies = [ "proc-macro2 1.0.43", ] +[[package]] +name = "raw-window-handle" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" +dependencies = [ + "cty", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -930,6 +1046,19 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "rodio" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb10b653d5ec0e9411a2e7d46e2c7f4046fd87d35b9955bd73ba4108d69072b5" +dependencies = [ + "claxon", + "cpal 0.14.0", + "hound", + "lewton", + "minimp3", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -963,6 +1092,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "slice-deque" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" +dependencies = [ + "libc", + "mach", + "winapi", +] + [[package]] name = "smallvec" version = "1.9.0" @@ -1049,6 +1189,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "toml" version = "0.5.9" @@ -1188,17 +1343,30 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] [[package]] @@ -1207,26 +1375,56 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" diff --git a/audio-logger/Cargo.toml b/audio-logger/Cargo.toml index 07a5389..4b7b6ab 100644 --- a/audio-logger/Cargo.toml +++ b/audio-logger/Cargo.toml @@ -1,17 +1,16 @@ [package] -name = "audio-logger" +name = "audio" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -clap = {url = "https://github.com/clap-rs/clap", features = ["derive"]} +clap = { url = "https://github.com/clap-rs/clap", features = ["derive"] } cpal= { version = "0.13.5", features = ["jack"] } anyhow = "1.0.61" hound = "3.4.0" chrono = "0.4.22" ctrlc = "3.2.3" +rodio = "0.16.0" [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))'.dependencies] alsa = "0.6" diff --git a/audio-logger/Makefile b/audio-logger/Makefile index 8cd27c9..aba5ce7 100644 --- a/audio-logger/Makefile +++ b/audio-logger/Makefile @@ -1,9 +1,9 @@ -test: all - bash run_test.sh - all: cargo build --release + +test: all mkdir -p recordings + bash run_test.sh clean: cargo clean @@ -11,7 +11,6 @@ clean: fclean: clean rm -rf recordings -re: fclean - cargo build --release +re: fclean all -.PHONY: all clean re test \ No newline at end of file +.PHONY: all clean fclean re test diff --git a/audio-logger/run_test.sh b/audio-logger/run_test.sh index 51044ee..ebd0a94 100644 --- a/audio-logger/run_test.sh +++ b/audio-logger/run_test.sh @@ -1,6 +1,6 @@ #!/bin/bash -target/release/audio-logger \ +target/release/audio rec \ --name test \ --output recordings/ \ --batch-recording 3 \ diff --git a/audio-logger/src/cli.rs b/audio-logger/src/cli.rs index d4c5e9e..0832a28 100644 --- a/audio-logger/src/cli.rs +++ b/audio-logger/src/cli.rs @@ -1,14 +1,46 @@ -use clap::{Parser, ValueEnum}; +use clap::{Parser, ValueEnum, Subcommand}; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum Hosts { Alsa, Jack, + CoreAudio, + Asio, +} + +#[derive(Subcommand)] +#[clap(about = "A tool to record audio on Linux using the command line.")] +pub enum Commands { + /// Record audio either continously or in batches (in case a possible + /// interruption is an issue) + Rec(Rec), + /// Play audio from a .wav file + Play(Play), + /// Merge audio resulting from a batch recording into a single file + Merge(Merge), + /// Get reports of system's audio resources + Info(Info), } #[derive(Parser, Debug)] -#[clap(about = "A tool to record audio.")] -pub struct Args { +#[clap(about = "Merge audio resulting from a batch recording into a single + file. +")] +pub struct Merge { + /// The directory to look for files to merge. + #[clap(short, long, default_value = "./", required = true)] + pub input: std::path::PathBuf, + + /// The directory to save the merged file to. + #[clap(short, long, default_value = "./")] + pub output: std::path::PathBuf, +} + +#[derive(Parser, Debug)] +#[clap(about = "Record audio either continously or in batches (in case a + possible interruption is an issue). +")] +pub struct Rec { /// Filename will be `[NAME]-yyyy-mm-dd-H:M:S.wav` #[clap(required = true, short, long)] @@ -22,9 +54,9 @@ pub struct Args { #[clap(short, long, value_name = "SECONDS")] pub batch_recording: Option, - /// Output the available devices and their configurations - #[clap(long)] - pub print_configs: bool, + /// (optional) Will record for a total of [SECONDS] + #[clap(short, long, value_name = "MAX_SECONDS")] + pub max_seconds: Option, /// Host API to use #[clap(value_enum)] @@ -42,3 +74,27 @@ pub struct Args { #[clap(long, value_name = "FRAMES")] pub buffer_size: Option, } +#[derive(Parser, Debug)] +#[clap(about = "Play audio from a .wav file. +")] +pub struct Play { + /// Path to the file to play + #[clap(value_parser, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] + pub input: std::path::PathBuf, +} + +#[derive(Parser, Debug)] +#[clap(about = "Get reports of system's audio resources.")] +pub struct Info { + /// Output the available devices and their configurations + #[clap(long)] + pub print_configs: bool, +} + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +pub struct Cli { + #[clap(subcommand)] + pub command: Commands, +} \ No newline at end of file diff --git a/audio-logger/src/constants.rs b/audio-logger/src/constants.rs new file mode 100644 index 0000000..7fa1495 --- /dev/null +++ b/audio-logger/src/constants.rs @@ -0,0 +1,7 @@ +pub const DEFAULT_SAMPLE_RATE: u32 = 44100; +pub const DEFAULT_CHANNEL_COUNT: u16 = 1; +pub const DEFAULT_BUFFER_SIZE: u32 = 1024; +pub const ALLOWED_SAMPLE_RATES: [u32; 6] = [44100, 48000, 88200, 96000, 176400, 192000]; +pub const MAX_CHANNEL_COUNT: u16 = 2; +pub const MIN_BUFFER_SIZE: usize = 64; +pub const MAX_BUFFER_SIZE: usize = 8192; \ No newline at end of file diff --git a/audio-logger/src/getters.rs b/audio-logger/src/getters.rs new file mode 100644 index 0000000..72b250c --- /dev/null +++ b/audio-logger/src/getters.rs @@ -0,0 +1,113 @@ +use super::*; +use cpal::{ + StreamConfig, + SupportedStreamConfig, + Device, + HostId, + Host, + SampleRate, + BufferSize, + traits::{DeviceTrait, HostTrait}, +}; +use hound::WavSpec; +use std::path::PathBuf; +use chrono::*; +use anyhow::{Error, Result, anyhow}; + +/// # Get Host +/// +/// Returns the host with the given id if it's available. +pub fn get_host(host: HostId) -> Result { + Ok(cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == host) + .ok_or(anyhow!("Requested host device not found"))? + )?) +} + +/// # Get Device +/// +/// Returns the default input device for the host if it's available. +pub fn get_device(host: Host) -> Result { + Ok(host.default_input_device() + .ok_or(anyhow!("No input device available. Try running `jackd -R -d alsa -d hw:0`", + ))?) +} + +/// # Get Default Config +/// +/// Get the default config for the given device. +pub fn get_default_config(device: &Device) -> Result { + Ok(device.default_input_config()?) +} + +/// # Get User Config +/// +/// Overrides certain fields of the default stream config with the user's config. +/// +/// sample_rate: The user's sample rate if it is supported by the device, otherwise the default sample rate. +/// channels: The user's number of channels if it is supported by the device, otherwise the default number of channels. +/// buffer_size: The user's buffer size if it is supported by the device, otherwise the default buffer size. +pub fn get_user_config(sample_rate: u32, channels: u16, buffer_size: u32) -> Result { + if !ALLOWED_SAMPLE_RATES.contains(&sample_rate) { + return Err(anyhow!( + "Sample rate {} is not supported. Allowed sample rates: {:?}", + sample_rate, + ALLOWED_SAMPLE_RATES + )); + } + if !(channels >= 1 && channels <= MAX_CHANNEL_COUNT) { + return Err(anyhow!( + "Channel count {} is not supported. Allowed channel counts: 1-{}", + channels, + MAX_CHANNEL_COUNT + )); + } + if !(buffer_size >= MIN_BUFFER_SIZE as u32 && buffer_size <= MAX_BUFFER_SIZE as u32) { + return Err(anyhow!( + "Buffer size {} is not supported. Allowed buffer sizes: {}-{}", + buffer_size, + MIN_BUFFER_SIZE, + MAX_BUFFER_SIZE + )); + } + Ok(StreamConfig { + channels, + sample_rate: SampleRate(sample_rate), + buffer_size: BufferSize::Fixed(buffer_size), + }) +} + +/// # Get WAV Spec +/// +/// Get the WAV spec for the given stream config. +pub fn get_wav_spec(default_config: &SupportedStreamConfig, user_config: &StreamConfig) -> Result { + Ok(WavSpec { + channels: user_config.channels, + sample_rate: user_config.sample_rate.0, + bits_per_sample: (default_config.sample_format().sample_size() * 8) as u16, + sample_format: match default_config.sample_format() { + cpal::SampleFormat::U16 => hound::SampleFormat::Int, + cpal::SampleFormat::I16 => hound::SampleFormat::Int, + cpal::SampleFormat::F32 => hound::SampleFormat::Float, + }, + }) +} + +pub fn get_date_time_string() -> String { + let now: DateTime = Local::now(); + format!( + "{}-{}-{}_{}:{}:{:02}", + now.year(), now.month(), now.day(), + now.hour(), now.minute(), now.second(), + ) +} +/// # Get Filename +/// +/// Get the filename for the current recording according to the given format, +/// the current date and time, and the name prefix. +pub fn get_filename(name: &str, path: &PathBuf) -> String { + let mut filename = path.clone(); + filename.push(format!("{}_{}.wav", get_date_time_string(), name)); + filename.to_str().unwrap().to_string() +} diff --git a/audio-logger/src/input_handling.rs b/audio-logger/src/input_handling.rs new file mode 100644 index 0000000..129c3b1 --- /dev/null +++ b/audio-logger/src/input_handling.rs @@ -0,0 +1,70 @@ +use super::*; +use std::sync::{ + Arc, + Mutex, + Condvar, + atomic::{AtomicBool, Ordering} +}; + +type StreamInterrupt = Arc<(Mutex, Condvar)>; +type BatchInterrupt= Arc; + +/// # Interrupts Handling +/// +/// The `Recorder` struct has two interrupt mechanisms: +/// +/// 1. `stream_interrupt` is used to interrupt the stream when the user presses `ctrl+c`. +/// 2. `batch_interrupt` is used to interrupt the batch recording when the user presses `ctrl+c`. +#[derive(Clone)] +pub struct InterruptHandles { + batch_interrupt: BatchInterrupt, + stream_interrupt: StreamInterrupt, + max_seconds: Option, +} + +impl InterruptHandles { + pub fn new(max_seconds: Option) -> Result { + let stream_interrupt = Arc::new((Mutex::new(false), Condvar::new())); + let stream_interrupt_cloned = stream_interrupt.clone(); + + let batch_interrupt = Arc::new(AtomicBool::new(false)); + let batch_interrupt_cloned = batch_interrupt.clone(); + + ctrlc::set_handler(move || { + // Set batch interrupt to true + batch_interrupt_cloned.store(true, Ordering::SeqCst); + + // Release the stream + let &(ref lock, ref cvar) = &*stream_interrupt_cloned; + let mut started = lock.lock().unwrap(); + *started = true; + cvar.notify_one(); + })?; + Ok(Self { + batch_interrupt, + stream_interrupt, + max_seconds, + }) + } + + pub fn stream_wait(&self) { + let &(ref lock, ref cvar) = &*self.stream_interrupt; + let mut started = lock.lock().unwrap(); + let now = std::time::Instant::now(); + while !*started { + match self.max_seconds { + Some(secs) => { + if now.elapsed().as_secs() >= secs { + break; + } + } + None => (), + } + started = cvar.wait(started).unwrap(); + } + } + + pub fn batch_is_running(&self) -> bool { + !self.batch_interrupt.load(Ordering::SeqCst) + } +} \ No newline at end of file diff --git a/audio-logger/src/main.rs b/audio-logger/src/main.rs index 8839314..048bd68 100644 --- a/audio-logger/src/main.rs +++ b/audio-logger/src/main.rs @@ -1,97 +1,43 @@ -//! Records a WAV file (roughly 3 seconds long) using the default input device and config. -//! -//! The input data is recorded to "$CARGO_MANIFEST_DIR/recorded.wav". +//! # Main mod cli; mod recorder; mod print_configs; +mod getters; +mod input_handling; +mod merge; +mod constants; +mod play; + +use print_configs::*; +use cli::*; +use merge::*; +use recorder::*; +use constants::*; +use getters::*; +use input_handling::*; +use play::*; +use anyhow::{Error, Result, anyhow}; use clap::Parser; -use recorder::{batch_recording, contiguous_recording}; -use cli::*; -use std::path::Path; -use print_configs::*; -use std::sync::{Arc, Mutex, Condvar, atomic::{AtomicBool, Ordering}}; -const DEFAULT_SAMPLE_RATE: u32 = 44100; -const DEFAULT_CHANNEL_COUNT: u16 = 1; -const DEFAULT_BUFFER_SIZE: u32 = 1024; -const ALLOWED_SAMPLE_RATES: &[u32] = &[44100, 48000, 88200, 96000, 176400, 192000]; -const MAX_CHANNEL_COUNT: u16 = 2; -const MIN_BUFFER_SIZE: usize = 64; -const MAX_BUFFER_SIZE: usize = 8192; -const FMT_TIME: &str = "%Y-%m-%d-%H:%M:%S.%f"; +fn main() -> Result<(), Error> { + let cli = Cli::parse(); -type StreamInterrupt = Arc<(Mutex, Condvar)>; -type BatchInterrupt= Arc; - -/// # Interrupts Handling -/// -/// The `Recorder` struct has two interrupt mechanisms: -/// -/// 1. `stream_interrupt` is used to interrupt the stream when the user presses `ctrl+c`. -/// 2. `batch_interrupt` is used to interrupt the batch recording when the user presses `ctrl+c`. -#[derive(Clone)] -pub struct InterruptHandles { - batch_interrupt: BatchInterrupt, - stream_interrupt: StreamInterrupt, -} - -impl InterruptHandles { - pub fn new() -> Result { - let stream_interrupt = Arc::new((Mutex::new(false), Condvar::new())); - let stream_interrupt_cloned = stream_interrupt.clone(); - - let batch_interrupt = Arc::new(AtomicBool::new(false)); - let batch_interrupt_cloned = batch_interrupt.clone(); - - ctrlc::set_handler(move || { - // Set batch interrupt to true - batch_interrupt_cloned.store(true, Ordering::SeqCst); - - // Release the stream - let &(ref lock, ref cvar) = &*stream_interrupt_cloned; - let mut started = lock.lock().unwrap(); - *started = true; - cvar.notify_one(); - })?; - Ok(Self { - batch_interrupt, - stream_interrupt, - }) - } - - pub fn stream_wait(&self) { - let &(ref lock, ref cvar) = &*self.stream_interrupt; - let mut started = lock.lock().unwrap(); - while !*started { - started = cvar.wait(started).unwrap(); - } - } - - pub fn batch_is_running(&self) -> bool { - !self.batch_interrupt.load(Ordering::SeqCst) - } -} - -fn main() -> Result<(), anyhow::Error> { - let args = Args::parse(); - - if args.print_configs { - print_configs()?; - return Ok(()); - } - - let interrupt_handles = InterruptHandles::new()?; - - match args.batch_recording { - Some(secs) => { - batch_recording(&args, secs, interrupt_handles)?; + match &cli.command { + Commands::Rec(args) => { + record(&args)?; }, - None => { - contiguous_recording(&args, interrupt_handles)?; + Commands::Play(args) => { + play(&args.input)?; + }, + Commands::Info(args) => { + if args.print_configs { + print_configs()?; + } + }, + Commands::Merge(args) => { + merge_wavs(&args.input, &args.output)?; } } - Ok(()) } - diff --git a/audio-logger/src/merge.rs b/audio-logger/src/merge.rs new file mode 100644 index 0000000..bebc093 --- /dev/null +++ b/audio-logger/src/merge.rs @@ -0,0 +1,43 @@ +use hound::{WavReader, WavWriter}; +use anyhow::{Result, Error}; + +pub fn merge_wavs(input: &std::path::PathBuf, output: &std::path::PathBuf) -> Result<(), Error> { + // Read files from input directory + let mut files = std::fs::read_dir(input)? + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().ok().map(|t| t.is_file()).unwrap_or(false)) + .filter(|entry| entry.path().extension().unwrap_or_default() == "wav") + .collect::>(); + + // Sort files by name + files.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + + // Use the name of the first file as the name of the output file + let output_name = files.first().unwrap().file_name(); + let output_name = output_name.to_str().unwrap(); + + // Get wav spec from file + let spec = WavReader::open(files.first().unwrap().path())?.spec(); + let mut writer = WavWriter::create(output.join(output_name), spec)?; + + match spec.sample_format { + hound::SampleFormat::Float => { + for file in files { + let mut reader = WavReader::open(file.path())?; + for sample in reader.samples::() { + writer.write_sample(sample?)?; + } + } + }, + hound::SampleFormat::Int => { + for file in files { + let mut reader = WavReader::open(file.path())?; + for sample in reader.samples::() { + writer.write_sample(sample?)?; + } + } + }, + } + writer.finalize()?; + Ok(()) +} diff --git a/audio-logger/src/play.rs b/audio-logger/src/play.rs new file mode 100644 index 0000000..0115156 --- /dev/null +++ b/audio-logger/src/play.rs @@ -0,0 +1,14 @@ +//! Play audio from a .wav file. + +use anyhow::{Error, Result}; +use rodio::{Decoder, OutputStream, Sink}; + +pub fn play(path: &std::path::PathBuf) -> Result<(), Error> { + let file = std::fs::File::open(path)?; + let source = Decoder::new(file).unwrap(); + let (_stream, stream_handle) = OutputStream::try_default()?; + let sink = Sink::try_new(&stream_handle)?; + sink.append(source); + sink.sleep_until_end(); + Ok(()) +} \ No newline at end of file diff --git a/audio-logger/src/print_configs.rs b/audio-logger/src/print_configs.rs index 6784749..45b0fb5 100644 --- a/audio-logger/src/print_configs.rs +++ b/audio-logger/src/print_configs.rs @@ -1,5 +1,19 @@ use cpal::traits::{DeviceTrait, HostTrait}; +// pub fn print_available_hosts() -> Result<(), anyhow::Error> { +// let hosts = cpal::available_hosts(); +// println!("Available hosts:"); +// for host in hosts { +// println!(" {}", host.name()); +// } +// Ok(()) +// } + +// pub fn print_supported_hosts() { +// println!("Supported hosts:"); +// println!("{:?}", cpal::ALL_HOSTS); +// } + pub fn print_configs() -> Result<(), anyhow::Error> { println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS); let available_hosts = cpal::available_hosts(); diff --git a/audio-logger/src/recorder.rs b/audio-logger/src/recorder.rs index aa508f9..adba7c7 100644 --- a/audio-logger/src/recorder.rs +++ b/audio-logger/src/recorder.rs @@ -1,67 +1,49 @@ -use anyhow::anyhow; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::*; -use std::fs::File; -use std::io::BufWriter; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use chrono::prelude::*; use super::*; +use cpal::{ + StreamConfig, + SupportedStreamConfig, + Device, + HostId, + Stream, + traits::{DeviceTrait, StreamTrait}, +}; +use std::{ + fs::File, + io::BufWriter, + path::{PathBuf, Path}, + sync::{Arc, Mutex}, +}; +use anyhow::Error; type WriteHandle = Arc>>>>; pub struct Recorder { writer: WriteHandle, - interrupt: InterruptHandles, + interrupt_handles: InterruptHandles, default_config: SupportedStreamConfig, user_config: StreamConfig, device: Device, - filename: String, -} - -/// # Stream User Config -/// -/// Overrides certain fields of the default stream config with the user's config. -/// -/// sample_rate: The user's sample rate if it is supported by the device, otherwise the default sample rate. -/// channels: The user's number of channels if it is supported by the device, otherwise the default number of channels. -/// buffer_size: The user's buffer size if it is supported by the device, otherwise the default buffer size. -fn stream_user_config(sample_rate: u32, channels: u16, buffer_size: u32) -> Result { - if !ALLOWED_SAMPLE_RATES.contains(&sample_rate) { - return Err(anyhow!( - "Sample rate {} is not supported. Allowed sample rates: {:?}", - sample_rate, - ALLOWED_SAMPLE_RATES - )); - } - if !(channels >= 1 && channels <= MAX_CHANNEL_COUNT) { - return Err(anyhow!( - "Channel count {} is not supported. Allowed channel counts: 1-{}", - channels, - MAX_CHANNEL_COUNT - )); - } - if !(buffer_size >= MIN_BUFFER_SIZE as u32 && buffer_size <= MAX_BUFFER_SIZE as u32) { - return Err(anyhow!( - "Buffer size {} is not supported. Allowed buffer sizes: {}-{}", - buffer_size, - MIN_BUFFER_SIZE, - MAX_BUFFER_SIZE - )); - } - Ok(StreamConfig { - channels, - sample_rate: SampleRate(sample_rate), - buffer_size: BufferSize::Fixed(buffer_size), - }) + spec: hound::WavSpec, + name: String, + path: PathBuf, + current_file: String, + max_seconds: Option, } /// # Recorder /// /// The `Recorder` struct is used to record audio. +/// +/// Use `init()` to initialize the recorder, `record()` to start a continuous recording, +/// and `rec_secs()` to record for a given number of seconds. The Recorder does not +/// need to be reinitialized after a recording is stopped. Calling `record()` or +/// `rec_secs()` again will start a new recording with a new filename according to +/// the time and date. impl Recorder { - /// Initializes a new recorder. + /// # Init + /// + /// Initializes the recorder with the given host, sample rate, channel count, and buffer size. pub fn init( name: String, path: PathBuf, @@ -69,53 +51,52 @@ impl Recorder { sample_rate: u32, channels: u16, buffer_size: u32, - interrupt: InterruptHandles, - ) -> Result { + max_seconds: Option, + ) -> Result { - // Select requested host - let host = cpal::host_from_id(cpal::available_hosts() - .into_iter() - .find(|id| *id == host) - .ok_or(anyhow!("Requested host device not found"))? - )?; + // Create interrupt handles to be used by the stream or batch loop. + let interrupt_handles = InterruptHandles::new(max_seconds)?; + + // Select requested host. + let host = get_host(host)?; // Set up the input device and stream with the default input config. - let device = host.default_input_device() - .ok_or(anyhow!("No input device available. Try running `jackd -R -d alsa -d hw:0`", - ))?; + let device = get_device(host)?; - let default_config = device.default_input_config()?; - let user_config = stream_user_config(sample_rate, channels, buffer_size)?; + // Get default config for the device. + let default_config = get_default_config(&device)?; - let spec = hound::WavSpec { - channels: user_config.channels as _, - sample_rate: user_config.sample_rate.0 as _, - bits_per_sample: (default_config.sample_format().sample_size() * 8) as _, - sample_format: match default_config.sample_format() { - cpal::SampleFormat::U16 => hound::SampleFormat::Int, - cpal::SampleFormat::I16 => hound::SampleFormat::Int, - cpal::SampleFormat::F32 => hound::SampleFormat::Float, - }, - }; + // Override certain fields of the default stream config with the user's config. + let user_config = get_user_config(sample_rate, channels, buffer_size)?; - // The WAV file we're recording to. - let ts: String = Utc::now().format(FMT_TIME).to_string(); - let filename: String = path.to_str().unwrap().to_owned() + &name + "-" + &ts + ".wav"; + // Get the hound WAV spec for the user's config. + let spec = get_wav_spec(&default_config, &user_config)?; Ok(Self { - writer: Arc::new(Mutex::new(Some(hound::WavWriter::create(filename.clone(), spec)?))), - interrupt, + writer: Arc::new(Mutex::new(None)), + interrupt_handles, default_config, user_config, device, - filename, + spec, + name, + path, + current_file: "".to_string(), + max_seconds, }) } - fn create_stream(&self) -> Result { + fn init_writer(&mut self) -> Result<(), Error> { + let filename = get_filename(&self.name, &self.path); + self.current_file = filename.clone(); + self.writer = Arc::new(Mutex::new(Some(hound::WavWriter::create(filename, self.spec)?))); + Ok(()) + } + + fn create_stream(&self) -> Result { let writer = self.writer.clone(); let config = self.user_config.clone(); - let err_fn = |err| { eprintln!("an error occurred on stream: {}", err); }; + let err_fn = |err| { eprintln!("{}: An error occurred on stream: {}", get_date_time_string(), err); }; let stream = match self.default_config.sample_format() { cpal::SampleFormat::F32 => self.device.build_input_stream( @@ -137,31 +118,45 @@ impl Recorder { Ok(stream) } - pub fn record(&self) -> Result<(), anyhow::Error> { + /// # Record + /// + /// Start a continuous recording. The recording will be stopped when the + /// user presses `Ctrl+C`. + pub fn record(&mut self) -> Result<(), Error> { + self.init_writer()?; let stream = self.create_stream()?; stream.play()?; - println!("REC: {}", self.filename); - self.interrupt.stream_wait(); + println!("REC: {}", self.current_file); + self.interrupt_handles.stream_wait(); drop(stream); self.writer.lock().unwrap().take().unwrap().finalize()?; - println!("STOP: {}", self.filename); + println!("STOP: {}", self.current_file); Ok(()) } - pub fn record_secs(&self, secs: u64) -> Result<(), anyhow::Error> { + /// # Record Seconds + /// + /// Record for a given number of seconds or until the user presses `Ctrl+C`. + /// Current batch is finished before stopping. + pub fn record_secs(&mut self, secs: u64) -> Result<(), Error> { + self.init_writer()?; let stream = self.create_stream()?; stream.play()?; - println!("REC: {}", self.filename); + println!("REC: {}", self.current_file); let now = std::time::Instant::now(); - loop { - std::thread::sleep(std::time::Duration::from_millis(500)); + while self.interrupt_handles.batch_is_running() { + std::thread::sleep(std::time::Duration::from_millis(1)); if now.elapsed().as_secs() >= secs { break; } } drop(stream); - self.writer.lock().unwrap().take().unwrap().finalize()?; - println!("STOP: {}", self.filename); + let writer = self.writer.clone(); + let current_file = self.current_file.clone(); + std::thread::spawn(move || { + writer.lock().unwrap().take().unwrap().finalize().unwrap(); + println!("STOP: {}", current_file); + }); Ok(()) } } @@ -181,44 +176,68 @@ where } } -pub fn batch_recording(args: &Args, secs: u64, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> { - while interrupt_handles.batch_is_running() { - let recorder = recorder::Recorder::init( - args.name.clone(), - match args.output.clone() { - Some(path) => path, - None => Path::new("./").to_path_buf(), - }, - match args.host { - Hosts::Alsa => cpal::HostId::Alsa, - Hosts::Jack => cpal::HostId::Jack, - }, - args.sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE), - args.channels.unwrap_or(DEFAULT_CHANNEL_COUNT), - args.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE), - interrupt_handles.clone(), - )?; - recorder.record_secs(secs)?; +fn batch_recording(rec: &mut Recorder, secs: u64) -> Result<(), Error> { + let now = std::time::Instant::now(); + while rec.interrupt_handles.batch_is_running() { + match rec.max_seconds { + Some(max_secs) => { + if now.elapsed().as_secs() >= max_secs { + break; + } + } + None => {} + } + rec.record_secs(secs)?; } Ok(()) } -pub fn contiguous_recording(args: &Args, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> { - let recorder = recorder::Recorder::init( +fn continuous_recording(rec: &mut Recorder) -> Result<(), Error> { + rec.record()?; + Ok(()) +} + +#[cfg(target_os = "linux")] +fn match_host_platform(host: Hosts) -> Result { + match host { + Hosts::Alsa => Ok(cpal::HostId::Alsa), + Hosts::Jack => Ok(cpal::HostId::Jack), + _ => Err(anyhow!("Host not supported on Linux.")), + } +} + +#[cfg(target_os = "macos")] +fn match_host_platform(host: Hosts) -> Result { + match host { + Hosts::CoreAudio => Ok(cpal::HostId::CoreAudio), + _ => Err(anyhow!("Host not supported on macOS.")), + } +} + +#[cfg(target_os = "windows")] +fn match_host_platform(host: Hosts) -> Result { + match host { + Hosts::Asio => Ok(cpal::HostId::Asio), + _ => Err(anyhow!("Host not supported on Windows.")), + } +} + +pub fn record(args: &Rec) -> Result<(), Error> { + let mut recorder = Recorder::init( args.name.clone(), match args.output.clone() { Some(path) => path, None => Path::new("./").to_path_buf(), }, - match args.host { - Hosts::Alsa => cpal::HostId::Alsa, - Hosts::Jack => cpal::HostId::Jack, - }, + match_host_platform(args.host)?, args.sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE), args.channels.unwrap_or(DEFAULT_CHANNEL_COUNT), args.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE), - interrupt_handles.clone(), + args.max_seconds, )?; - recorder.record()?; - Ok(()) + + match args.batch_recording { + Some(seconds) => batch_recording(&mut recorder, seconds), + None => continuous_recording(&mut recorder), + } } diff --git a/configuration/hydrophonitor-config.txt b/configuration/hydrophonitor-config.txt new file mode 100644 index 0000000..96eba0f --- /dev/null +++ b/configuration/hydrophonitor-config.txt @@ -0,0 +1,21 @@ +# Audio + +SAMPLE_RATE=44100 +CHANNELS=2 +BITRATE="192k" +BATCH_RECORD_LENGTH=60 + +# GPS + +GPS_INTERVAL=5 + +# Depth + +DEPTH_INTERVAL=5 + +# Output location + +TRY_MOUNT_SSD=true + +DEFAULT_BASE_DIR_PATH=/home/pi/recordings +BASE_DIR_PATH=/home/pi/data diff --git a/configuration/setup-raspberry-pi.sh b/configuration/setup-raspberry-pi.sh new file mode 100644 index 0000000..85e71f6 --- /dev/null +++ b/configuration/setup-raspberry-pi.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +set -ex + +DIR_PATH=$HOME +BOOT_DIR_PATH=/boot/hydrophonitor + +# Copy the files to DIR_PATH +echo +echo "### Copy files to $DIR_PATH" +echo + +mkdir -p "$DIR_PATH" +cd "$DIR_PATH" +cp -R $BOOT_DIR_PATH/ . + +# Install the Rust toolchain +echo +echo "### Install the Rust toolchain" +echo + +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +source "$HOME/.cargo/env" + +# Install some developer tools +echo +echo "### Install some developer tools" +echo + +sudo apt-get update && sudo apt-get install -y build-essential + +# Setup audio +echo +echo "### Setup audio" +echo + +cd "$DIR_PATH" && sh hydrophonitor/scripts/setup-audio.sh + +# Setup GPS +echo +echo "### Setup GPS" +echo + +cd "$DIR_PATH" && sh hydrophonitor/scripts/setup-gps.sh + +# Setup depth sensor +echo +echo "### Setup depth recording" +echo + +cd "$DIR_PATH" && sh hydrophonitor/scripts/setup-pressure-depth.sh + +# Set up cron job to start the recordings at boot +echo +echo "### Set up a cron job to start the recordings at boot" +echo + +# USER=$(whoami) +CRON_FILE=/etc/crontab +CRON_COMMAND="@reboot root $DIR_PATH/hydrophonitor/scripts/start-all.sh 2>&1 >> $BOOT_DIR_PATH/log.txt" + +# Append command to cron file only if it's not there yet +sudo grep -qxF "$CRON_COMMAND" $CRON_FILE || echo "$CRON_COMMAND" | sudo tee -a $CRON_FILE + +# Reboot +echo +echo "### Setup ready, run 'sudo reboot' to apply all changes" +echo diff --git a/configuration/ssh.txt b/configuration/ssh.txt new file mode 100644 index 0000000..e69de29 diff --git a/configuration/userconf.txt b/configuration/userconf.txt new file mode 100644 index 0000000..19ef386 --- /dev/null +++ b/configuration/userconf.txt @@ -0,0 +1 @@ +: \ No newline at end of file diff --git a/configuration/wpa_supplicant.conf b/configuration/wpa_supplicant.conf new file mode 100644 index 0000000..11648d1 --- /dev/null +++ b/configuration/wpa_supplicant.conf @@ -0,0 +1,13 @@ +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 +country= + +network={ + scan_ssid=1 + ssid="" + psk="" + proto=RSN + key_mgmt=WPA-PSK + pairwise=CCMP + auth_alg=OPEN +} \ No newline at end of file diff --git a/data/depth/2022-09-03T17-55-58_depth_data.csv b/data/depth/2022-09-03T17-55-58_depth_data.csv deleted file mode 100644 index 51e0d0c..0000000 --- a/data/depth/2022-09-03T17-55-58_depth_data.csv +++ /dev/null @@ -1,43 +0,0 @@ -time and date, Voltage of depth sensor (V), Depth (m) -2022-09-03T17:55:58,0.714,-0.13 -2022-09-03T17:56:01,0.714,-0.13 -2022-09-03T17:56:04,0.714,-0.19 -2022-09-03T17:56:07,0.714,-0.19 -2022-09-03T17:56:10,0.714,-0.19 -2022-09-03T17:56:13,0.714,-0.13 -2022-09-03T17:56:16,0.714,-0.19 -2022-09-03T17:56:19,0.716,-0.19 -2022-09-03T17:56:22,0.714,-0.13 -2022-09-03T17:56:25,0.714,-0.19 -2022-09-03T17:56:28,0.714,-0.19 -2022-09-03T17:56:31,0.714,-0.19 -2022-09-03T17:56:34,0.714,-0.19 -2022-09-03T17:56:37,0.714,-0.13 -2022-09-03T17:56:40,0.714,-0.13 -2022-09-03T17:56:43,0.714,-0.19 -2022-09-03T17:56:46,0.716,-0.13 -2022-09-03T17:56:49,0.714,-0.13 -2022-09-03T17:56:52,0.714,-0.13 -2022-09-03T17:56:55,0.716,-0.19 -2022-09-03T17:56:59,0.714,-0.19 -2022-09-03T17:57:02,0.714,-0.19 -2022-09-03T17:57:05,0.716,-0.13 -2022-09-03T17:57:08,0.714,-0.13 -2022-09-03T17:57:11,0.714,-0.19 -2022-09-03T17:57:14,0.714,-0.13 -2022-09-03T17:57:17,0.894,5.48 -2022-09-03T17:57:20,0.986,8.54 -2022-09-03T17:57:23,0.988,8.47 -2022-09-03T17:57:26,1.368,20.58 -2022-09-03T17:57:29,1.354,20.26 -2022-09-03T17:57:32,0.714,-0.19 -2022-09-03T17:57:35,0.716,-0.13 -2022-09-03T17:57:38,0.714,-0.19 -2022-09-03T17:57:41,0.714,-0.19 -2022-09-03T17:57:44,0.714,-0.19 -2022-09-03T17:57:47,0.716,-0.19 -2022-09-03T17:57:50,0.714,-0.19 -2022-09-03T17:57:53,0.714,-0.13 -2022-09-03T17:57:56,0.714,-0.13 -2022-09-03T17:57:59,0.714,-0.19 -2022-09-03T17:58:02,0.714,-0.13 diff --git a/data/depth/2022-09-03T18-01-18_depth_data.csv b/data/depth/2022-09-03T18-01-18_depth_data.csv deleted file mode 100644 index 60e093b..0000000 --- a/data/depth/2022-09-03T18-01-18_depth_data.csv +++ /dev/null @@ -1,49 +0,0 @@ -time and date, Voltage of depth sensor (V), Depth (m) -2022-09-03T18:01:18,0.716,-0.13 -2022-09-03T18:01:21,0.716,-0.13 -2022-09-03T18:01:24,0.714,-0.19 -2022-09-03T18:01:27,0.716,-0.13 -2022-09-03T18:01:30,0.714,-0.19 -2022-09-03T18:01:33,0.716,-0.13 -2022-09-03T18:01:36,0.716,-0.13 -2022-09-03T18:01:39,0.716,-0.13 -2022-09-03T18:01:42,0.716,-0.13 -2022-09-03T18:01:45,0.714,-0.19 -2022-09-03T18:01:48,0.714,-0.19 -2022-09-03T18:01:51,0.714,-0.19 -2022-09-03T18:01:54,0.714,-0.19 -2022-09-03T18:01:57,0.716,-0.13 -2022-09-03T18:02:00,0.716,-0.13 -2022-09-03T18:02:03,0.714,-0.19 -2022-09-03T18:02:06,0.714,-0.19 -2022-09-03T18:02:09,0.714,-0.19 -2022-09-03T18:02:12,0.714,-0.19 -2022-09-03T18:02:15,0.714,-0.19 -2022-09-03T18:02:18,0.716,-0.13 -2022-09-03T18:02:21,0.716,-0.13 -2022-09-03T18:02:24,0.714,-0.19 -2022-09-03T18:02:27,0.714,-0.19 -2022-09-03T18:02:30,0.714,-0.19 -2022-09-03T18:02:33,0.714,-0.19 -2022-09-03T18:02:36,0.714,-0.19 -2022-09-03T18:02:39,0.716,-0.13 -2022-09-03T18:02:42,0.714,-0.19 -2022-09-03T18:02:45,0.714,-0.19 -2022-09-03T18:02:48,0.714,-0.19 -2022-09-03T18:02:51,0.714,-0.19 -2022-09-03T18:02:54,0.714,-0.19 -2022-09-03T18:02:57,0.714,-0.19 -2022-09-03T18:03:00,0.714,-0.19 -2022-09-03T18:03:03,0.714,-0.19 -2022-09-03T18:03:06,0.714,-0.19 -2022-09-03T18:03:09,0.714,-0.19 -2022-09-03T18:03:12,0.714,-0.19 -2022-09-03T18:03:15,0.714,-0.19 -2022-09-03T18:03:18,0.716,-0.13 -2022-09-03T18:03:21,0.716,-0.13 -2022-09-03T18:03:24,0.714,-0.19 -2022-09-03T18:03:27,0.716,-0.13 -2022-09-03T18:03:30,0.714,-0.19 -2022-09-03T18:03:33,0.714,-0.19 -2022-09-03T18:03:36,0.714,-0.19 -2022-09-03T18:03:39,0.714,-0.19 diff --git a/data/depth/2022-09-04T11-50-21_depth_data.csv b/data/depth/2022-09-04T11-50-21_depth_data.csv deleted file mode 100644 index 7998f67..0000000 --- a/data/depth/2022-09-04T11-50-21_depth_data.csv +++ /dev/null @@ -1,17 +0,0 @@ -time and date, Voltage of depth sensor (V), Depth (m) -2022-09-04T11:50:21,0.716,-0.13 -2022-09-04T11:50:24,0.716,-0.13 -2022-09-04T11:50:27,0.716,-0.13 -2022-09-04T11:50:31,1.12,12.74 -2022-09-04T11:50:34,0.716,-0.13 -2022-09-04T11:50:37,1.074,11.28 -2022-09-04T11:50:40,1.154,13.82 -2022-09-04T11:50:43,0.716,-0.13 -2022-09-04T11:50:46,1.034,10.0 -2022-09-04T11:50:49,1.234,16.37 -2022-09-04T11:50:52,1.212,15.67 -2022-09-04T11:50:55,0.714,-0.19 -2022-09-04T11:50:58,0.716,-0.13 -2022-09-04T11:51:01,0.716,-0.13 -2022-09-04T11:51:04,0.716,-0.13 -2022-09-04T11:51:07,0.716,-0.13 diff --git a/data/depth/2022-09-04T11-56-09_depth_data.csv b/data/depth/2022-09-04T11-56-09_depth_data.csv deleted file mode 100644 index f021fa0..0000000 --- a/data/depth/2022-09-04T11-56-09_depth_data.csv +++ /dev/null @@ -1,7 +0,0 @@ -time and date, Voltage of depth sensor (V), Depth (m) -2022-09-04T11:56:09,0.716,-0.13 -2022-09-04T11:56:12,0.716,-0.13 -2022-09-04T11:56:15,0.716,-0.13 -2022-09-04T11:56:18,0.716,-0.13 -2022-09-04T11:56:21,0.716,-0.13 -2022-09-04T11:56:24,0.716,-0.13 diff --git a/data/gps/2022-08-20T12-23-39_GPS_data.csv b/data/gps/2022-08-20T12-23-39_GPS_data.csv deleted file mode 100644 index 525378d..0000000 --- a/data/gps/2022-08-20T12-23-39_GPS_data.csv +++ /dev/null @@ -1,11 +0,0 @@ -GPStime utc,latitude,longitude,speed,sats in view -2022-08-20T09:23:40.000Z,60.196451148,24.960462239,0.582,0 -2022-08-20T09:23:41.000Z,60.196446687,24.960462239,0.374,0 -2022-08-20T09:23:42.000Z,60.196437765,24.960462239,0.257,7 -2022-08-20T09:23:43.000Z,60.196441539,24.960486191,0.192,7 -2022-08-20T09:23:44.000Z,60.196441539,24.960486191,0.121,7 -2022-08-20T09:23:45.000Z,60.196437078,24.960486191,0.279,7 -2022-08-20T09:23:46.000Z,60.196439678,24.960493798,0.233,7 -2022-08-20T09:23:47.000Z,60.196439678,24.960493798,0.059,7 -2022-08-20T09:23:48.000Z,60.196439678,24.960493798,0.124,7 -2022-08-20T09:23:49.000Z,60.196446739,24.960501406,0.369,7 diff --git a/depth-logger/record-depth.py b/depth-logger/record-depth.py index 3447583..b801707 100644 --- a/depth-logger/record-depth.py +++ b/depth-logger/record-depth.py @@ -1,17 +1,23 @@ #!/usr/bin/python3 +import argparse import board import time import busio import adafruit_ads1x15.ads1015 as ADS from adafruit_ads1x15.analog_in import AnalogIn -from time import sleep, strftime -from rpi_lcd import LCD +# from rpi_lcd import LCD -try: - lcd = LCD(bus=2) -except OSError: - lcd = None +parser = argparse.ArgumentParser(description='GPS Logger') +parser.add_argument('-o', '--output', help='Output directory', required=True) +parser.add_argument('-i', '--interval', help='Interval in seconds', required=False) + +args = parser.parse_args() + +# try: +# lcd = LCD(bus=2) +# except OSError: +# lcd = None # Create the I2C bus i2c = busio.I2C(board.SCL, board.SDA) @@ -19,44 +25,47 @@ i2c = busio.I2C(board.SCL, board.SDA) # Create the ADC object using the I2C bus ads = ADS.ADS1015(i2c) +# Create a single-ended input on channel 1 +depthS = AnalogIn(ads, ADS.P1) + # Create single-ended input on channel 0 # tmp36 = AnalogIn(ads, ADS.P0) -# Attempting to create a single-ended input on channel 1 -depthS = AnalogIn(ads, ADS.P1) - # Subtract the offset from the sensor voltage # and convert chan.voltage * (1 degree C / 0.01V) = Degrees Celcius # temperatureC = (tmp36.voltage - 0.5) / 0.01 -# Open the file to write down the results -timestr = time.strftime("%Y-%m-%dT%H-%M-%S") -filename = "/home/shared/hydrophonitor/data/depth/" + timestr + "_depth_data.csv" +# File to write down the results +filename = args.output + "/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_depth_data.csv" -#depthM = ((depthS.voltage * 31.848) - 22.93) +interval = int(args.interval) if args.interval else 5 #Attempting to round the figure to a more intelligible figure #rounddepth = round(depthM, ndigits) #psi = depthS.voltage * 104.1666667 - 75 - #bar = psi * 14.503773800722 with open(filename, "w", 1) as f: + print(f"Writing pressure/depth output to {filename}, interval {interval} seconds", flush=True) f.write("time and date, Voltage of depth sensor (V), Depth (m)\n") - while True: - voltage = depthS.voltage - depthM = ((voltage * 31.848) - 22.93) - rounddepth = round(depthM, 2) - # roundtemp = round(temperatureC, 2) - roundvolts = round(voltage, 3) + try: + while True: + voltage = depthS.voltage + depthM = ((voltage * 31.848) - 22.93) + rounddepth = round(depthM, 2) + roundvolts = round(voltage, 3) + # roundtemp = round(temperatureC, 2) - print((str(voltage) + " V ") + (str(depthM) + " m ") + (str(roundvolts) + " V ") + (str(rounddepth) + " m")) + print((str(voltage) + " V ") + (str(depthM) + " m ") + (str(roundvolts) + " V ") + (str(rounddepth) + " m"), flush=True) - if lcd: - lcd.clear() - lcd.text((str(roundvolts) + " V ") + (str(rounddepth) + " m"), 1) - f.write(time.strftime("%Y-%m-%dT%H:%M:%S") + ",") - f.write(str(roundvolts) + "," + str(rounddepth) + "\n") - - time.sleep(3) + # if lcd: + # lcd.clear() + # lcd.text((str(roundvolts) + " V ") + (str(rounddepth) + " m"), 1) + + f.write(time.strftime("%Y-%m-%dT%H:%M:%S") + "," + str(roundvolts) + "," + str(rounddepth) + "\n") + + time.sleep(interval) + + except (KeyboardInterrupt, SystemExit): # when you press ctrl+c + print("Exiting depth recording.", flush=True) diff --git a/depth-logger/requirements.txt b/depth-logger/requirements.txt new file mode 100644 index 0000000..1a18997 --- /dev/null +++ b/depth-logger/requirements.txt @@ -0,0 +1,2 @@ +Adafruit-Blinka==8.5.0 +adafruit-circuitpython-ads1x15==2.2.21 \ No newline at end of file diff --git a/gps-logger/record-gps.py b/gps-logger/record-gps.py index 7d944d2..97b2a54 100644 --- a/gps-logger/record-gps.py +++ b/gps-logger/record-gps.py @@ -1,29 +1,39 @@ #!/usr/bin/python3 -from gps import * -from time import sleep, strftime +import gpsd +import time +import argparse -filename = "/home/shared/logger-raspi-setup/data/gps/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_GPS_data.csv" -# filename = "/mnt/myssd/GPS_Data" + timestr +".csv" +parser = argparse.ArgumentParser(description='GPS Logger') +parser.add_argument('-o', '--output', help='Output directory', required=True) +parser.add_argument('-i', '--interval', help='Interval in seconds', required=False) + +args = parser.parse_args() + +filename = args.output + "/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_GPS_data.csv" + +interval = int(args.interval) if args.interval else 5 with open(filename, "w", 1) as f: - gpsd = gps(mode=WATCH_ENABLE|WATCH_NEWSTYLE) - f.write("GPStime utc,latitude,longitude,speed,sats in view\n") + gpsd.connect() - try: - while True: - report = gpsd.next() - if report["class"] == "TPV": - GPStime = str(getattr(report,"time","")) - lat = str(getattr(report,"lat",0.0)) - lon = str(getattr(report,"lon",0.0)) - speed = str(getattr(report,"speed","nan")) - sats = str(len(gpsd.satellites)) + print(f"Writing GPS output to {filename}, interval {interval} seconds", flush=True) + f.write("system_time,gps_time_utc,latitude,longitude,speed,sats_in_view\n") - f.write(GPStime + "," + lat +"," + lon + "," + speed + "," + sats + "\n") - - time.sleep(5) - - except (KeyboardInterrupt, SystemExit): # when you press ctrl+c - print("Done.\nExiting.") - f.close() + while True: + try: + packet = gpsd.get_current() + gps_time_utc = str(packet.time) if packet.mode >= 2 else "-" + lat = str(packet.lat) if packet.mode >= 2 else "0.0" + lon = str(packet.lon) if packet.mode >= 2 else "0.0" + speed = str(packet.hspeed) if packet.mode >= 2 else "0.0" + sats = str(packet.sats) + system_time = time.strftime("%Y-%m-%dT%H-%M-%S") + f.write(f"{system_time},{gps_time_utc},{lat},{lon},{speed},{sats}\n") + except (KeyboardInterrupt, SystemExit): # when you press ctrl+c + print("Exiting GPS recording.", flush=True) + break + except Exception as e: + print(f"GPS error: {e}", flush=True) + + time.sleep(interval) diff --git a/gps-logger/requirements.txt b/gps-logger/requirements.txt new file mode 100644 index 0000000..2a628b2 --- /dev/null +++ b/gps-logger/requirements.txt @@ -0,0 +1 @@ +gpsd-py3==0.3.0 \ No newline at end of file diff --git a/resources.md b/resources.md index cf41b0d..37e8e26 100755 --- a/resources.md +++ b/resources.md @@ -6,4 +6,9 @@ https://www.circuito.io/app?components=9443,200000,267055 # Setting up Jack on Raspberry Pi -https://wiki.linuxaudio.org/wiki/raspberrypi \ No newline at end of file +https://wiki.linuxaudio.org/wiki/raspberrypi +https://github.com/supercollider/supercollider/blob/develop/README_RASPBERRY_PI.md +https://madskjeldgaard.dk/posts/raspi4-notes/ + +# Setting up GPS + diff --git a/scripts/audio-setup-utils.sh b/scripts/audio-setup-utils.sh index 60862da..d879fa2 100755 --- a/scripts/audio-setup-utils.sh +++ b/scripts/audio-setup-utils.sh @@ -8,14 +8,10 @@ GOVERNOR="performance" MAX_SPEED="0" MIN_SPEED="0" " | sudo tee -a /etc/default/cpufrequtils -# Install other useful tools -sudo apt-get install htop git perl vim - # Set CPU governor sudo sed -i 's/exit 0/sudo cpufreq-set -r -g performance/g' /etc/rc.local sudo echo "exit 0" | sudo tee -a /etc/rc.local - # Set realtime priority and memlock sudo echo " @audio nice -15 @@ -29,4 +25,4 @@ sudo echo " sudo echo " vm.swappiness = 10 fs.inotify.max_user_watches = 524288 -" | sudo tee /etc/sysctl.conf \ No newline at end of file +" | sudo tee /etc/sysctl.conf diff --git a/scripts/export-config-values.sh b/scripts/export-config-values.sh new file mode 100644 index 0000000..8b397a8 --- /dev/null +++ b/scripts/export-config-values.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +CONFIG_FILE=$1 + +export $(grep -v '^#' $CONFIG_FILE | tr -d '[:space:]' | xargs -d '\n') diff --git a/scripts/setup-audio.sh b/scripts/setup-audio.sh index 936a568..7fea15c 100755 --- a/scripts/setup-audio.sh +++ b/scripts/setup-audio.sh @@ -1,5 +1,28 @@ -#!/bin/sh +#!/bin/bash -# Install jackd +echo "Setting up audio recording" -# Audio setup utils \ No newline at end of file +# Install packages +sudo apt-get update && sudo apt-get install -y libasound2-dev libjack-dev + +# Get ID and number of the USB audio device +card_number=$(aplay -l | grep -i usb | grep -i audio | cut -d ' ' -f 2 | cut -d ':' -f 1) + +# Change default audio device +sudo touch /etc/asound.conf + +sudo cat << EOF | sudo tee /etc/asound.conf +pcm.!default { + type plug + slave { + pcm "hw:$card_number,0" + } +} + +ctl.!default { + type hw + card $card_number +} +EOF + +cd hydrophonitor/audio-logger && cargo build --release diff --git a/scripts/setup-gps.sh b/scripts/setup-gps.sh index 051d067..81258d9 100755 --- a/scripts/setup-gps.sh +++ b/scripts/setup-gps.sh @@ -1,8 +1,12 @@ #!/bin/sh +echo "Setting up GPS recording" + sudo apt-get update && sudo apt-get install -y \ gpsd gpsd-clients +sudo pip install -r /home/pi/hydrophonitor/gps-logger/requirements.txt + sudo systemctl stop gpsd.socket sudo systemctl disable gpsd.socket @@ -12,5 +16,4 @@ sudo gpsd ${device} -F /var/run/gpsd.sock sudo sed -i "s|DEVICES=\"\"|DEVICES=\"${device}\"|g" /etc/default/gpsd -echo "START_DAEMON=\"true\"" | sudo tee -a /etc/default/gpsd - +sudo grep -qxF "START_DAEMON=\"true\"" /etc/default/gpsd || echo "START_DAEMON=\"true\"" | sudo tee -a /etc/default/gpsd diff --git a/scripts/setup-pressure-depth.sh b/scripts/setup-pressure-depth.sh new file mode 100644 index 0000000..eeacb8e --- /dev/null +++ b/scripts/setup-pressure-depth.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +echo "Setting up depth recording" + +# Enable i2c bus on Raspberry Pi +sudo raspi-config nonint do_i2c 0 + +# Install packages +sudo apt-get update && sudo apt-get install -y i2c-tools python3-pip + +sudo pip install -r /home/pi/hydrophonitor/depth-logger/requirements.txt diff --git a/scripts/start-all.sh b/scripts/start-all.sh index 17cbbb4..d6c4c21 100755 --- a/scripts/start-all.sh +++ b/scripts/start-all.sh @@ -1,3 +1,19 @@ -#!/bin/sh +#!/bin/bash -(trap 'kill 0' SIGINT; ./scripts/start-gps.sh & ./scripts/start-audio.sh) +# Print all commands to standard output +set -x + +SCRIPT_PATH=/home/pi/hydrophonitor/scripts + +# Export the configuration values +$SCRIPT_PATH/export-config-values.sh + +# Create output directory +OUTPUT_DIR=$BASE_DIR_PATH/$(date +"%Y-%m-%d_%H-%M-%S_output") +echo "Create output directory $OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR"/audio + +# Sleep for a little to wait for GPS and sound card to be ready +sleep 10 + +(export OUTPUT_DIR=$OUTPUT_DIR; $SCRIPT_PATH/start-audio.sh & $SCRIPT_PATH/start-gps.sh & $SCRIPT_PATH/start-pressure-depth.sh) >> "$OUTPUT_DIR"/log.txt 2>&1 diff --git a/scripts/start-audio.sh b/scripts/start-audio.sh index c82669e..4af6078 100755 --- a/scripts/start-audio.sh +++ b/scripts/start-audio.sh @@ -1,9 +1,17 @@ -#!/bin/sh +#!/bin/bash -# Start jack server +# Export the configuration values +/home/pi/hydrophonitor/scripts/export-config-values.sh -sh scripts/start-jack.sh +AUDIO_TARGET_EXECUTABLE="audio" -# Start recording +OPTIONS="rec \ +--name audio_data \ +--output $OUTPUT_DIR/audio \ +--batch-recording $BATCH_RECORD_LENGTH \ +--sample-rate $SAMPLE_RATE \ +--channels $CHANNELS \ +--buffer-size 1024 \ +alsa" -cd audio-logger && cargo run \ No newline at end of file +cd /home/pi/hydrophonitor/audio-logger/target/release && ./$AUDIO_TARGET_EXECUTABLE $OPTIONS diff --git a/scripts/start-gps.sh b/scripts/start-gps.sh index be7cf51..f8a4a19 100755 --- a/scripts/start-gps.sh +++ b/scripts/start-gps.sh @@ -1,3 +1,8 @@ -#!/usr/bin/sh +#!/bin/bash -python /home/shared/logger-raspi-setup/gps-logger/record-gps.py +# Export the configuration values +/home/pi/hydrophonitor/scripts/export-config-values.sh + +OPTIONS="--output $OUTPUT_DIR --interval $GPS_INTERVAL" + +cd /home/pi/hydrophonitor/gps-logger && python record-gps.py $OPTIONS diff --git a/scripts/start-jack.sh b/scripts/start-jack.sh deleted file mode 100755 index c587dcf..0000000 --- a/scripts/start-jack.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# Get soundcard name -soundcard=$(grep USB /proc/asound/cards | grep -oe "\[.*]" | tr -d "[] ") - -# Start jack server -/usr/bin/jackd -P75 -d alsa -d hw:${soundcard} -r 44100 -p 512 -n 3 & diff --git a/scripts/start-pressure-depth.sh b/scripts/start-pressure-depth.sh new file mode 100644 index 0000000..5c29960 --- /dev/null +++ b/scripts/start-pressure-depth.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Export the configuration values +/home/pi/hydrophonitor/scripts/export-config-values.sh + +OPTIONS="--output $OUTPUT_DIR --interval $DEPTH_INTERVAL" + +cd /home/pi/hydrophonitor/depth-logger && python record-depth.py $OPTIONS diff --git a/setup.md b/setup.md index 5e38c38..d6ac351 100755 --- a/setup.md +++ b/setup.md @@ -2,52 +2,135 @@ ## Components +- Raspberry Pi (tested on 4B) +- MicroSD card + adapter +- Card reader to access the sd card on the computer +- Audio card connected via USB and a microphone/hydrophone attached to the audio card +- USB GPS receiver +- Depth recording components: + - Pressure sensor + - Adafruit ADS1015 ADC + - breadboard, resistors, jumper wires, 12V battery ## Raspberry OS basic setup -### 1. OS +### 1. Install the operating system and set up user, Wi-Fi, ssh access + +#### 1.1 With Raspberry Pi Imager + +The easiest way to install the operating system (Raspberry Pi OS, a Linux Debian-based OS) is to use the official Raspberry Pi Imager utility which works on macOS, Ubuntu and Windows. + +Install from here: https://www.raspberrypi.com/software/ + +After installing, plug the SD card to the computer and launch Raspberry Pi Imager. + +Then the following steps: + +1. Select operating system: click Raspberry Pi OS (other) and then, depending on the Pi, either a 32-bit or 64-bit Raspberry Pi OS Lite +2. Select storage: the sd card should be listed +3. Click the cog icon to set some configurations: + - Enable SSH (use password authentication) + - Set username and password + - Configure wireless LAN: input the name and password of the wi-fi network, select Wireless LAN country + - Set locale settings: select options correct for you +4. Click Write (all existing data on the SD card will be erased and the OS installed) + +#### 1.2 With some other utility + +If you do not use the Raspberry Pi Imager to set up the SD card, the following steps are required: + +1. Download the 32-bit / 64-bit Rasbperry Pi OS Lite from here: https://www.raspberrypi.com/software/operating-systems/ +2. Flash the image to the SD card with the utility of your choice (options for Mac, Linux, Windows?) +3. Fill in required details in the configuration files in configuration folder and copy them to the boot folder on the SD card (this is the folder that should open when you open the SD card volume on your computer): + - ssh.txt: this enables ssh on the Raspberry Pi, no need to edit the file (it's empty, the existence of the file in the boot folder is enough) + - userconf.txt: creates a user + - replace with the username of choice (e.g. pi) + - replace with an encrypted version of your password which can be created with the openssl command line tool: + - open Terminal, write `openssl passwd` and press Enter + - input your password and press enter (asked twice) + - as output, you will get the encrypted version of the password + - wpa_supplicant.conf: set up Wi-Fi + - replace with your country code (e.g. FI) + - replace "" with the name of your Wi-Fi network, e.g. "explorersden" + - replace "" with the Wi-Fi password, e.g. "password" +### 2. Setting up the recording programs on the Raspberry Pi -### 2. Users +After flashing the operating system to the SD card, it should show up as volume called "boot". +To install all the needed components and to configure the Raspberry Pi to start the recordings when it is turned on, four steps are needed: copying the needed files to the SD card, putting the SD card in the Raspberry Pi and connecting to it on the command line over SSH, running an installer script on the command line, and finally letting it restart and verify that everything works as intended. +#### 2.1 Copy files to the SD card, set configuration values -### 3. Wifi +First, set the configuration values in the file hydrophonitor/configuration/hydrophonitor-config.txt. Then, copy the entire `hydrophonitor` folder to the SD card (simple Ctrl+C and Ctrl+V works). +#### 2.2 Plug the SD card in and connect to the Raspberry Pi over SSH +Plug the SD card in the Raspberry Pi. Connect the audio card and the GPS receiver over USB to the Raspberry Pi, and plug the power cable. It will take some time for the Raspberry Pi to be ready to accept SSH connections. -### 4. SSH access +To figure out what IP address the Raspberry Pi has been assigned in the local network, a tool called `nmap` is needed. +To check whether nmap is already installed on the system, open a terminal and run the following command (write it to the terminal and press Enter): +``` +nmap --version +``` -### 5. Installing needed packages +If this prints out version information about nmap (e.g. Nmap version 7.93 ( https://nmap.org)), it is installed. Otherwise, installation instructions can be found here: https://nmap.org/download.html +After installing, run the following command (it will ask for your user password, write it and press Enter) to find all devices connected to the local network: +``` +sudo nmap -sn 192.168.1.0/24 +``` -## SSD +The result will contain a series of discovered devices (hosts) with the following information for each device: +``` +Nmap scan report for 192.168.1.108 +Host is up (0.18s latency). +MAC Address: E4:5F:01:B3:65:DE (Raspberry Pi Trading) +``` +The Raspberry Pi should show up with its IP address (here, 192.168.1.108), MAC address and a name after the MAC address that should help identifying it (here, it's Raspberry Pi Trading). -## Real time clock (RTC) module +Now, this IP address can be used to connect to the Raspberry Pi over SSH on the command line. Connect by running the command `ssh @`, which with a user called `pi` and an IP address of 192.168.1.108 would be +``` +ssh pi@192.168.1.108 +``` +When asked `Are you sure you want to continue connecting (yes/no/[fingerprint])?`, type `yes` and press Enter. Then, write the Raspberry Pi user's password when asked and press Enter. -## Set up recording +After successfully connecting, your prompt should change to `@raspberrypi:~` or something similar. -### 1. Audio +#### 2.3 Run the installer script +After establishing the SSH connection to the Raspberry Pi, change the current directory to the location of the installer script and run it: +``` +cd /boot/hydrophonitor/configuration +./setup-raspberry-pi.sh +``` -### 2. GPS +At the end of successful configuration, the script should print "### Setup ready, run 'sudo reboot' to apply all changes". Run the command and input the Raspberry Pi user's password if requested: +``` +sudo reboot +``` +This will restart the Raspberry Pi and apply the changes made in the setup. On startup, it should now start recording audio, GPS and depth data. -### 3. Water pressure +### 3. Configuration options +todo +### 4. Mount SSD + +todo ## Test & run - +todo diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..182c6d1 --- /dev/null +++ b/todo.md @@ -0,0 +1,23 @@ +# To do list + +- [ ] Configurable, documented setup process for the Raspberry Pi + - [ ] Setup script + - [ ] Copy executables & scripts + - [ ] Required packages to install + - [ ] System configurations + - [ ] Configurable values + - [ ] Test +- [ ] Output formats & location for output data + - [ ] Automatic SSD mounting +- [ ] Audio recording + - [ ] Logging + - [ ] Test +- [ ] GPS recording + - [ ] Logging + - [ ] Test +- [ ] Depth recording + - [ ] Double check formulas to calculate depth & pressure from voltage + - [ ] Logging + - [ ] Test +- [ ] Test autonomous recording as a whole +