#![allow(missing_debug_implementations)]
use std::{
fs::File,
io::{BufReader, Read},
path::{Path, PathBuf},
result::Result,
u8,
};
#[cfg(feature = "raspicam")]
use std::{ffi::OsStr, i8, u16};
#[cfg(any(feature = "gps", feature = "fona"))]
use std::fmt;
use colored::Colorize;
use failure::{Error, ResultExt};
use lazy_static::lazy_static;
use serde::Deserialize;
use toml;
#[cfg(any(feature = "gps", feature = "fona"))]
use serde::de::{self, Deserializer, Visitor};
#[cfg(any(feature = "gps", feature = "fona"))]
use sysfs_gpio::Pin;
use crate::{error, generate_error_string, CONFIG_FILE};
lazy_static! {
pub static ref CONFIG: Config = match Config::from_file(CONFIG_FILE) {
Err(e) => {
panic!("{}", generate_error_string(&e, "error loading configuration").red());
},
Ok(c) => c,
};
}
#[derive(Debug, Deserialize)]
pub struct Config {
debug: Option<bool>,
data_dir: PathBuf,
flight: Flight,
#[cfg(feature = "fona")]
battery: Battery,
#[cfg(feature = "raspicam")]
video: Video,
#[cfg(feature = "raspicam")]
picture: Picture,
#[cfg(feature = "gps")]
gps: Gps,
#[cfg(feature = "fona")]
fona: Fona,
#[cfg(feature = "telemetry")]
telemetry: Telemetry,
}
impl Config {
fn from_file<P: AsRef<Path>>(path: P) -> Result<Config, Error> {
let file = File::open(path.as_ref()).context(error::Config::Open {
path: path.as_ref().to_owned(),
})?;
let mut reader = BufReader::new(file);
let mut contents = String::new();
let _ = reader
.read_to_string(&mut contents)
.context(error::Config::Read {
path: path.as_ref().to_owned(),
})?;
let config: Config = toml::from_str(&contents).context(error::Config::InvalidToml {
path: path.as_ref().to_owned(),
})?;
if let (false, errors) = config.verify() {
Err(error::Config::Invalid { errors }.into())
} else {
Ok(config)
}
}
#[allow(clippy::replace_consts)]
fn verify(&self) -> (bool, String) {
#[cfg(feature = "raspicam")]
let mut errors = String::new();
#[cfg(feature = "raspicam")]
let mut ok = true;
#[cfg(feature = "raspicam")]
{
if self.picture.width > 3280 {
ok = false;
errors.push_str(&format!(
"picture width must be below or equal to 3280px, found {}px\n",
self.picture.width
));
}
if self.picture.height > 2464 {
ok = false;
errors.push_str(&format!(
"picture height must be below or equal to 2464px, found {}px\n",
self.picture.height
));
}
if let Some(r @ 360...u16::MAX) = self.picture.rotation {
ok = false;
errors.push_str(&format!(
"camera rotation must be between 0 and 359 degrees, found {} degrees\n",
r
));
}
if self.picture.quality > 100 {
ok = false;
errors.push_str(&format!(
"picture quality must be a number between 0 and 100, found {}px\n",
self.picture.quality
));
}
if let Some(b @ 101...u8::MAX) = self.picture.brightness {
ok = false;
errors.push_str(&format!(
"picture brightness must be between 0 and 100, found {}\n",
b
));
}
match self.picture.contrast {
Some(c @ i8::MIN...-101) | Some(c @ 101...i8::MAX) => {
ok = false;
errors.push_str(&format!(
"picture contrast must be between -100 and 100, found {}\n",
c
));
}
_ => {}
}
match self.picture.sharpness {
Some(s @ i8::MIN...-101) | Some(s @ 101...i8::MAX) => {
ok = false;
errors.push_str(&format!(
"picture sharpness must be between -100 and 100, found {}\n",
s
));
}
_ => {}
}
match self.picture.saturation {
Some(s @ i8::MIN...-101) | Some(s @ 101...i8::MAX) => {
ok = false;
errors.push_str(&format!(
"picture saturation must be between -100 and 100, found {}\n",
s
));
}
_ => {}
}
match self.picture.iso {
Some(i @ 0...99) | Some(i @ 801...u16::MAX) => {
ok = false;
errors.push_str(&format!(
"picture ISO must be between 100 and 800, found {}\n",
i
));
}
_ => {}
}
match self.picture.ev {
Some(e @ i8::MIN...-11) | Some(e @ 11...i8::MAX) => {
ok = false;
errors.push_str(&format!(
"picture EV compensation must be between -10 and 10, found {}\n",
e
));
}
_ => {}
}
if self.video.width > 2592 {
ok = false;
errors.push_str(&format!(
"video width must be below or equal to 2592px, found {}px\n",
self.video.width
));
}
if self.video.height > 1944 {
ok = false;
errors.push_str(&format!(
"video height must be below or equal to 1944px, found {}px\n",
self.video.height
));
}
if let Some(r @ 360...u16::MAX) = self.video.rotation {
ok = false;
errors.push_str(&format!(
"camera rotation must be between 0 and 359 degrees, found {} degrees\n",
r
));
}
if self.video.fps > 90 {
ok = false;
errors.push_str(&format!(
"video framerate must be below or equal to 90fps, found {}fps\n",
self.video.fps
));
}
if let Some(b @ 101...u8::MAX) = self.video.brightness {
ok = false;
errors.push_str(&format!(
"video brightness must be between 0 and 100, found {}\n",
b
));
}
match self.video.contrast {
Some(c @ i8::MIN...-101) | Some(c @ 101...i8::MAX) => {
ok = false;
errors.push_str(&format!(
"video contrast must be between -100 and 100, found {}\n",
c
));
}
_ => {}
}
match self.video.sharpness {
Some(s @ i8::MIN...-101) | Some(s @ 101...i8::MAX) => {
ok = false;
errors.push_str(&format!(
"video sharpness must be between -100 and 100, found \
{}\n",
s
));
}
_ => {}
}
match self.video.saturation {
Some(s @ i8::MIN...-101) | Some(s @ 101...i8::MAX) => {
ok = false;
errors.push_str(&format!(
"video saturation must be between -100 and 100, found {}\n",
s
));
}
_ => {}
}
match self.video.iso {
Some(i @ 0...99) | Some(i @ 801...u16::MAX) => {
ok = false;
errors.push_str(&format!(
"video ISO must be between 100 and 800, found {}\n",
i
));
}
_ => {}
}
match self.video.ev {
Some(e @ i8::MIN...-11) | Some(e @ 11...i8::MAX) => {
ok = false;
errors.push_str(&format!(
"video EV compensation must be between -10 and 10, found {}\n",
e
));
}
_ => {}
}
match (self.video.width, self.video.height, self.video.fps) {
(2592, 1944, 1...15)
| (1920, 1080, 1...30)
| (1296, 972, 1...42)
| (1296, 730, 1...49)
| (640, 480, 1...90) => {}
(w, h, f) => {
ok = false;
errors.push_str(&format!(
"video mode must be one of 2592\u{d7}1944 1-15fps, 1920\u{d7}1080 \
1-30fps, 1296\u{d7}972 1-42fps, 1296\u{d7}730 1-49fps, 640\u{d7}480 \
1-60fps, found {}x{} {}fps\n",
w, h, f
));
}
}
}
#[cfg(feature = "raspicam")]
{
(ok, errors)
}
#[cfg(not(feature = "raspicam"))]
{
(true, String::new())
}
}
pub fn debug(&self) -> bool {
self.debug == Some(true)
}
pub fn flight(&self) -> Flight {
self.flight
}
#[cfg(feature = "fona")]
pub fn battery(&self) -> Battery {
self.battery
}
#[cfg(feature = "raspicam")]
pub fn video(&self) -> &Video {
&self.video
}
#[cfg(feature = "raspicam")]
pub fn picture(&self) -> &Picture {
&self.picture
}
#[cfg(feature = "gps")]
pub fn gps(&self) -> &Gps {
&self.gps
}
#[cfg(feature = "fona")]
pub fn fona(&self) -> &Fona {
&self.fona
}
#[cfg(feature = "telemetry")]
pub fn telemetry(&self) -> &Telemetry {
&self.telemetry
}
pub fn data_dir(&self) -> &Path {
self.data_dir.as_path()
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct Flight {
length: u32,
expected_max_height: u32,
}
impl Flight {
pub fn length(self) -> u32 {
self.length
}
pub fn expected_max_height(self) -> u32 {
self.expected_max_height
}
}
#[cfg(feature = "fona")]
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct Battery {
main_min: f32,
main_max: f32,
fona_min: f32,
fona_max: f32,
main_min_percent: f32,
fona_min_percent: f32,
}
#[cfg(feature = "fona")]
impl Battery {
pub fn main_min(self) -> f32 {
self.main_min
}
pub fn main_max(self) -> f32 {
self.main_max
}
pub fn fona_min(self) -> f32 {
self.fona_min
}
pub fn fona_max(self) -> f32 {
self.fona_min
}
pub fn main_min_percent(self) -> f32 {
self.main_min_percent
}
pub fn fona_min_percent(self) -> f32 {
self.fona_min_percent
}
}
#[cfg(feature = "raspicam")]
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct Video {
height: u16,
width: u16,
rotation: Option<u16>,
fps: u8,
bitrate: u32,
exposure: Option<Exposure>,
brightness: Option<u8>,
contrast: Option<i8>,
sharpness: Option<i8>,
saturation: Option<i8>,
iso: Option<u16>,
stabilization: Option<bool>,
ev: Option<i8>,
white_balance: Option<WhiteBalance>,
}
#[cfg(feature = "raspicam")]
impl Video {
pub fn height(self) -> u16 {
self.height
}
pub fn width(self) -> u16 {
self.width
}
pub fn rotation(self) -> Option<u16> {
self.rotation
}
pub fn fps(self) -> u8 {
self.fps
}
pub fn bitrate(self) -> u32 {
self.bitrate
}
pub fn exposure(self) -> Option<Exposure> {
self.exposure
}
pub fn brightness(self) -> Option<u8> {
self.brightness
}
pub fn contrast(self) -> Option<i8> {
self.contrast
}
pub fn sharpness(self) -> Option<i8> {
self.sharpness
}
pub fn saturation(self) -> Option<i8> {
self.saturation
}
pub fn iso(self) -> Option<u16> {
self.iso
}
pub fn stabilization(self) -> bool {
self.stabilization == Some(true)
}
pub fn ev(self) -> Option<i8> {
self.ev
}
pub fn white_balance(self) -> Option<WhiteBalance> {
self.white_balance
}
}
#[cfg(feature = "raspicam")]
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct Picture {
height: u16,
width: u16,
rotation: Option<u16>,
quality: u8,
#[cfg(feature = "gps")]
exif: Option<bool>,
raw: Option<bool>,
exposure: Option<Exposure>,
brightness: Option<u8>,
contrast: Option<i8>,
sharpness: Option<i8>,
saturation: Option<i8>,
iso: Option<u16>,
ev: Option<i8>,
white_balance: Option<WhiteBalance>,
interval: u32,
repeat: Option<u32>,
first_timeout: u32,
}
#[cfg(feature = "raspicam")]
impl Picture {
pub fn height(self) -> u16 {
self.height
}
pub fn width(self) -> u16 {
self.width
}
pub fn rotation(self) -> Option<u16> {
self.rotation
}
pub fn quality(self) -> u8 {
self.quality
}
#[cfg(feature = "gps")]
pub fn exif(self) -> bool {
self.exif == Some(true)
}
pub fn raw(self) -> bool {
self.raw == Some(true)
}
pub fn exposure(self) -> Option<Exposure> {
self.exposure
}
pub fn brightness(self) -> Option<u8> {
self.brightness
}
pub fn contrast(self) -> Option<i8> {
self.contrast
}
pub fn sharpness(self) -> Option<i8> {
self.sharpness
}
pub fn saturation(self) -> Option<i8> {
self.saturation
}
pub fn iso(self) -> Option<u16> {
self.iso
}
pub fn ev(self) -> Option<i8> {
self.ev
}
pub fn white_balance(self) -> Option<WhiteBalance> {
self.white_balance
}
pub fn interval(self) -> u32 {
self.interval
}
pub fn repeat(self) -> Option<u32> {
self.repeat
}
pub fn first_timeout(self) -> u32 {
self.first_timeout
}
}
#[cfg(feature = "raspicam")]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
pub enum Exposure {
Off,
Auto,
Night,
NightPreview,
BackLight,
SpotLight,
Sports,
Snow,
Beach,
VeryLong,
FixedFps,
AntiShake,
Fireworks,
}
#[cfg(feature = "raspicam")]
impl AsRef<OsStr> for Exposure {
fn as_ref(&self) -> &OsStr {
OsStr::new(match *self {
Exposure::Off => "off",
Exposure::Auto => "auto",
Exposure::Night => "night",
Exposure::NightPreview => "nightpreview",
Exposure::BackLight => "backlight",
Exposure::SpotLight => "spotlight",
Exposure::Sports => "sports",
Exposure::Snow => "snow",
Exposure::Beach => "beach",
Exposure::VeryLong => "verylong",
Exposure::FixedFps => "fixedfps",
Exposure::AntiShake => "antishake",
Exposure::Fireworks => "fireworks",
})
}
}
#[cfg(feature = "raspicam")]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
pub enum WhiteBalance {
Off,
Auto,
Sun,
CloudShade,
Tungsten,
Fluorescent,
Incandescent,
Flash,
Horizon,
}
#[cfg(feature = "raspicam")]
impl AsRef<OsStr> for WhiteBalance {
fn as_ref(&self) -> &OsStr {
OsStr::new(match *self {
WhiteBalance::Off => "off",
WhiteBalance::Auto => "auto",
WhiteBalance::Sun => "sun",
WhiteBalance::CloudShade => "cloudshade",
WhiteBalance::Tungsten => "tungsten",
WhiteBalance::Fluorescent => "fluorescent",
WhiteBalance::Incandescent => "incandescent",
WhiteBalance::Flash => "flash",
WhiteBalance::Horizon => "horizon",
})
}
}
#[cfg(feature = "gps")]
#[derive(Debug, Deserialize)]
pub struct Gps {
uart: PathBuf,
baud_rate: u32,
#[serde(deserialize_with = "deserialize_pin")]
power_gpio: Pin,
}
#[cfg(feature = "gps")]
impl Gps {
pub fn uart(&self) -> &Path {
&self.uart
}
pub fn baud_rate(&self) -> u32 {
self.baud_rate
}
pub fn power_gpio(&self) -> Pin {
self.power_gpio
}
}
#[cfg(feature = "fona")]
#[derive(Debug, Deserialize)]
pub struct Fona {
uart: PathBuf,
baud_rate: u32,
#[serde(deserialize_with = "deserialize_pin")]
power_gpio: Pin,
#[serde(deserialize_with = "deserialize_pin")]
status_gpio: Pin,
sms_phone: PhoneNumber,
location_service: String,
}
#[cfg(feature = "fona")]
impl Fona {
pub fn uart(&self) -> &Path {
&self.uart
}
pub fn baud_rate(&self) -> u32 {
self.baud_rate
}
pub fn power_gpio(&self) -> Pin {
self.power_gpio
}
pub fn status_gpio(&self) -> Pin {
self.status_gpio
}
pub fn sms_phone(&self) -> &PhoneNumber {
&self.sms_phone
}
pub fn location_service(&self) -> &str {
&self.location_service
}
}
#[cfg(feature = "fona")]
#[derive(Debug)]
pub struct PhoneNumber(String);
#[cfg(feature = "fona")]
impl PhoneNumber {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(feature = "fona")]
impl<'de> Deserialize<'de> for PhoneNumber {
fn deserialize<D>(deserializer: D) -> Result<PhoneNumber, D::Error>
where
D: Deserializer<'de>,
{
struct PhoneNumberVisitor;
impl<'dev> Visitor<'dev> for PhoneNumberVisitor {
type Value = PhoneNumber;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid phone number")
}
fn visit_str<E>(self, value: &str) -> Result<PhoneNumber, E>
where
E: de::Error,
{
Ok(PhoneNumber(value.to_owned()))
}
}
deserializer.deserialize_str(PhoneNumberVisitor)
}
}
#[cfg(feature = "telemetry")]
#[derive(Debug, Deserialize)]
pub struct Telemetry {
uart: PathBuf,
baud_rate: u32,
}
#[cfg(feature = "telemetry")]
impl Telemetry {
pub fn uart(&self) -> &Path {
&self.uart
}
pub fn baud_rate(&self) -> u32 {
self.baud_rate
}
}
#[cfg(any(feature = "gps", feature = "fona"))]
fn deserialize_pin<'de, D>(deserializer: D) -> Result<Pin, D::Error>
where
D: Deserializer<'de>,
{
struct PinVisitor;
impl<'dev> Visitor<'dev> for PinVisitor {
type Value = Pin;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer between 2 and 28")
}
#[allow(clippy::absurd_extreme_comparisons)]
fn visit_i64<E>(self, value: i64) -> Result<Pin, E>
where
E: de::Error,
{
if value >= 2 && value <= 28 {
#[allow(clippy::cast_sign_loss)]
{
Ok(Pin::new(value as u64))
}
} else {
Err(E::custom(format!("pin out of range: {}", value)))
}
}
}
deserializer.deserialize_u8(PinVisitor)
}
#[cfg(test)]
mod tests {
#[cfg(all(feature = "gps", feature = "raspicam"))]
use super::Gps;
#[cfg(all(feature = "raspicam", feature = "telemetry"))]
use super::Telemetry;
#[cfg(all(feature = "raspicam", feature = "fona"))]
use super::{Battery, Fona, PhoneNumber};
use super::{Config, CONFIG};
#[cfg(feature = "raspicam")]
use super::{Exposure, Flight, Picture, Video, WhiteBalance};
#[cfg(all(feature = "raspicam", any(feature = "gps", feature = "fona")))]
use sysfs_gpio::Pin;
#[cfg(feature = "gps")]
use std::path::Path;
#[cfg(feature = "raspicam")]
use std::path::PathBuf;
#[test]
fn load_config() {
let config = Config::from_file("config.toml").unwrap();
assert_eq!(config.debug(), true);
#[cfg(feature = "raspicam")]
{
assert_eq!(config.picture().height(), 2464);
assert_eq!(config.picture().width(), 3280);
#[cfg(feature = "gps")]
{
assert_eq!(config.picture().exif(), true);
}
assert_eq!(config.video().height(), 1080);
assert_eq!(config.video().width(), 1920);
assert_eq!(config.video().fps(), 30);
}
#[cfg(feature = "gps")]
{
assert_eq!(config.gps().uart(), Path::new("/dev/ttyAMA0"));
assert_eq!(config.gps().baud_rate(), 9_600);
assert_eq!(config.gps().power_gpio().get_pin(), 3)
}
}
#[test]
#[cfg(feature = "raspicam")]
fn config_error() {
let flight = Flight {
length: 300,
expected_max_height: 35000,
};
#[cfg(feature = "gps")]
let picture = Picture {
height: 10_345,
width: 5_246,
rotation: Some(180),
quality: 95,
raw: Some(true),
exif: Some(true),
exposure: Some(Exposure::AntiShake),
brightness: Some(50),
contrast: Some(50),
sharpness: None,
saturation: None,
iso: None,
ev: None,
white_balance: Some(WhiteBalance::Horizon),
first_timeout: 120,
interval: 300,
repeat: Some(30),
};
#[cfg(not(feature = "gps"))]
let picture = Picture {
height: 10_345,
width: 5_246,
rotation: Some(180),
quality: 95,
raw: Some(true),
exposure: Some(Exposure::AntiShake),
brightness: Some(50),
contrast: Some(50),
sharpness: None,
saturation: None,
iso: None,
ev: None,
white_balance: Some(WhiteBalance::Horizon),
first_timeout: 120,
interval: 300,
repeat: Some(30),
};
let video = Video {
height: 12_546,
width: 5_648,
rotation: Some(180),
fps: 92,
bitrate: 20_000_000,
exposure: Some(Exposure::AntiShake),
brightness: Some(50),
contrast: Some(50),
sharpness: None,
saturation: None,
iso: None,
stabilization: Some(true),
ev: None,
white_balance: Some(WhiteBalance::Horizon),
};
#[cfg(feature = "fona")]
let fona = Fona {
uart: PathBuf::from("/dev/ttyUSB0"),
baud_rate: 9_600,
power_gpio: Pin::new(7),
status_gpio: Pin::new(21),
sms_phone: PhoneNumber(String::new()),
location_service: "gprs-service.com".to_owned(),
};
#[cfg(feature = "fona")]
let battery = Battery {
main_min: 1.952_777_7,
main_max: 2.216_666_7,
fona_min: 3.7,
fona_max: 4.2,
main_min_percent: 0.8,
fona_min_percent: 0.75,
};
#[cfg(feature = "telemetry")]
let telemetry = Telemetry {
uart: PathBuf::from("/dev/ttyUSB0"),
baud_rate: 230_400,
};
#[cfg(feature = "gps")]
let gps = Gps {
uart: PathBuf::from("/dev/ttyAMA0"),
baud_rate: 9_600,
power_gpio: Pin::new(3),
};
#[cfg(all(feature = "gps", feature = "fona", feature = "telemetry"))]
let config = Config {
debug: None,
flight,
battery,
data_dir: PathBuf::from("data"),
picture,
video,
gps,
fona,
telemetry,
};
#[cfg(all(feature = "gps", feature = "fona", not(feature = "telemetry")))]
let config = Config {
debug: None,
flight,
battery,
data_dir: PathBuf::from("data"),
picture,
video,
gps,
fona,
};
#[cfg(all(feature = "gps", not(feature = "fona"), feature = "telemetry"))]
let config = Config {
debug: None,
flight,
data_dir: PathBuf::from("data"),
picture,
video,
gps,
telemetry,
};
#[cfg(all(feature = "gps", not(feature = "fona"), not(feature = "telemetry")))]
let config = Config {
debug: None,
flight,
data_dir: PathBuf::from("data"),
picture,
video,
gps,
};
#[cfg(all(not(feature = "gps"), feature = "fona", feature = "telemetry"))]
let config = Config {
debug: None,
flight,
battery,
data_dir: PathBuf::from("data"),
picture,
video,
fona,
telemetry,
};
#[cfg(all(not(feature = "gps"), feature = "fona", not(feature = "telemetry")))]
let config = Config {
debug: None,
flight,
battery,
data_dir: PathBuf::from("data"),
picture,
video,
fona,
};
#[cfg(all(not(feature = "gps"), not(feature = "fona"), feature = "telemetry"))]
let config = Config {
debug: None,
flight,
data_dir: PathBuf::from("data"),
picture,
video,
telemetry,
};
#[cfg(all(
not(feature = "gps"),
not(feature = "fona"),
not(feature = "telemetry")
))]
let config = Config {
debug: None,
flight,
data_dir: PathBuf::from("data"),
picture,
video,
};
let (verify, errors) = config.verify();
assert_eq!(verify, false);
assert_eq!(
errors,
"picture width must be below or equal to 3280px, found 5246px\npicture height \
must be below or equal to 2464px, found 10345px\nvideo width must be below or \
equal to 2592px, found 5648px\nvideo height must be below or equal to 1944px, \
found 12546px\nvideo framerate must be below or equal to 90fps, found 92fps\n\
video mode must be one of 2592\u{d7}1944 1-15fps, 1920\u{d7}1080 1-30fps, \
1296\u{d7}972 1-42fps, 1296\u{d7}730 1-49fps, 640\u{d7}480 1-60fps, found 5648x12546 \
92fps\n"
);
}
#[test]
fn config_static() {
assert!(CONFIG.debug());
}
}