Merge branch 'audio-logger'

This commit is contained in:
Satu Koskinen 2022-10-12 08:20:02 +03:00
commit 3a2b209392
43 changed files with 1152 additions and 446 deletions

17
Makefile Normal file
View File

@ -0,0 +1,17 @@
configure:
build:
run-audio-recorder:
run-gps-recorder:
run-depth-recorder:

View File

@ -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 | <start-time-of-recording>_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 | <start-time-of-recording>_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 | <start-time-of-recording>_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 | <start-time-of-recording>_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 (<x>) instead.
<ssd card automatic mount??>
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 <start-time-of-recording>_recordings.

3
audio-logger/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
taget
*.wav
recordings

222
audio-logger/Cargo.lock generated
View File

@ -51,20 +51,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "audio-logger" name = "audio"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alsa", "alsa",
"anyhow", "anyhow",
"chrono", "chrono",
"clap", "clap",
"cpal", "cpal 0.13.5",
"ctrlc", "ctrlc",
"hound", "hound",
"jack 0.9.2", "jack 0.9.2",
"libc", "libc",
"nix 0.23.1", "nix 0.23.1",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"rodio",
] ]
[[package]] [[package]]
@ -104,6 +105,12 @@ version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.2.1" version = "1.2.1"
@ -205,6 +212,12 @@ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]]
name = "claxon"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.6" version = "4.6.6"
@ -255,7 +268,7 @@ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
"mach", "mach",
"ndk", "ndk 0.6.0",
"ndk-glue", "ndk-glue",
"nix 0.23.1", "nix 0.23.1",
"oboe", "oboe",
@ -266,6 +279,31 @@ dependencies = [
"winapi", "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]] [[package]]
name = "ctrlc" name = "ctrlc"
version = "3.2.3" version = "3.2.3"
@ -276,6 +314,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "cty"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.13.4" version = "0.13.4"
@ -523,6 +567,17 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 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]] [[package]]
name = "libc" name = "libc"
version = "0.2.133" version = "0.2.133"
@ -598,6 +653,26 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 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]] [[package]]
name = "ndk" name = "ndk"
version = "0.6.0" version = "0.6.0"
@ -606,11 +681,25 @@ checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"jni-sys", "jni-sys",
"ndk-sys", "ndk-sys 0.3.0",
"num_enum", "num_enum",
"thiserror", "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]] [[package]]
name = "ndk-context" name = "ndk-context"
version = "0.1.1" version = "0.1.1"
@ -626,10 +715,10 @@ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
"ndk", "ndk 0.6.0",
"ndk-context", "ndk-context",
"ndk-macro", "ndk-macro",
"ndk-sys", "ndk-sys 0.3.0",
] ]
[[package]] [[package]]
@ -654,6 +743,15 @@ dependencies = [
"jni-sys", "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]] [[package]]
name = "nix" name = "nix"
version = "0.23.1" version = "0.23.1"
@ -747,7 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1"
dependencies = [ dependencies = [
"jni", "jni",
"ndk", "ndk 0.6.0",
"ndk-context", "ndk-context",
"num-derive", "num-derive",
"num-traits", "num-traits",
@ -763,6 +861,15 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "ogg"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.15.0" version = "1.15.0"
@ -906,6 +1013,15 @@ dependencies = [
"proc-macro2 1.0.43", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.16" version = "0.2.16"
@ -930,6 +1046,19 @@ version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 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]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
@ -963,6 +1092,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" 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]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.9.0" version = "1.9.0"
@ -1049,6 +1189,21 @@ dependencies = [
"winapi", "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]] [[package]]
name = "toml" name = "toml"
version = "0.5.9" version = "0.5.9"
@ -1188,17 +1343,30 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.36.1" version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [ dependencies = [
"windows_aarch64_msvc", "windows_aarch64_msvc 0.36.1",
"windows_i686_gnu", "windows_i686_gnu 0.36.1",
"windows_i686_msvc", "windows_i686_msvc 0.36.1",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc", "windows_x86_64_msvc 0.36.1",
] ]
[[package]] [[package]]
@ -1207,26 +1375,56 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.36.1" version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.36.1" version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.36.1" version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 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]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.36.1" version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"

View File

@ -1,17 +1,16 @@
[package] [package]
name = "audio-logger" name = "audio"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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"] } cpal= { version = "0.13.5", features = ["jack"] }
anyhow = "1.0.61" anyhow = "1.0.61"
hound = "3.4.0" hound = "3.4.0"
chrono = "0.4.22" chrono = "0.4.22"
ctrlc = "3.2.3" ctrlc = "3.2.3"
rodio = "0.16.0"
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))'.dependencies] [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))'.dependencies]
alsa = "0.6" alsa = "0.6"

