Rewrite hydrophonitor as a NixOS RPi SD image (#19)
- Initially tested on Raspberry Pi 4B & Raspberry Pi 3B - Create user + password, enable SSH, connect to local network - Implemented modules: audio-recorder, gps-recorder, real-time-clock, shutdown-button - Deployment option with deploy-rs Signed-off-by: Satu Koskinen <satu.a.koskinen@gmail.com>
This commit is contained in:
parent
56d6d5ecbd
commit
b5e34360a7
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,2 +1,7 @@
|
||||
target
|
||||
*.wav
|
||||
**/target
|
||||
**/*.wav
|
||||
package
|
||||
result
|
||||
.wifi-name
|
||||
.wifi-password
|
||||
*.pem
|
8
Makefile
8
Makefile
@ -1,8 +0,0 @@
|
||||
install:
|
||||
cd scripts && setup-raspberry-pi.sh
|
||||
|
||||
build-audio:
|
||||
cd audio-logger && cargo build --release
|
||||
|
||||
start-all:
|
||||
cd scripts && start-all.sh
|
109
README.md
109
README.md
@ -1,35 +1,106 @@
|
||||
# Hydrophonitor
|
||||
|
||||
A software package to record audio and related metadata from a configuration
|
||||
of hydrophones.
|
||||
|
||||
For setup instructions, see `docs/setup.md`.
|
||||
A NixOS flake based operating system for Raspberry Pi 3 (tested on v1.2) and Raspberry Pi 4 (tested on 4B), configured for recording audio through hydrophones via a USB sound card, logging GPS data through a USB GPS dongle, and recording depth through a pressure sensor and an analog-to-digital converter (depth recording not implemented yet).
|
||||
|
||||
## 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
|
||||
audio-recorder | Receive an audio signal from the DAC and write it on disk in `.wav` format.
|
||||
gps-recorder | Record position and time of the device in `.json` format.
|
||||
*depth-recorder | Record depth of the device and save it in `.csv` format.
|
||||
real-time-clock | Configure and use an RTC to update system time from hardware clock when not connected to the internet.
|
||||
shutdown-button | Gracefully shut down the system using a button attached to the GPIO pin.
|
||||
*device-controls | Provide device control using physical buttons.
|
||||
|
||||
*) todo, not implemented yet
|
||||
|
||||
## Data Formats
|
||||
### 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
|
||||
Type | Output file format | Output file name | Output structure
|
||||
------------|--------------------|--------------------------------------|------------------
|
||||
Audio Data | .wav | `<start-time-of-recording>.wav` | Each recorded batch will be written to its own file in `/output/audio` folder
|
||||
GPS Data | .json | `<start-time-of-recording>.json` | All data written to a single file in `/output/gps`
|
||||
|
||||
## Output Locations
|
||||
### Notes on Modules and Configuration Options
|
||||
|
||||
The location for the output directories is defined by a configurable value OUTPUT_PATH in `hydrophonitor-config.txt`. 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 (DEFAULT_OUTPUT_PATH in hydrophonitor-config.txt) instead.
|
||||
- `audio-recorder` module
|
||||
- `audio-recorder` systemd service connects to a USB soundcard and starts recording by default in batches to a specified output directory
|
||||
- configuration options and default values:
|
||||
- `output-folder` (/output/audio)
|
||||
- `sample-rate` (192000)
|
||||
- `sample-format` (S32_LE)
|
||||
- `channels` (4)
|
||||
- `max-file-time-secs`(300)
|
||||
- `gps-recorder` module: fetched from hydrophonitor-gps repository
|
||||
- enables gpsd
|
||||
- `gps-recorder` systemd service starts `gps-recorder` program that listens for and logs GPS data to a json file in a specified directory
|
||||
- configuration options and default values:
|
||||
- `output-folder` (/output/gps)
|
||||
- `interval-secs` (10)
|
||||
- `real-time-clock` module
|
||||
- `i2c-rtc-start` systemd service configures I2C bus to connect to DS3231 RTC attached to GPIO pins of the RPi and updates system time to hardware clock time
|
||||
- configuration options and default values:
|
||||
- `i2c-bus` (1)
|
||||
- `shutdown-button` module (currently only for RPi 4)
|
||||
- `shutdown-button` service runs `shutdown-button` program that listens for a button press (by default from GPIO pin 21) and once button press is detected, runs a graceful shutdown
|
||||
- configuration options yet to be implemented in the program:
|
||||
- `gpio-pin` (21)
|
||||
- `shutdown-press-secs` (1)
|
||||
|
||||
SSD card mounting is not yet configured in the setup.
|
||||
|
||||
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.
|
||||
## Other Configurations
|
||||
|
||||
- user `kaskelotti` is created with sudo privileges
|
||||
- ssh enabled with password authentication
|
||||
- `i2c-dev`, `i2c_bcm2708`, and `rtc_1307` kernel modules enabled for i2c-rtc, `i2c-tools` package installed
|
||||
- `deploy-rs` used for deployments (after initial bootstrapping and connecting the pi over wifi or ethernet)
|
||||
|
||||
### Enabling WiFi
|
||||
|
||||
Add the following to targets/<raspberry-pi-model>/default.nix where "SSID name" is replaced with the network name and "SSID password" is the network password:
|
||||
|
||||
```nix
|
||||
networking = {
|
||||
hostName = "kaskelotti";
|
||||
wireless = {
|
||||
enable = true;
|
||||
networks = {
|
||||
"SSID name" = {
|
||||
psk = "SSID password";
|
||||
};
|
||||
};
|
||||
interfaces = ["wlan0"];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Building the Image on Mac M1
|
||||
|
||||
Image has been built with UTM (virtualization for silicon Macs, now running on 2020 M1 Air) on Ubuntu 22.04 VM (with nix installed) with the following command:
|
||||
```
|
||||
nix build .#systems.raspberry-pi-4.config.system.build.sdImage --extra-experimental-features "nix-command flakes"
|
||||
```
|
||||
The result image has to be copied from the VM nix store path to the directory that was shared between the client and the host.
|
||||
```
|
||||
cp -rL result/sd-image/nixos-sd-image-23.11.20230908.db9208a-aarch64-linux.img .
|
||||
```
|
||||
|
||||
Initially, image is flashed to the SD card with the following command (on the host)
|
||||
```
|
||||
diskutil list # check SD card, here /dev/disk4
|
||||
diskutil unmountdisk /dev/disk4
|
||||
sudo dd if=../UTM/hydrophonitor/nixos-sd-image-23.11.20230702.0fbe93c-aarch64-linux.img of=/dev/disk4 status=progress
|
||||
diskutil eject /dev/disk4
|
||||
```
|
||||
|
||||
After bootstrapping and connecting the Pi to the local network, deploy-rs can be used for deployments from UTM (update correct IP address to the deploy configuration):
|
||||
```
|
||||
nix run github:serokell/deploy-rs .#raspberry-pi-4
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- On Raspberry Pi 4, we use the [Argon One m.2](https://argon40.com/products/argon-one-m-2-case-for-raspberry-pi-4) case with [Intenso M.2 TOP SATA III 1TB SSD](https://www.intenso.de/en/products/solid-state-drives/M.2-SSD-Top) and boot from SSD. We lacked a USB extension cable that would have allowed us to flash the SD card image directly to the SSD from the computer, so we ended up booting the Pi from a USB stick, copying the SD card image to the system on the USB stick, and from there flashing the image with `dd` to the SSD (which showed up as /dev/sda). After removing the USB stick, the Raspberry Pi successfully booted from the SSD. The setup also works using an ordinary SD card without an SSD. It would also be possible to run the operating system from the SD card, add an additional SSD, partition and format it and configure the outputs to be written to the SSD.
|
||||
- On Raspberry Pi 3, we only tested with an SD card without the SSD, and witnessed some buffer overruns with `arecord` with the default settings, i.e. some audio output is lost. Also, running the `shutdown-button` program on Raspberry Pi 3 failed with permission denied on /dev/mem, even though the service is run as root.
|
||||
|
||||
|
3
audio-logger/.gitignore
vendored
3
audio-logger/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
target
|
||||
*.wav
|
||||
recordings
|
1430
audio-logger/Cargo.lock
generated
1430
audio-logger/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "audio"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
nix = "0.23"
|
||||
libc = "0.2.65"
|
||||
parking_lot = "0.12"
|
||||
jack = { version = "0.9", optional = true }
|
@ -1,16 +0,0 @@
|
||||
all:
|
||||
cargo build --release
|
||||
|
||||
test: all
|
||||
mkdir -p recordings
|
||||
bash run_test.sh
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
fclean: clean
|
||||
rm -rf recordings
|
||||
|
||||
re: fclean all
|
||||
|
||||
.PHONY: all clean fclean re test
|
@ -1,29 +0,0 @@
|
||||
# Audio Logger
|
||||
|
||||
CLI-tool for recording audio in a Linux environment.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
|
||||
USAGE:
|
||||
audio-logger [OPTIONS] --name <NAME> <HOST>
|
||||
|
||||
ARGS:
|
||||
<HOST> Host API to use [possible values: alsa, jack]
|
||||
|
||||
OPTIONS:
|
||||
-b, --batch-recording <SECONDS> (optional) Will record in [SECONDS] batches
|
||||
--buffer-size <FRAMES> Buffer size in frames
|
||||
--channels <CHANNELS> Channels to record
|
||||
-h, --help Print help information
|
||||
-n, --name <NAME> Filename will be `[NAME]-yyyy-mm-dd-H:M:S.wav`
|
||||
-o, --output <PATH> Path to save the file(s)
|
||||
--print-configs Output the available devices and their configurations
|
||||
--sample-rate <SAMPLE_RATE> Sample rate in Hz (default = 44,000Hz)
|
||||
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Use the Makefile commands to build the project and run a simple test.
|
@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
target/release/audio rec \
|
||||
--name test \
|
||||
--output recordings/ \
|
||||
--batch-recording 3 \
|
||||
--sample-rate 44100 \
|
||||
--channels 2 \
|
||||
--buffer-size 1024 \
|
||||
alsa \
|
@ -1,100 +0,0 @@
|
||||
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 = "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)]
|
||||
pub name: String,
|
||||
|
||||
/// Path to save the file(s)
|
||||
#[clap(long, short, value_parser, value_name = "PATH", value_hint = clap::ValueHint::DirPath)]
|
||||
pub output: Option<std::path::PathBuf>,
|
||||
|
||||
/// (optional) Will record in [SECONDS] batches
|
||||
#[clap(short, long, value_name = "SECONDS")]
|
||||
pub batch_recording: Option<u64>,
|
||||
|
||||
/// (optional) Will record for a total of [SECONDS]
|
||||
#[clap(short, long, value_name = "MAX_SECONDS")]
|
||||
pub max_seconds: Option<u64>,
|
||||
|
||||
/// Host API to use
|
||||
#[clap(value_enum)]
|
||||
pub host: Hosts,
|
||||
|
||||
/// Sample rate in Hz (default = 44,000Hz)
|
||||
#[clap(long)]
|
||||
pub sample_rate: Option<u32>,
|
||||
|
||||
/// Channels to record
|
||||
#[clap(long, value_name = "CHANNELS")]
|
||||
pub channels: Option<u16>,
|
||||
|
||||
/// Buffer size in frames
|
||||
#[clap(long, value_name = "FRAMES")]
|
||||
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,
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
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;
|
@ -1,113 +0,0 @@
|
||||
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}-{:02}-{:02}_{:02}:{:02}:{: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()
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
//! # 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;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Commands::Rec(args) => {
|
||||
record(&args)?;
|
||||
},
|
||||
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(())
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
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(())
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
//! 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(())
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
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();
|
||||
println!("Available hosts:\n {:?}", available_hosts);
|
||||
|
||||
for host_id in available_hosts {
|
||||
println!("{}", host_id.name());
|
||||
let host = cpal::host_from_id(host_id)?;
|
||||
|
||||
let default_in = host.default_input_device().map(|e| e.name().unwrap());
|
||||
let default_out = host.default_output_device().map(|e| e.name().unwrap());
|
||||
println!(" Default Input Device:\n {:?}", default_in);
|
||||
println!(" Default Output Device:\n {:?}", default_out);
|
||||
|
||||
let devices = host.devices()?;
|
||||
println!(" Devices: ");
|
||||
for (device_index, device) in devices.enumerate() {
|
||||
println!(" {}. \"{}\"", device_index + 1, device.name()?);
|
||||
|
||||
// Input configs
|
||||
if let Ok(conf) = device.default_input_config() {
|
||||
println!(" Default input stream config:\n {:?}", conf);
|
||||
}
|
||||
let input_configs = match device.supported_input_configs() {
|
||||
Ok(f) => f.collect(),
|
||||
Err(e) => {
|
||||
println!(" Error getting supported input configs: {:?}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !input_configs.is_empty() {
|
||||
println!(" All supported input stream configs:");
|
||||
for (config_index, config) in input_configs.into_iter().enumerate() {
|
||||
println!(
|
||||
" {}.{}. {:?}",
|
||||
device_index + 1,
|
||||
config_index + 1,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Output configs
|
||||
if let Ok(conf) = device.default_output_config() {
|
||||
println!(" Default output stream config:\n {:?}", conf);
|
||||
}
|
||||
let output_configs = match device.supported_output_configs() {
|
||||
Ok(f) => f.collect(),
|
||||
Err(e) => {
|
||||
println!(" Error getting supported output configs: {:?}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !output_configs.is_empty() {
|
||||
println!(" All supported output stream configs:");
|
||||
for (config_index, config) in output_configs.into_iter().enumerate() {
|
||||
println!(
|
||||
" {}.{}. {:?}",
|
||||
device_index + 1,
|
||||
config_index + 1,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,246 +0,0 @@
|
||||
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>>>>>;
|
||||
|
||||
pub struct Recorder {
|
||||
writer: WriteHandle,
|
||||
interrupt_handles: InterruptHandles,
|
||||
default_config: SupportedStreamConfig,
|
||||
user_config: StreamConfig,
|
||||
device: Device,
|
||||
spec: hound::WavSpec,
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
current_file: String,
|
||||
max_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// # 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 {
|
||||
|
||||
/// # Init
|
||||
///
|
||||
/// Initializes the recorder with the given host, sample rate, channel count, and buffer size.
|
||||
pub fn init(
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
host: HostId,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
buffer_size: u32,
|
||||
max_seconds: Option<u64>,
|
||||
) -> Result<Self, Error> {
|
||||
|
||||
// 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 = get_device(host)?;
|
||||
|
||||
// Get default config for the device.
|
||||
let default_config = get_default_config(&device)?;
|
||||
|
||||
// Override certain fields of the default stream config with the user's config.
|
||||
let user_config = get_user_config(sample_rate, channels, buffer_size)?;
|
||||
|
||||
// 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(None)),
|
||||
interrupt_handles,
|
||||
default_config,
|
||||
user_config,
|
||||
device,
|
||||
spec,
|
||||
name,
|
||||
path,
|
||||
current_file: "".to_string(),
|
||||
max_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
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 config = self.user_config.clone();
|
||||
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(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<f32, f32>(data, &writer),
|
||||
err_fn,
|
||||
)?,
|
||||
cpal::SampleFormat::I16 => self.device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<i16, i16>(data, &writer),
|
||||
err_fn,
|
||||
)?,
|
||||
cpal::SampleFormat::U16 => self.device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<u16, i16>(data, &writer),
|
||||
err_fn,
|
||||
)?,
|
||||
};
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// # 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.current_file);
|
||||
self.interrupt_handles.stream_wait();
|
||||
drop(stream);
|
||||
self.writer.lock().unwrap().take().unwrap().finalize()?;
|
||||
println!("STOP: {}", self.current_file);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # 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()?;
|
||||
let date_time = get_date_time_string();
|
||||
println!("REC[{}]: {}", date_time, self.current_file);
|
||||
let now = std::time::Instant::now();
|
||||
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);
|
||||
let writer = self.writer.clone();
|
||||
let current_file = self.current_file.clone();
|
||||
std::thread::spawn(move || {
|
||||
writer.lock().unwrap().take().unwrap().finalize().unwrap();
|
||||
// Print STOP and the time and date
|
||||
let date_time = get_date_time_string();
|
||||
println!("STOP[{}]: {}", date_time, current_file);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input_data<T, U>(input: &[T], writer: &WriteHandle)
|
||||
where
|
||||
T: cpal::Sample,
|
||||
U: cpal::Sample + hound::Sample,
|
||||
{
|
||||
if let Ok(mut guard) = writer.try_lock() {
|
||||
if let Some(writer) = guard.as_mut() {
|
||||
for &sample in input.iter() {
|
||||
let sample: U = cpal::Sample::from(&sample);
|
||||
writer.write_sample(sample).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
fn continuous_recording(rec: &mut Recorder) -> Result<(), Error> {
|
||||
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(),
|
||||
match args.output.clone() {
|
||||
Some(path) => path,
|
||||
None => Path::new("./").to_path_buf(),
|
||||
},
|
||||
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),
|
||||
args.max_seconds,
|
||||
)?;
|
||||
|
||||
match args.batch_recording {
|
||||
Some(seconds) => batch_recording(&mut recorder, seconds),
|
||||
None => continuous_recording(&mut recorder),
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import argparse
|
||||
import time
|
||||
import board
|
||||
import busio
|
||||
import adafruit_ads1x15.ads1015 as ADS
|
||||
from adafruit_extended_bus import ExtendedI2C as I2C
|
||||
from adafruit_ads1x15.analog_in import AnalogIn
|
||||
|
||||
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)
|
||||
parser.add_argument('-b', '--bus', help='Custom i2c bus to use', required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create the I2C bus
|
||||
if args.bus:
|
||||
i2c_bus = args.bus
|
||||
i2c = I2C(i2c_bus)
|
||||
else:
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
# File to write down the results
|
||||
filename = args.output + "/" + time.strftime("%Y-%m-%dT%H-%M-%S") + "_depth_data.csv"
|
||||
|
||||
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")
|
||||
|
||||
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"), flush=True)
|
||||
|
||||
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)
|
@ -1,3 +0,0 @@
|
||||
Adafruit-Blinka==8.5.0
|
||||
adafruit-circuitpython-ads1x15==2.2.21
|
||||
adafruit-extended-bus==1.0.2
|
215
flake.lock
Normal file
215
flake.lock
Normal file
@ -0,0 +1,215 @@
|
||||
{
|
||||
"nodes": {
|
||||
"deploy-rs": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"utils": "utils"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694158470,
|
||||
"narHash": "sha256-yWx9eBDHt6WR3gr65+J85KreHdMypty/P6yM35tIYYM=",
|
||||
"owner": "serokell",
|
||||
"repo": "deploy-rs",
|
||||
"rev": "d0cfc042eba92eb206611c9e8784d41a2c053bab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "serokell",
|
||||
"repo": "deploy-rs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1668681692,
|
||||
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "009399224d5e398d03b22badca40a37ac85412a1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"hydrophonitor-gps": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694371631,
|
||||
"narHash": "sha256-o2lx75F0VnFnBKajbVbEUuQRcbcSHdCxjUVjdaAoAn8=",
|
||||
"owner": "nordic-dev-net",
|
||||
"repo": "hydrophonitor-gps",
|
||||
"rev": "d13bb78ce2d16b842f0be802f9d669ba32a35c3d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nordic-dev-net",
|
||||
"repo": "hydrophonitor-gps",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixos-hardware": {
|
||||
"locked": {
|
||||
"lastModified": 1693718952,
|
||||
"narHash": "sha256-+nGdJlgTk0MPN7NygopipmyylVuAVi7OItIwTlwtGnw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "793de77d9f83418b428e8ba70d1e42c6507d0d35",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "master",
|
||||
"repo": "nixos-hardware",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1671417167,
|
||||
"narHash": "sha256-JkHam6WQOwZN1t2C2sbp1TqMv3TVRjzrdoejqfefwrM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bb31220cca6d044baa6dc2715b07497a2a7c4bc7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1694183432,
|
||||
"narHash": "sha256-YyPGNapgZNNj51ylQMw9lAgvxtM2ai1HZVUu3GS8Fng=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1694183432,
|
||||
"narHash": "sha256-YyPGNapgZNNj51ylQMw9lAgvxtM2ai1HZVUu3GS8Fng=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"deploy-rs": "deploy-rs",
|
||||
"flake-utils": "flake-utils",
|
||||
"hydrophonitor-gps": "hydrophonitor-gps",
|
||||
"nixos-hardware": "nixos-hardware",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
94
flake.nix
Normal file
94
flake.nix
Normal file
@ -0,0 +1,94 @@
|
||||
{
|
||||
description = "NixOS Raspberry Pi configuration flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs = {
|
||||
url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
deploy-rs.url = "github:serokell/deploy-rs";
|
||||
hydrophonitor-gps.url = "github:nordic-dev-net/hydrophonitor-gps";
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
nixos-hardware,
|
||||
flake-utils,
|
||||
deploy-rs,
|
||||
hydrophonitor-gps,
|
||||
...
|
||||
}: let
|
||||
forEachSystem = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"];
|
||||
forEachPkgs = f: forEachSystem (sys: f (nixpkgs.legacyPackages.${sys}));
|
||||
system = "aarch64-linux";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config = {allowUnfree = true;};
|
||||
overlays = [
|
||||
(final: super: {
|
||||
makeModulesClosure = x:
|
||||
super.makeModulesClosure (x // {allowMissing = true;});
|
||||
})
|
||||
];
|
||||
};
|
||||
in {
|
||||
systems = {
|
||||
raspberry-pi-4 = nixpkgs.lib.nixosSystem {
|
||||
system = "aarch64-linux";
|
||||
specialArgs = {inherit pkgs;};
|
||||
modules = [
|
||||
./targets/raspberry-pi-4
|
||||
./modules/audio-recorder.nix
|
||||
./modules/real-time-clock/i2c-rtc.nix
|
||||
./modules/shutdown-button/service.nix
|
||||
nixos-hardware.nixosModules.raspberry-pi-4
|
||||
hydrophonitor-gps.nixosModules.hydrophonitor-gps
|
||||
"${nixpkgs}/nixos/modules/installer/sd-card/sd-image-aarch64.nix"
|
||||
];
|
||||
};
|
||||
|
||||
raspberry-pi-3 = nixpkgs.lib.nixosSystem {
|
||||
system = "aarch64-linux";
|
||||
specialArgs = {inherit pkgs;};
|
||||
modules = [
|
||||
./targets/raspberry-pi-3
|
||||
./modules/audio-recorder.nix
|
||||
./modules/real-time-clock/i2c-rtc.nix
|
||||
hydrophonitor-gps.nixosModules.hydrophonitor-gps
|
||||
"${nixpkgs}/nixos/modules/installer/sd-card/sd-image-aarch64.nix"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
deploy.nodes = {
|
||||
raspberry-pi-4 = {
|
||||
hostname = "192.168.1.76";
|
||||
profiles.system = {
|
||||
sshUser = "kaskelotti";
|
||||
sshOpts = ["-t"];
|
||||
magicRollback = false;
|
||||
path =
|
||||
deploy-rs.lib.aarch64-linux.activate.nixos
|
||||
self.systems.raspberry-pi-4;
|
||||
user = "root";
|
||||
};
|
||||
};
|
||||
|
||||
raspberry-pi-3 = {
|
||||
hostname = "192.168.1.117";
|
||||
profiles.system = {
|
||||
sshUser = "kaskelotti";
|
||||
sshOpts = ["-t"];
|
||||
magicRollback = false;
|
||||
path =
|
||||
deploy-rs.lib.aarch64-linux.activate.nixos
|
||||
self.systems.raspberry-pi-3;
|
||||
user = "root";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
formatter = forEachPkgs (pkgs: pkgs.alejandra);
|
||||
};
|
||||
}
|
8
flash.sh
Normal file
8
flash.sh
Normal file
@ -0,0 +1,8 @@
|
||||
# Flash image to sd-card
|
||||
#
|
||||
# Check sd-card device name with lsblk
|
||||
|
||||
SDCARD=$1
|
||||
IMAGE=$2
|
||||
|
||||
sudo dd if=${IMAGE} of=/dev/${SDCARD} status=progress
|
@ -1,39 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import gpsd
|
||||
import time
|
||||
import argparse
|
||||
|
||||
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.connect()
|
||||
|
||||
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")
|
||||
|
||||
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}, trying again...", flush=True)
|
||||
|
||||
time.sleep(interval)
|
@ -1 +0,0 @@
|
||||
gpsd-py3==0.3.0
|
@ -1,27 +0,0 @@
|
||||
# Audio
|
||||
|
||||
SAMPLE_RATE=44100
|
||||
CHANNELS=2
|
||||
BITRATE=192k
|
||||
|
||||
# in seconds
|
||||
BATCH_RECORD_LENGTH=60
|
||||
|
||||
# GPS
|
||||
|
||||
# in seconds
|
||||
GPS_INTERVAL=5
|
||||
|
||||
# Depth
|
||||
|
||||
# in seconds
|
||||
DEPTH_INTERVAL=5
|
||||
|
||||
# Output location
|
||||
|
||||
# TODO
|
||||
#TRY_MOUNT_SSD=true
|
||||
OUTPUT_PATH=/home/pi/data
|
||||
|
||||
HOME_PATH=/home/pi
|
||||
DEFAULT_OUTPUT_PATH=/home/pi/recordings
|
70
modules/audio-recorder.nix
Normal file
70
modules/audio-recorder.nix
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
options = with lib; {
|
||||
services.audio-recorder = {
|
||||
enable = mkEnableOption "Whether to enable the audio recording service.";
|
||||
|
||||
output-folder = mkOption {
|
||||
type = types.str;
|
||||
default = "/output/audio";
|
||||
description = "The folder to save recordings to.";
|
||||
};
|
||||
|
||||
sample-rate = mkOption {
|
||||
type = types.int;
|
||||
default = 44100;
|
||||
description = "The sample rate to use for recording.";
|
||||
};
|
||||
|
||||
sample-format = mkOption {
|
||||
type = types.str;
|
||||
default = "S32_LE";
|
||||
description = "The sample format to use for recording.";
|
||||
};
|
||||
|
||||
channels = mkOption {
|
||||
type = types.int;
|
||||
default = 2;
|
||||
description = "The amount of channels to use.";
|
||||
};
|
||||
|
||||
max-file-time-secs = mkOption {
|
||||
type = types.int;
|
||||
default = 600;
|
||||
description = "The maximum length of a recording in seconds.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.services.audio-recorder.enable {
|
||||
systemd.services.audio-recorder = {
|
||||
description = "Audio Recording Service";
|
||||
wantedBy = ["multi-user.target"];
|
||||
script = ''
|
||||
#!/usr/bin/env bash
|
||||
set -x
|
||||
${pkgs.coreutils}/bin/mkdir -p ${config.services.audio-recorder.output-folder}
|
||||
${pkgs.alsaUtils}/bin/arecord -l
|
||||
CARD_ID=$(${pkgs.alsaUtils}/bin/arecord -l | grep "USB Audio" | grep -oP '(?<=card )\d')
|
||||
DEVICE_ID=$(${pkgs.alsaUtils}/bin/arecord -l | grep "USB Audio" | grep -oP '(?<=device )\d')
|
||||
${pkgs.alsaUtils}/bin/arecord \
|
||||
--device hw:$CARD_ID,$DEVICE_ID \
|
||||
--format ${config.services.audio-recorder.sample-format} \
|
||||
--max-file-time ${toString config.services.audio-recorder.max-file-time-secs} \
|
||||
--rate ${toString config.services.audio-recorder.sample-rate} \
|
||||
--channels ${toString config.services.audio-recorder.channels} \
|
||||
--use-strftime \
|
||||
${config.services.audio-recorder.output-folder}/%Y-%m-%d_%H-%M-%S.wav
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = "root"; # Replace with appropriate user
|
||||
Restart = "always";
|
||||
};
|
||||
startLimitIntervalSec = 0;
|
||||
};
|
||||
};
|
||||
}
|
49
modules/real-time-clock/i2c-rtc.nix
Normal file
49
modules/real-time-clock/i2c-rtc.nix
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
options = with lib; {
|
||||
services.i2c-rtc-start = {
|
||||
enable = mkEnableOption "Whether to enable the audio recording service.";
|
||||
|
||||
i2c-bus = mkOption {
|
||||
type = types.int;
|
||||
default = 1;
|
||||
description = "The I2C bus to use.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.services.i2c-rtc-start.enable {
|
||||
systemd.services.i2c-rtc-start = {
|
||||
description = "RTC start service; reads the RTC and sets the system time from it";
|
||||
wantedBy = ["multi-user.target"];
|
||||
requires = ["systemd-modules-load.service"];
|
||||
after = ["systemd-modules-load.service"];
|
||||
script = ''
|
||||
#!/usr/bin/env bash
|
||||
set -x
|
||||
# Remove the i2c device if it already exists
|
||||
if [ -e /sys/class/i2c-adapter/i2c-${toString config.services.i2c-rtc-start.i2c-bus}/new_device ]; then
|
||||
echo "Try deleting existing i2c device"
|
||||
echo 0x68 | tee /sys/class/i2c-adapter/i2c-${toString config.services.i2c-rtc-start.i2c-bus}/delete_device || true
|
||||
fi
|
||||
echo "Adding i2c device"
|
||||
echo ds1307 0x68 | tee /sys/class/i2c-adapter/i2c-${toString config.services.i2c-rtc-start.i2c-bus}/new_device
|
||||
echo "Current hwclock time:"
|
||||
/run/current-system/sw/bin/hwclock -r
|
||||
echo "Current system time:"
|
||||
date
|
||||
echo "Setting system time from hwclock"
|
||||
/run/current-system/sw/bin/hwclock -s
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = "root";
|
||||
Type = "oneshot";
|
||||
};
|
||||
startLimitIntervalSec = 0;
|
||||
};
|
||||
};
|
||||
}
|
41
modules/shutdown-button/Cargo.lock
generated
Normal file
41
modules/shutdown-button/Cargo.lock
generated
Normal file
@ -0,0 +1,41 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
|
||||
[[package]]
|
||||
name = "rppal"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb26758c881b4837b2f4aef569e4251f75388e36b37204e1804ef429c220121c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_gpiozero"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef0e15226913de082030649dc55bb1db057ffc1c7c2b418454912a85b40523f3"
|
||||
dependencies = [
|
||||
"rppal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shutdown-button"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rust_gpiozero",
|
||||
]
|
9
modules/shutdown-button/Cargo.toml
Normal file
9
modules/shutdown-button/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "shutdown-button"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rust_gpiozero = "^0.2"
|
16
modules/shutdown-button/default.nix
Normal file
16
modules/shutdown-button/default.nix
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
rustPlatform,
|
||||
pkg-config,
|
||||
pkgs,
|
||||
}:
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "shutdown-button";
|
||||
version = "0.1.0";
|
||||
|
||||
src = ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
}
|
43
modules/shutdown-button/service.nix
Normal file
43
modules/shutdown-button/service.nix
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
shutdownButton = pkgs.callPackage ./default.nix {};
|
||||
in {
|
||||
options = {
|
||||
services.shutdown-button = {
|
||||
enable = lib.mkEnableOption "Whether to enable the shutdown-button service.";
|
||||
|
||||
gpio-pin = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 21;
|
||||
description = "GPIO pin number to which button is connected";
|
||||
};
|
||||
|
||||
shutdown-press-secs = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 1;
|
||||
description = "How many seconds button must be pushed down to trigger shutdown signal";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.services.shutdown-button.enable {
|
||||
systemd.services.shutdown-button = {
|
||||
description = "Shutdown button service";
|
||||
wantedBy = ["multi-user.target"];
|
||||
script = ''
|
||||
#!/usr/bin/env bash
|
||||
set -x
|
||||
${shutdownButton}/bin/shutdown-button --gpio-pin ${toString config.services.shutdown-button.gpio-pin} --shutdown-press-secs ${toString config.services.shutdown-button.shutdown-press-secs}
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = "root"; # Replace with appropriate user
|
||||
Restart = "always";
|
||||
};
|
||||
startLimitIntervalSec = 0;
|
||||
};
|
||||
};
|
||||
}
|
25
modules/shutdown-button/src/main.rs
Normal file
25
modules/shutdown-button/src/main.rs
Normal file
@ -0,0 +1,25 @@
|
||||
//! Display message in console when a Button is pressed
|
||||
// use rust_gpiozero::{Button, Debounce};
|
||||
use rust_gpiozero::{Button};
|
||||
// use std::time::Duration;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
// Create a button which is attached to Pin 21
|
||||
let mut button = Button::new(21);
|
||||
// Add debouncing so that subsequent presses within 100ms don't trigger a press
|
||||
// .debounce(Duration::from_millis(100));
|
||||
|
||||
// Add an async interrupt to trigger whenever the button is pressed
|
||||
// button
|
||||
// .when_pressed(|_| {
|
||||
// println!("button pressed");
|
||||
// })
|
||||
// .unwrap();
|
||||
println!("Waiting for shutdown button press");
|
||||
button.wait_for_press(None);
|
||||
println!("Shutdown button pressed");
|
||||
Command::new("shutdown")
|
||||
.output()
|
||||
.expect("failed to execute shutdown command");
|
||||
}
|
@ -1 +0,0 @@
|
||||
<username>:<encrypted password>
|
@ -1,13 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Install utils for cpu freq
|
||||
sudo apt-get install cpufrequtils
|
||||
sudo cpufreq-set -r -g performance
|
||||
sudo echo "ENABLE="true"
|
||||
GOVERNOR="performance"
|
||||
MAX_SPEED="0"
|
||||
MIN_SPEED="0" " | sudo tee -a /etc/default/cpufrequtils
|
||||
|
||||
# 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
|
||||
@audio - rtprio 90 # maximum realtime priority
|
||||
@audio - memlock unlimited # maximum locked-in-memory address space (KB)
|
||||
" | sudo tee -a /etc/security/limits.conf
|
||||
|
||||
# Set swappiness
|
||||
# This setting changes the so-called swappiness of your system,
|
||||
# or in other words, the moment when your system starts to use its swap partition.
|
||||
sudo echo "
|
||||
vm.swappiness = 10
|
||||
fs.inotify.max_user_watches = 524288
|
||||
" | sudo tee /etc/sysctl.conf
|
@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -eq 1 ]; then
|
||||
CONFIG_FILE=$1
|
||||
else
|
||||
CONFIG_FILE=/boot/hydrophonitor/hydrophonitor-config.txt
|
||||
fi
|
||||
|
||||
# Select non-comment lines in CONFIG_FILE, clean horizontal whitespace
|
||||
args=$(grep -v '^#' $CONFIG_FILE | tr -d '[:blank:]' | tr '\n' ' ')
|
||||
|
||||
for arg in $args; do
|
||||
echo "export $arg"
|
||||
export "${arg?}"
|
||||
done
|
@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
for sysdevpath in $(find /sys/bus/usb/devices/usb*/ -name dev); do
|
||||
(
|
||||
syspath="${sysdevpath%/dev}"
|
||||
devname="$(udevadm info -q name -p $syspath)"
|
||||
[[ "$devname" == "bus/"* ]] && exit
|
||||
eval "$(udevadm info -q property --export -p $syspath)"
|
||||
[[ -z "$ID_SERIAL" ]] && exit
|
||||
echo "/dev/$devname - $ID_SERIAL"
|
||||
)
|
||||
done
|
@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
echo "Setting up audio recording"
|
||||
|
||||
# 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
|
||||
|
||||
config="pcm.!default {
|
||||
type plug
|
||||
slave {
|
||||
pcm \"hw:$card_number,0\"
|
||||
}
|
||||
}
|
||||
|
||||
ctl.!default {
|
||||
type hw
|
||||
card $card_number
|
||||
}"
|
||||
|
||||
if ! grep -q "$config" /etc/asound.conf; then
|
||||
echo "$config" | sudo tee -a /etc/asound.conf
|
||||
fi
|
||||
|
||||
cd $HOME/hydrophonitor/audio-logger && cargo build --release
|
@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
echo "Setting up GPS recording"
|
||||
|
||||
sudo apt-get update && sudo apt-get install -y gpsd gpsd-clients
|
||||
|
||||
sudo pip install -r $HOME/hydrophonitor/gps-logger/requirements.txt
|
||||
|
||||
sudo systemctl stop gpsd.socket
|
||||
sudo systemctl disable gpsd.socket
|
||||
|
||||
device="/dev/ttyUSB0"
|
||||
|
||||
sudo gpsd ${device} -F /var/run/gpsd.sock
|
||||
|
||||
sudo sed -i "s|DEVICES=\"\"|DEVICES=\"${device}\"|g" /etc/default/gpsd
|
||||
|
||||
config="START_DAEMON=\"true\""
|
||||
|
||||
if ! grep -q "$config" /etc/default/gpsd; then
|
||||
echo "$config" | sudo tee -a /etc/default/gpsd
|
||||
fi
|
@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
SDA_GPIO_PIN=10
|
||||
SCL_GPIO_PIN=11
|
||||
I2C_BUS=3
|
||||
|
||||
echo "Setting up depth recording"
|
||||
|
||||
# Enable i2c
|
||||
sudo raspi-config nonint do_i2c 0
|
||||
|
||||
# Enable i2c on bus 3 and GPIO pins sda=23 and scl=24
|
||||
config="dtoverlay=i2c-gpio,bus=$I2C_BUS,i2c_gpio_sda=$SDA_GPIO_PIN,i2c_gpio_scl=$SCL_GPIO_PIN"
|
||||
|
||||
if ! grep -q "$config" /boot/config.txt; then
|
||||
echo "$config" | sudo tee -a /boot/config.txt
|
||||
fi
|
||||
|
||||
# Install packages
|
||||
sudo apt-get update && sudo apt-get install -y i2c-tools python3-pip
|
||||
|
||||
sudo pip install -r $HOME/hydrophonitor/depth-logger/requirements.txt
|
@ -1,101 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
BOOT_DIR_PATH=/boot/hydrophonitor
|
||||
LOG_FILE=$BOOT_DIR_PATH/$(date +"%Y-%m-%dT%H-%M-%S")-setup-log.txt
|
||||
|
||||
# Output from commands within the curly braces is written
|
||||
# to $LOG_FILE
|
||||
{
|
||||
DIR_PATH=$HOME
|
||||
|
||||
echo
|
||||
echo "Starting setup, writing logs to $LOG_FILE"
|
||||
echo
|
||||
|
||||
echo
|
||||
echo "### Update file paths in config and start script files"
|
||||
echo
|
||||
|
||||
# Update hydrophonitor-config.txt HOME_PATH to the current user's home
|
||||
sudo sed -i "s|^HOME_PATH=.*$|HOME_PATH=$HOME|" $BOOT_DIR_PATH/hydrophonitor-config.txt
|
||||
|
||||
# Update script path in start scripts to the current user's home
|
||||
sudo sed -i "s|^SCRIPT_PATH=.*$|SCRIPT_PATH=$HOME/hydrophonitor/scripts|" $BOOT_DIR_PATH/scripts/start-all.sh
|
||||
sudo sed -i "s|^SCRIPT_PATH=.*$|SCRIPT_PATH=$HOME/hydrophonitor/scripts|" $BOOT_DIR_PATH/scripts/start-audio.sh
|
||||
sudo sed -i "s|^SCRIPT_PATH=.*$|SCRIPT_PATH=$HOME/hydrophonitor/scripts|" $BOOT_DIR_PATH/scripts/start-gps.sh
|
||||
sudo sed -i "s|^SCRIPT_PATH=.*$|SCRIPT_PATH=$HOME/hydrophonitor/scripts|" $BOOT_DIR_PATH/scripts/start-pressure-depth.sh
|
||||
|
||||
# Copy the files to DIR_PATH
|
||||
echo
|
||||
echo "### Copy files to $DIR_PATH"
|
||||
echo
|
||||
|
||||
mkdir -p "$DIR_PATH"
|
||||
cd "$DIR_PATH"
|
||||
rm -rf hydrophonitor
|
||||
cp -R $BOOT_DIR_PATH/ .
|
||||
|
||||
# Install some development tools
|
||||
echo
|
||||
echo "### Install some development tools"
|
||||
echo
|
||||
|
||||
sudo apt-get update && sudo apt-get install -y build-essential python3-pip
|
||||
|
||||
# Install the Rust programming language 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"
|
||||
|
||||
# Setup audio
|
||||
echo
|
||||
echo "### Setup audio"
|
||||
echo
|
||||
|
||||
cd "$DIR_PATH" && ./hydrophonitor/scripts/setup-audio.sh
|
||||
|
||||
# Setup GPS
|
||||
echo
|
||||
echo "### Setup GPS"
|
||||
echo
|
||||
|
||||
cd "$DIR_PATH" && ./hydrophonitor/scripts/setup-gps.sh
|
||||
|
||||
# Setup depth sensor
|
||||
echo
|
||||
echo "### Setup depth recording"
|
||||
echo
|
||||
|
||||
cd "$DIR_PATH" && ./hydrophonitor/scripts/setup-pressure-depth.sh
|
||||
|
||||
# Setup shutdown button
|
||||
echo
|
||||
echo "### Setup shutdown button"
|
||||
echo
|
||||
|
||||
cd "$DIR_PATH" && ./hydrophonitor/scripts/setup-shutdown-button.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
|
||||
|
||||
CRON_FILE=/etc/crontab
|
||||
CRON_LOG_FILE="$BOOT_DIR_PATH/cron-log.txt"
|
||||
CRON_COMMAND="@reboot root $DIR_PATH/hydrophonitor/scripts/start-all.sh 2>&1 >> $CRON_LOG_FILE"
|
||||
|
||||
# Append command to cron file only if it's not there yet
|
||||
if ! grep -q "$CRON_COMMAND" "$CRON_FILE"; then
|
||||
echo "$CRON_COMMAND" | sudo tee -a "$CRON_FILE"
|
||||
fi
|
||||
|
||||
# Reboot
|
||||
echo
|
||||
echo "### Setup ready, run 'sudo reboot' to apply all changes"
|
||||
echo
|
||||
} 2>&1 | sudo tee $LOG_FILE
|
@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
echo "Setting up the real time clock module, part 1"
|
||||
|
||||
# Enable i2c
|
||||
sudo raspi-config nonint do_i2c 0
|
||||
|
||||
# Enable i2c rtc on the default i2c pins
|
||||
config="dtoverlay=i2c-rtc,ds3231"
|
||||
|
||||
if ! grep -q "$config" /boot/config.txt; then
|
||||
echo "$config" | sudo tee -a /boot/config.txt
|
||||
fi
|
||||
|
||||
# Disable fake-hwclock
|
||||
sudo apt-get remove -y fake-hwclock
|
||||
sudo update-rc.d -f fake-hwclock remove
|
||||
sudo systemctl disable fake-hwclock
|
||||
|
||||
# Load needed modules at boot
|
||||
config="i2c-bcm2708
|
||||
i2c-dev
|
||||
rtc-ds1307"
|
||||
|
||||
if ! grep -q "$config" /etc/modules; then
|
||||
echo "$config" | sudo tee -a /etc/modules
|
||||
fi
|
||||
|
||||
# Remove some lines from /lib/udev/hwclock-set
|
||||
sudo sed -i '/^if \[ \-e \/run\/systemd\/system \] ; then$/,/^fi$/d' /lib/udev/hwclock-set
|
||||
|
||||
echo "Setup RTC part 1 done, reboot and run setup-rtc-2.sh"
|
@ -1,38 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
echo "Setting up the real time clock module, part 2"
|
||||
|
||||
I2C_BUS=1
|
||||
|
||||
echo ds1307 0x68 | sudo tee /sys/class/i2c-adapter/i2c-$I2C_BUS/new_device
|
||||
|
||||
# Load RTC clock at boot
|
||||
config="echo ds1307 0x68 | sudo tee /sys/class/i2c-adapter/i2c-$I2C_BUS/new_device
|
||||
sudo hwclock -s
|
||||
exit 0"
|
||||
|
||||
if ! grep -q "$config" /etc/rc.local; then
|
||||
sudo sed -i "s/^exit 0$//" /etc/rc.local
|
||||
echo "$config" | sudo tee -a /etc/rc.local
|
||||
fi
|
||||
|
||||
# Set system time to Internet time
|
||||
echo "Restarting systmd-timesyncd to update system time"
|
||||
sudo systemctl restart systemd-timesyncd
|
||||
|
||||
echo "System time now:"
|
||||
date
|
||||
|
||||
sleep 5
|
||||
|
||||
# Write system time to the RTC module
|
||||
echo "Hardware clock time now:"
|
||||
sudo hwclock -r --verbose
|
||||
|
||||
echo "Writing system time to hardware clock"
|
||||
sudo hwclock -w --verbose
|
||||
|
||||
echo "Hardware clock time now:"
|
||||
sudo hwclock -r --verbose
|
@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
echo "Setting up shutdown button"
|
||||
|
||||
config="dtoverlay=gpio-shutdown,gpio_pin=21,gpio_pull=up,active_low=1"
|
||||
|
||||
if ! grep -q "$config" /boot/config.txt; then
|
||||
echo "$config" | sudo tee -a /boot/config.txt
|
||||
fi
|
@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Print all commands to standard output
|
||||
set -x
|
||||
|
||||
BOOT_DIR_PATH=/boot/hydrophonitor
|
||||
LOG_FILE=$BOOT_DIR_PATH/$(date +"%Y-%m-%dT%H-%M-%S")-startup-log.txt
|
||||
|
||||
{
|
||||
SCRIPT_PATH=/home/pi/hydrophonitor/scripts
|
||||
|
||||
# Export the configuration values
|
||||
. $SCRIPT_PATH/export-config-values.sh
|
||||
|
||||
# Create output directory
|
||||
OUTPUT_DIR=$OUTPUT_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) 2>&1 | tee "$OUTPUT_DIR"/log.txt
|
||||
} 2>&1 | tee $LOG_FILE
|
@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
# Export the configuration values
|
||||
SCRIPT_PATH=/home/pi/hydrophonitor/scripts
|
||||
. $SCRIPT_PATH/export-config-values.sh
|
||||
|
||||
AUDIO_TARGET_EXECUTABLE="audio"
|
||||
|
||||
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 $HOME_PATH/hydrophonitor/audio-logger/target/release && ./$AUDIO_TARGET_EXECUTABLE $OPTIONS
|
@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
# Export the configuration values
|
||||
SCRIPT_PATH=/home/pi/hydrophonitor/scripts
|
||||
. $SCRIPT_PATH/export-config-values.sh
|
||||
|
||||
OPTIONS="--output $OUTPUT_DIR --interval $GPS_INTERVAL"
|
||||
|
||||
cd $HOME_PATH/hydrophonitor/gps-logger && python record-gps.py $OPTIONS
|
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
I2C_BUS=3
|
||||
|
||||
# Export the configuration values
|
||||
SCRIPT_PATH=/home/pi/hydrophonitor/scripts
|
||||
. $SCRIPT_PATH/export-config-values.sh
|
||||
|
||||
OPTIONS="--output $OUTPUT_DIR --interval $DEPTH_INTERVAL --bus $I2C_BUS"
|
||||
|
||||
cd $HOME_PATH/hydrophonitor/depth-logger && python record-depth.py $OPTIONS
|
74
targets/raspberry-pi-3/default.nix
Normal file
74
targets/raspberry-pi-3/default.nix
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
system = "aarch64-linux";
|
||||
in {
|
||||
imports = [
|
||||
./i2c.nix
|
||||
];
|
||||
system.stateVersion = "23.05";
|
||||
sdImage.compressImage = false;
|
||||
|
||||
boot = {
|
||||
# disable internal sound card and vc4 gpu
|
||||
blacklistedKernelModules = ["snd_bcm2835" "vc4"];
|
||||
# enable i2c and rtc modules
|
||||
kernelModules = ["i2c-dev" "i2c_bcm2708" "rtc_ds1307"];
|
||||
kernelPackages = pkgs.linuxKernel.packages.linux_rpi3;
|
||||
initrd.availableKernelModules = [
|
||||
"usbhid"
|
||||
"usb_storage"
|
||||
"pcie_brcmstb" # required for the pcie bus to work
|
||||
"reset-raspberrypi" # required for vl805 firmware to load
|
||||
];
|
||||
loader = {
|
||||
grub.enable = false;
|
||||
generic-extlinux-compatible.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
hardware = {
|
||||
raspberry-pi."3".i2c1.enable = true;
|
||||
deviceTree.filter = "bcm2711-rpi-*.dtb";
|
||||
# Required for the wireless firmware
|
||||
enableRedistributableFirmware = true;
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [i2c-tools libgpiod];
|
||||
|
||||
users.users.kaskelotti = {
|
||||
isNormalUser = true;
|
||||
initialHashedPassword = "$6$ySDQdXbGH/qDvjpe$Jp5icbEFRSBLsxB2XGxFz.dACxOS/.KYHENxVSUzFED0UYi9R64858JevedVB06sTsFvlKOPSlzBvbACbxNZr1";
|
||||
extraGroups = ["wheel" "networkmanager"];
|
||||
};
|
||||
nix.settings.trusted-users = ["kaskelotti"];
|
||||
|
||||
sound.enable = true;
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings.PasswordAuthentication = true;
|
||||
};
|
||||
|
||||
services.audio-recorder = {
|
||||
enable = true;
|
||||
output-folder = "/output/audio";
|
||||
sample-rate = 192000;
|
||||
sample-format = "S32_LE";
|
||||
channels = 4;
|
||||
max-file-time-secs = 300;
|
||||
};
|
||||
|
||||
services.gps-recorder = {
|
||||
enable = true;
|
||||
output-folder = "/output/gps";
|
||||
interval-secs = 10;
|
||||
};
|
||||
|
||||
services.i2c-rtc-start = {
|
||||
enable = true;
|
||||
i2c-bus = 1;
|
||||
};
|
||||
}
|
84
targets/raspberry-pi-3/i2c.nix
Normal file
84
targets/raspberry-pi-3/i2c.nix
Normal file
@ -0,0 +1,84 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
cfg = config.hardware.raspberry-pi."3";
|
||||
optionalProperty = name: value: lib.optionalString (value != null) "${name} = <${builtins.toString value}>;";
|
||||
simple-overlay = {
|
||||
target,
|
||||
status,
|
||||
frequency,
|
||||
}: {
|
||||
name = "${target}-${status}-overlay";
|
||||
dtsText = ''
|
||||
/dts-v1/;
|
||||
/plugin/;
|
||||
/ {
|
||||
compatible = "brcm,bcm2711";
|
||||
fragment@0 {
|
||||
target = <&${target}>;
|
||||
__overlay__ {
|
||||
status = "${status}";
|
||||
${optionalProperty "clock-frequency" frequency}
|
||||
};
|
||||
};
|
||||
};
|
||||
'';
|
||||
};
|
||||
in {
|
||||
options.hardware.raspberry-pi."3" = {
|
||||
i2c0 = {
|
||||
enable = lib.mkEnableOption ''
|
||||
Turn on the VideoCore I2C bus (maps to /dev/i2c-22) and enable access from the i2c group.
|
||||
After a reboot, i2c-tools (e.g. i2cdetect -F 22) should work for root or any user in i2c.
|
||||
'';
|
||||
frequency = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
description = ''
|
||||
interface clock-frequency
|
||||
'';
|
||||
};
|
||||
};
|
||||
i2c1 = {
|
||||
enable = lib.mkEnableOption ''
|
||||
Turn on the ARM I2C bus (/dev/i2c-1 on GPIO pins 3 and 5) and enable access from the i2c group.
|
||||
After a reboot, i2c-tools (e.g. i2cdetect -F 1) should work for root or any user in i2c.
|
||||
'';
|
||||
frequency = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
description = ''
|
||||
interface clock-frequency
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
config.hardware = lib.mkMerge [
|
||||
(lib.mkIf cfg.i2c0.enable {
|
||||
i2c.enable = lib.mkDefault true;
|
||||
deviceTree = {
|
||||
overlays = [
|
||||
(simple-overlay {
|
||||
target = "i2c0if";
|
||||
status = "okay";
|
||||
inherit (cfg.i2c0) frequency;
|
||||
})
|
||||
];
|
||||
};
|
||||
})
|
||||
(lib.mkIf cfg.i2c1.enable {
|
||||
i2c.enable = lib.mkDefault true;
|
||||
deviceTree = {
|
||||
overlays = [
|
||||
(simple-overlay {
|
||||
target = "i2c1";
|
||||
status = "okay";
|
||||
inherit (cfg.i2c1) frequency;
|
||||
})
|
||||
];
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
63
targets/raspberry-pi-4/default.nix
Normal file
63
targets/raspberry-pi-4/default.nix
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
system = "aarch64-linux";
|
||||
in {
|
||||
system.stateVersion = "23.05";
|
||||
sdImage.compressImage = false;
|
||||
|
||||
# disable internal sound card and vc4 gpu
|
||||
boot.blacklistedKernelModules = ["snd_bcm2835" "vc4"];
|
||||
# enable i2c and rtc modules
|
||||
boot.kernelModules = ["i2c-dev" "i2c_bcm2708" "rtc_ds1307"];
|
||||
|
||||
hardware = {
|
||||
raspberry-pi."4".i2c1.enable = true;
|
||||
raspberry-pi."4".apply-overlays-dtmerge.enable = true;
|
||||
deviceTree.filter = "bcm2711-rpi-4*.dtb";
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [i2c-tools];
|
||||
|
||||
users.users.kaskelotti = {
|
||||
isNormalUser = true;
|
||||
initialHashedPassword = "$6$ySDQdXbGH/qDvjpe$Jp5icbEFRSBLsxB2XGxFz.dACxOS/.KYHENxVSUzFED0UYi9R64858JevedVB06sTsFvlKOPSlzBvbACbxNZr1";
|
||||
extraGroups = ["wheel" "networkmanager"];
|
||||
};
|
||||
nix.settings.trusted-users = ["kaskelotti"];
|
||||
|
||||
sound.enable = true;
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings.PasswordAuthentication = true;
|
||||
};
|
||||
|
||||
services.audio-recorder = {
|
||||
enable = true;
|
||||
output-folder = "/output/audio";
|
||||
sample-rate = 192000;
|
||||
sample-format = "S32_LE";
|
||||
channels = 4;
|
||||
max-file-time-secs = 300;
|
||||
};
|
||||
|
||||
services.gps-recorder = {
|
||||
enable = true;
|
||||
output-folder = "/output/gps";
|
||||
interval-secs = 10;
|
||||
};
|
||||
|
||||
services.i2c-rtc-start = {
|
||||
enable = true;
|
||||
i2c-bus = 1;
|
||||
};
|
||||
|
||||
services.shutdown-button = {
|
||||
enable = true;
|
||||
gpio-pin = 21; # option not implemented yet
|
||||
shutdown-press-secs = 1; # option not implemented yet
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user