View File

@ -1,9 +1,9 @@
test: all
bash run_test.sh
all: all:
cargo build --release cargo build --release
test: all
mkdir -p recordings mkdir -p recordings
bash run_test.sh
clean: clean:
cargo clean cargo clean
@ -11,7 +11,6 @@ clean:
fclean: clean fclean: clean
rm -rf recordings rm -rf recordings
re: fclean re: fclean all
cargo build --release
.PHONY: all clean re test .PHONY: all clean fclean re test

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
target/release/audio-logger \ target/release/audio rec \
--name test \ --name test \
--output recordings/ \ --output recordings/ \
--batch-recording 3 \ --batch-recording 3 \

View File

@ -1,14 +1,46 @@
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum, Subcommand};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum Hosts { pub enum Hosts {
Alsa, Alsa,
Jack, 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)] #[derive(Parser, Debug)]
#[clap(about = "A tool to record audio.")] #[clap(about = "Merge audio resulting from a batch recording into a single
pub struct Args { 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` /// Filename will be `[NAME]-yyyy-mm-dd-H:M:S.wav`
#[clap(required = true, short, long)] #[clap(required = true, short, long)]
@ -22,9 +54,9 @@ pub struct Args {
#[clap(short, long, value_name = "SECONDS")] #[clap(short, long, value_name = "SECONDS")]
pub batch_recording: Option<u64>, pub batch_recording: Option<u64>,
/// Output the available devices and their configurations /// (optional) Will record for a total of [SECONDS]
#[clap(long)] #[clap(short, long, value_name = "MAX_SECONDS")]
pub print_configs: bool, pub max_seconds: Option<u64>,
/// Host API to use /// Host API to use
#[clap(value_enum)] #[clap(value_enum)]
@ -42,3 +74,27 @@ pub struct Args {
#[clap(long, value_name = "FRAMES")] #[clap(long, value_name = "FRAMES")]
pub buffer_size: Option<u32>, pub buffer_size: Option<u32>,
} }
#[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,
}

View File

@ -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;

113
audio-logger/src/getters.rs Normal file
View File

@ -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<Host, Error> {
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<Device, Error> {
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<SupportedStreamConfig, Error> {
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<StreamConfig, Error> {
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<WavSpec, Error> {
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> = 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()
}

View File

@ -0,0 +1,70 @@
use super::*;
use std::sync::{
Arc,
Mutex,
Condvar,
atomic::{AtomicBool, Ordering}
};
type StreamInterrupt = Arc<(Mutex<bool>, Condvar)>;
type BatchInterrupt= Arc<AtomicBool>;
/// # 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<u64>,
}
impl InterruptHandles {
pub fn new(max_seconds: Option<u64>) -> Result<Self, anyhow::Error> {
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)
}
}

View File

@ -1,97 +1,43 @@
//! Records a WAV file (roughly 3 seconds long) using the default input device and config. //! # Main
//!
//! The input data is recorded to "$CARGO_MANIFEST_DIR/recorded.wav".
mod cli; mod cli;
mod recorder; mod recorder;
mod print_configs; 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 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; fn main() -> Result<(), Error> {
const DEFAULT_CHANNEL_COUNT: u16 = 1; let cli = Cli::parse();
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";
type StreamInterrupt = Arc<(Mutex<bool>, Condvar)>;
type BatchInterrupt= Arc<AtomicBool>;
/// # 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<Self, anyhow::Error> {
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();
match &cli.command {
Commands::Rec(args) => {
record(&args)?;
},
Commands::Play(args) => {
play(&args.input)?;
},
Commands::Info(args) => {
if args.print_configs { if args.print_configs {
print_configs()?; print_configs()?;
return Ok(());
} }
let interrupt_handles = InterruptHandles::new()?;
match args.batch_recording {
Some(secs) => {
batch_recording(&args, secs, interrupt_handles)?;
}, },
None => { Commands::Merge(args) => {
contiguous_recording(&args, interrupt_handles)?; merge_wavs(&args.input, &args.output)?;
} }
} }
Ok(()) Ok(())
} }

43
audio-logger/src/merge.rs Normal file
View File

@ -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::<Vec<_>>();
// 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::<f32>() {
writer.write_sample(sample?)?;
}
}
},
hound::SampleFormat::Int => {
for file in files {
let mut reader = WavReader::open(file.path())?;
for sample in reader.samples::<i32>() {
writer.write_sample(sample?)?;
}
}
},
}
writer.finalize()?;
Ok(())
}

14
audio-logger/src/play.rs Normal file
View File

@ -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(())
}

View File

@ -1,5 +1,19 @@
use cpal::traits::{DeviceTrait, HostTrait}; 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> { pub fn print_configs() -> Result<(), anyhow::Error> {
println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS); println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
let available_hosts = cpal::available_hosts(); let available_hosts = cpal::available_hosts();

View File

@ -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 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<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>; type WriteHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
pub struct Recorder { pub struct Recorder {
writer: WriteHandle, writer: WriteHandle,
interrupt: InterruptHandles, interrupt_handles: InterruptHandles,
default_config: SupportedStreamConfig, default_config: SupportedStreamConfig,
user_config: StreamConfig, user_config: StreamConfig,
device: Device, device: Device,
filename: String, spec: hound::WavSpec,
} name: String,
path: PathBuf,
/// # Stream User Config current_file: String,
/// max_seconds: Option<u64>,
/// 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<StreamConfig, anyhow::Error> {
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),
})
} }
/// # Recorder /// # Recorder
/// ///
/// The `Recorder` struct is used to record audio. /// 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 { impl Recorder {
/// Initializes a new recorder. /// # Init
///
/// Initializes the recorder with the given host, sample rate, channel count, and buffer size.
pub fn init( pub fn init(
name: String, name: String,
path: PathBuf, path: PathBuf,
@ -69,53 +51,52 @@ impl Recorder {
sample_rate: u32, sample_rate: u32,
channels: u16, channels: u16,
buffer_size: u32, buffer_size: u32,
interrupt: InterruptHandles, max_seconds: Option<u64>,
) -> Result<Self, anyhow::Error> { ) -> Result<Self, Error> {
// Select requested host // Create interrupt handles to be used by the stream or batch loop.
let host = cpal::host_from_id(cpal::available_hosts() let interrupt_handles = InterruptHandles::new(max_seconds)?;
.into_iter()
.find(|id| *id == host) // Select requested host.
.ok_or(anyhow!("Requested host device not found"))? let host = get_host(host)?;
)?;
// Set up the input device and stream with the default input config. // Set up the input device and stream with the default input config.
let device = host.default_input_device() let device = get_device(host)?;
.ok_or(anyhow!("No input device available. Try running `jackd -R -d alsa -d hw:0`",
))?;
let default_config = device.default_input_config()?; // Get default config for the device.
let user_config = stream_user_config(sample_rate, channels, buffer_size)?; let default_config = get_default_config(&device)?;
let spec = hound::WavSpec { // Override certain fields of the default stream config with the user's config.
channels: user_config.channels as _, let user_config = get_user_config(sample_rate, channels, buffer_size)?;
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,
},
};
// The WAV file we're recording to. // Get the hound WAV spec for the user's config.
let ts: String = Utc::now().format(FMT_TIME).to_string(); let spec = get_wav_spec(&default_config, &user_config)?;
let filename: String = path.to_str().unwrap().to_owned() + &name + "-" + &ts + ".wav";
Ok(Self { Ok(Self {
writer: Arc::new(Mutex::new(Some(hound::WavWriter::create(filename.clone(), spec)?))), writer: Arc::new(Mutex::new(None)),
interrupt, interrupt_handles,
default_config, default_config,
user_config, user_config,
device, device,
filename, spec,
name,
path,
current_file: "".to_string(),
max_seconds,
}) })
} }
fn create_stream(&self) -> Result<Stream, anyhow::Error> { 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<Stream, Error> {
let writer = self.writer.clone(); let writer = self.writer.clone();
let config = self.user_config.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() { let stream = match self.default_config.sample_format() {
cpal::SampleFormat::F32 => self.device.build_input_stream( cpal::SampleFormat::F32 => self.device.build_input_stream(
@ -137,31 +118,45 @@ impl Recorder {
Ok(stream) 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()?; let stream = self.create_stream()?;
stream.play()?; stream.play()?;
println!("REC: {}", self.filename); println!("REC: {}", self.current_file);
self.interrupt.stream_wait(); self.interrupt_handles.stream_wait();
drop(stream); drop(stream);
self.writer.lock().unwrap().take().unwrap().finalize()?; self.writer.lock().unwrap().take().unwrap().finalize()?;
println!("STOP: {}", self.filename); println!("STOP: {}", self.current_file);
Ok(()) 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()?; let stream = self.create_stream()?;
stream.play()?; stream.play()?;
println!("REC: {}", self.filename); println!("REC: {}", self.current_file);
let now = std::time::Instant::now(); let now = std::time::Instant::now();
loop { while self.interrupt_handles.batch_is_running() {
std::thread::sleep(std::time::Duration::from_millis(500)); std::thread::sleep(std::time::Duration::from_millis(1));
if now.elapsed().as_secs() >= secs { if now.elapsed().as_secs() >= secs {
break; break;
} }
} }
drop(stream); drop(stream);
self.writer.lock().unwrap().take().unwrap().finalize()?; let writer = self.writer.clone();
println!("STOP: {}", self.filename); let current_file = self.current_file.clone();
std::thread::spawn(move || {
writer.lock().unwrap().take().unwrap().finalize().unwrap();
println!("STOP: {}", current_file);
});
Ok(()) Ok(())
} }
} }
@ -181,44 +176,68 @@ where
} }
} }
pub fn batch_recording(args: &Args, secs: u64, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> { fn batch_recording(rec: &mut Recorder, secs: u64) -> Result<(), Error> {
while interrupt_handles.batch_is_running() { let now = std::time::Instant::now();
let recorder = recorder::Recorder::init( while rec.interrupt_handles.batch_is_running() {
args.name.clone(), match rec.max_seconds {
match args.output.clone() { Some(max_secs) => {
Some(path) => path, if now.elapsed().as_secs() >= max_secs {
None => Path::new("./").to_path_buf(), break;
}, }
match args.host { }
Hosts::Alsa => cpal::HostId::Alsa, None => {}
Hosts::Jack => cpal::HostId::Jack, }
}, rec.record_secs(secs)?;
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)?;
} }
Ok(()) Ok(())
} }
pub fn contiguous_recording(args: &Args, interrupt_handles: InterruptHandles) -> Result<(), anyhow::Error> { fn continuous_recording(rec: &mut Recorder) -> Result<(), Error> {
let recorder = recorder::Recorder::init( rec.record()?;
Ok(())
}
#[cfg(target_os = "linux")]
fn match_host_platform(host: Hosts) -> Result<cpal::HostId, Error> {
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<cpal::HostId, Error> {
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<cpal::HostId, Error> {
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(), args.name.clone(),
match args.output.clone() { match args.output.clone() {
Some(path) => path, Some(path) => path,
None => Path::new("./").to_path_buf(), None => Path::new("./").to_path_buf(),
}, },
match args.host { match_host_platform(args.host)?,
Hosts::Alsa => cpal::HostId::Alsa,
Hosts::Jack => cpal::HostId::Jack,
},
args.sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE), args.sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE),
args.channels.unwrap_or(DEFAULT_CHANNEL_COUNT), args.channels.unwrap_or(DEFAULT_CHANNEL_COUNT),
args.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE), 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),
}
} }

View File

@ -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

View File

@ -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

0
configuration/ssh.txt Normal file
View File

View File

@ -0,0 +1 @@
<username>:<encrypted password>

View File

@ -0,0 +1,13 @@
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=<Insert 2 letter ISO 3166-1 country code here>
network={
scan_ssid=1
ssid="<Name of your wireless LAN>"
psk="<Password for your wireless LAN>"
proto=RSN
key_mgmt=WPA-PSK
pairwise=CCMP
auth_alg=OPEN
}

View File

@ -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
1 time and date Voltage of depth sensor (V) Depth (m)
2 2022-09-03T17:55:58 0.714 -0.13
3 2022-09-03T17:56:01 0.714 -0.13
4 2022-09-03T17:56:04 0.714 -0.19
5 2022-09-03T17:56:07 0.714 -0.19
6 2022-09-03T17:56:10 0.714 -0.19
7 2022-09-03T17:56:13 0.714 -0.13
8 2022-09-03T17:56:16 0.714 -0.19
9 2022-09-03T17:56:19 0.716 -0.19
10 2022-09-03T17:56:22 0.714 -0.13
11 2022-09-03T17:56:25 0.714 -0.19
12 2022-09-03T17:56:28 0.714 -0.19
13 2022-09-03T17:56:31 0.714 -0.19
14 2022-09-03T17:56:34 0.714 -0.19
15 2022-09-03T17:56:37 0.714 -0.13
16 2022-09-03T17:56:40 0.714 -0.13
17 2022-09-03T17:56:43 0.714 -0.19
18 2022-09-03T17:56:46 0.716 -0.13
19 2022-09-03T17:56:49 0.714 -0.13
20 2022-09-03T17:56:52 0.714 -0.13
21 2022-09-03T17:56:55 0.716 -0.19
22 2022-09-03T17:56:59 0.714 -0.19
23 2022-09-03T17:57:02 0.714 -0.19
24 2022-09-03T17:57:05 0.716 -0.13
25 2022-09-03T17:57:08 0.714 -0.13
26 2022-09-03T17:57:11 0.714 -0.19
27 2022-09-03T17:57:14 0.714 -0.13
28 2022-09-03T17:57:17 0.894 5.48
29 2022-09-03T17:57:20 0.986 8.54
30 2022-09-03T17:57:23 0.988 8.47
31 2022-09-03T17:57:26 1.368 20.58
32 2022-09-03T17:57:29 1.354 20.26
33 2022-09-03T17:57:32 0.714 -0.19
34 2022-09-03T17:57:35 0.716 -0.13
35 2022-09-03T17:57:38 0.714 -0.19
36 2022-09-03T17:57:41 0.714 -0.19
37 2022-09-03T17:57:44 0.714 -0.19
38 2022-09-03T17:57:47 0.716 -0.19
39 2022-09-03T17:57:50 0.714 -0.19
40 2022-09-03T17:57:53 0.714 -0.13
41 2022-09-03T17:57:56 0.714 -0.13
42 2022-09-03T17:57:59 0.714 -0.19
43 2022-09-03T17:58:02 0.714 -0.13

View File

@ -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
1 time and date Voltage of depth sensor (V) Depth (m)
2 2022-09-03T18:01:18 0.716 -0.13
3 2022-09-03T18:01:21 0.716 -0.13
4 2022-09-03T18:01:24 0.714 -0.19
5 2022-09-03T18:01:27 0.716 -0.13
6 2022-09-03T18:01:30 0.714 -0.19
7 2022-09-03T18:01:33 0.716 -0.13
8 2022-09-03T18:01:36 0.716 -0.13
9 2022-09-03T18:01:39 0.716 -0.13
10 2022-09-03T18:01:42 0.716 -0.13
11 2022-09-03T18:01:45 0.714 -0.19
12 2022-09-03T18:01:48 0.714 -0.19
13 2022-09-03T18:01:51 0.714 -0.19
14 2022-09-03T18:01:54 0.714 -0.19
15 2022-09-03T18:01:57 0.716 -0.13
16 2022-09-03T18:02:00 0.716 -0.13
17 2022-09-03T18:02:03 0.714 -0.19
18 2022-09-03T18:02:06 0.714 -0.19
19 2022-09-03T18:02:09 0.714 -0.19
20 2022-09-03T18:02:12 0.714 -0.19
21 2022-09-03T18:02:15 0.714 -0.19
22 2022-09-03T18:02:18 0.716 -0.13
23 2022-09-03T18:02:21 0.716 -0.13
24 2022-09-03T18:02:24 0.714 -0.19
25 2022-09-03T18:02:27 0.714 -0.19
26 2022-09-03T18:02:30 0.714 -0.19
27 2022-09-03T18:02:33 0.714 -0.19
28 2022-09-03T18:02:36 0.714 -0.19
29 2022-09-03T18:02:39 0.716 -0.13
30 2022-09-03T18:02:42 0.714 -0.19
31 2022-09-03T18:02:45 0.714 -0.19
32 2022-09-03T18:02:48 0.714 -0.19
33 2022-09-03T18:02:51 0.714 -0.19
34 2022-09-03T18:02:54 0.714 -0.19
35 2022-09-03T18:02:57 0.714 -0.19
36 2022-09-03T18:03:00 0.714 -0.19
37 2022-09-03T18:03:03 0.714 -0.19
38 2022-09-03T18:03:06 0.714 -0.19
39 2022-09-03T18:03:09 0.714 -0.19
40 2022-09-03T18:03:12 0.714 -0.19
41 2022-09-03T18:03:15 0.714 -0.19
42 2022-09-03T18:03:18 0.716 -0.13
43 2022-09-03T18:03:21 0.716 -0.13
44 2022-09-03T18:03:24 0.714 -0.19
45 2022-09-03T18:03:27 0.716 -0.13
46 2022-09-03T18:03:30 0.714 -0.19
47 2022-09-03T18:03:33 0.714 -0.19
48 2022-09-03T18:03:36 0.714 -0.19
49 2022-09-03T18:03:39 0.714 -0.19

View File

@ -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
1 time and date Voltage of depth sensor (V) Depth (m)
2 2022-09-04T11:50:21 0.716 -0.13
3 2022-09-04T11:50:24 0.716 -0.13
4 2022-09-04T11:50:27 0.716 -0.13
5 2022-09-04T11:50:31 1.12 12.74
6 2022-09-04T11:50:34 0.716 -0.13
7 2022-09-04T11:50:37 1.074 11.28
8 2022-09-04T11:50:40 1.154 13.82
9 2022-09-04T11:50:43 0.716 -0.13
10 2022-09-04T11:50:46 1.034 10.0
11 2022-09-04T11:50:49 1.234 16.37
12 2022-09-04T11:50:52 1.212 15.67
13 2022-09-04T11:50:55 0.714 -0.19
14 2022-09-04T11:50:58 0.716 -0.13
15 2022-09-04T11:51:01 0.716 -0.13
16 2022-09-04T11:51:04 0.716 -0.13
17 2022-09-04T11:51:07 0.716 -0.13

View File

@ -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
1 time and date Voltage of depth sensor (V) Depth (m)
2 2022-09-04T11:56:09 0.716 -0.13
3 2022-09-04T11:56:12 0.716 -0.13
4 2022-09-04T11:56:15 0.716 -0.13
5 2022-09-04T11:56:18 0.716 -0.13
6 2022-09-04T11:56:21 0.716 -0.13
7 2022-09-04T11:56:24 0.716 -0.13

View File

@ -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
1 GPStime utc latitude longitude speed sats in view
2 2022-08-20T09:23:40.000Z 60.196451148 24.960462239 0.582 0
3 2022-08-20T09:23:41.000Z 60.196446687 24.960462239 0.374 0
4 2022-08-20T09:23:42.000Z 60.196437765 24.960462239 0.257 7
5 2022-08-20T09:23:43.000Z 60.196441539 24.960486191 0.192 7
6 2022-08-20T09:23:44.000Z 60.196441539 24.960486191 0.121 7
7 2022-08-20T09:23:45.000Z 60.196437078 24.960486191 0.279 7
8 2022-08-20T09:23:46.000Z 60.196439678 24.960493798 0.233 7
9 2022-08-20T09:23:47.000Z 60.196439678 24.960493798 0.059 7
10 2022-08-20T09:23:48.000Z 60.196439678 24.960493798 0.124 7
11 2022-08-20T09:23:49.000Z 60.196446739 24.960501406 0.369 7

View File

@ -1,17 +1,23 @@
#!/usr/bin/python3 #!/usr/bin/python3
import argparse
import board import board
import time import time
import busio import busio
import adafruit_ads1x15.ads1015 as ADS import adafruit_ads1x15.ads1015 as ADS
from adafruit_ads1x15.analog_in import AnalogIn from adafruit_ads1x15.analog_in import AnalogIn
from time import sleep, strftime # from rpi_lcd import LCD
from rpi_lcd import LCD
try: parser = argparse.ArgumentParser(description='GPS Logger')
lcd = LCD(bus=2) parser.add_argument('-o', '--output', help='Output directory', required=True)
except OSError: parser.add_argument('-i', '--interval', help='Interval in seconds', required=False)
lcd = None
args = parser.parse_args()
# try:
# lcd = LCD(bus=2)
# except OSError:
# lcd = None
# Create the I2C bus # Create the I2C bus
i2c = busio.I2C(board.SCL, board.SDA) 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 # Create the ADC object using the I2C bus
ads = ADS.ADS1015(i2c) ads = ADS.ADS1015(i2c)
# Create a single-ended input on channel 1
depthS = AnalogIn(ads, ADS.P1)
# Create single-ended input on channel 0 # Create single-ended input on channel 0
# tmp36 = AnalogIn(ads, ADS.P0) # 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 # Subtract the offset from the sensor voltage
# and convert chan.voltage * (1 degree C / 0.01V) = Degrees Celcius # and convert chan.voltage * (1 degree C / 0.01V) = Degrees Celcius
# temperatureC = (tmp36.voltage - 0.5) / 0.01 # temperatureC = (tmp36.voltage - 0.5) / 0.01
# Open the file to write down the results # File to write down the results
timestr = time.strftime("%Y-%m-%dT%H-%M-%S") filename = args.output + "/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_depth_data.csv"
filename = "/home/shared/hydrophonitor/data/depth/" + timestr + "_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 #Attempting to round the figure to a more intelligible figure
#rounddepth = round(depthM, ndigits) #rounddepth = round(depthM, ndigits)
#psi = depthS.voltage * 104.1666667 - 75 #psi = depthS.voltage * 104.1666667 - 75
#bar = psi * 14.503773800722 #bar = psi * 14.503773800722
with open(filename, "w", 1) as f: 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") f.write("time and date, Voltage of depth sensor (V), Depth (m)\n")
try:
while True: while True:
voltage = depthS.voltage voltage = depthS.voltage
depthM = ((voltage * 31.848) - 22.93) depthM = ((voltage * 31.848) - 22.93)
rounddepth = round(depthM, 2) rounddepth = round(depthM, 2)
# roundtemp = round(temperatureC, 2)
roundvolts = round(voltage, 3) 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: # if lcd:
lcd.clear() # lcd.clear()
lcd.text((str(roundvolts) + " V ") + (str(rounddepth) + " m"), 1) # 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) 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)

View File

@ -0,0 +1,2 @@
Adafruit-Blinka==8.5.0
adafruit-circuitpython-ads1x15==2.2.21

View File

@ -1,29 +1,39 @@
#!/usr/bin/python3 #!/usr/bin/python3
from gps import * import gpsd
from time import sleep, strftime import time
import argparse
filename = "/home/shared/logger-raspi-setup/data/gps/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_GPS_data.csv" parser = argparse.ArgumentParser(description='GPS Logger')
# filename = "/mnt/myssd/GPS_Data" + timestr +".csv" 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: with open(filename, "w", 1) as f:
gpsd = gps(mode=WATCH_ENABLE|WATCH_NEWSTYLE) gpsd.connect()
f.write("GPStime utc,latitude,longitude,speed,sats in view\n")
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")
try:
while True: while True:
report = gpsd.next() try:
if report["class"] == "TPV": packet = gpsd.get_current()
GPStime = str(getattr(report,"time","")) gps_time_utc = str(packet.time) if packet.mode >= 2 else "-"
lat = str(getattr(report,"lat",0.0)) lat = str(packet.lat) if packet.mode >= 2 else "0.0"
lon = str(getattr(report,"lon",0.0)) lon = str(packet.lon) if packet.mode >= 2 else "0.0"
speed = str(getattr(report,"speed","nan")) speed = str(packet.hspeed) if packet.mode >= 2 else "0.0"
sats = str(len(gpsd.satellites)) sats = str(packet.sats)
system_time = time.strftime("%Y-%m-%dT%H-%M-%S")
f.write(GPStime + "," + lat +"," + lon + "," + speed + "," + sats + "\n") f.write(f"{system_time},{gps_time_utc},{lat},{lon},{speed},{sats}\n")
time.sleep(5)
except (KeyboardInterrupt, SystemExit): # when you press ctrl+c except (KeyboardInterrupt, SystemExit): # when you press ctrl+c
print("Done.\nExiting.") print("Exiting GPS recording.", flush=True)
f.close() break
except Exception as e:
print(f"GPS error: {e}", flush=True)
time.sleep(interval)

View File

@ -0,0 +1 @@
gpsd-py3==0.3.0

View File

@ -7,3 +7,8 @@ https://www.circuito.io/app?components=9443,200000,267055
# Setting up Jack on Raspberry Pi # Setting up Jack on Raspberry Pi
https://wiki.linuxaudio.org/wiki/raspberrypi 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

View File

@ -8,14 +8,10 @@ GOVERNOR="performance"
MAX_SPEED="0" MAX_SPEED="0"
MIN_SPEED="0" " | sudo tee -a /etc/default/cpufrequtils MIN_SPEED="0" " | sudo tee -a /etc/default/cpufrequtils
# Install other useful tools
sudo apt-get install htop git perl vim
# Set CPU governor # Set CPU governor
sudo sed -i 's/exit 0/sudo cpufreq-set -r -g performance/g' /etc/rc.local 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 sudo echo "exit 0" | sudo tee -a /etc/rc.local
# Set realtime priority and memlock # Set realtime priority and memlock
sudo echo " sudo echo "
@audio nice -15 @audio nice -15

View File

@ -0,0 +1,5 @@
#!/bin/bash
CONFIG_FILE=$1
export $(grep -v '^#' $CONFIG_FILE | tr -d '[:space:]' | xargs -d '\n')

View File

@ -1,5 +1,28 @@
#!/bin/sh #!/bin/bash
# Install jackd echo "Setting up audio recording"
# Audio setup utils # 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

View File

@ -1,8 +1,12 @@
#!/bin/sh #!/bin/sh
echo "Setting up GPS recording"
sudo apt-get update && sudo apt-get install -y \ sudo apt-get update && sudo apt-get install -y \
gpsd gpsd-clients gpsd gpsd-clients
sudo pip install -r /home/pi/hydrophonitor/gps-logger/requirements.txt
sudo systemctl stop gpsd.socket sudo systemctl stop gpsd.socket
sudo systemctl disable 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 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

View File

@ -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

View File

@ -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

View File

@ -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 cd /home/pi/hydrophonitor/audio-logger/target/release && ./$AUDIO_TARGET_EXECUTABLE $OPTIONS

View File

@ -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

View File

@ -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 &

View File

@ -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

107
setup.md
View File

@ -2,52 +2,135 @@
## Components ## 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 ## 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 <username> with the username of choice (e.g. pi)
- replace <encrypted password> 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 <Insert 2 letter ISO 3166-1 country code here> with your country code (e.g. FI)
- replace "<Name of your wireless LAN>" with the name of your Wi-Fi network, e.g. "explorersden"
- replace "<Password for your wireless LAN>" 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 <user>@<IP address>`, 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 `<user>@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 ## Test & run
todo

23
todo.md Normal file
View File

@ -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