Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically check/set the time on cameras, and bump timeout #54

Merged
merged 4 commits into from
Aug 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
270 changes: 228 additions & 42 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ regex = "1"
serde = { version = "1.0", features = ["derive"] }
socket2 = "0.3"
structopt = "0.3"
time = "0.2"
toml = "0.5"
yaserde = "0.3.16"
yaserde_derive = "0.3.16"
Expand Down
12 changes: 1 addition & 11 deletions docs/Setting Up Neolink For Use With Blue Iris.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,7 @@ _This is the most reliable setup since Neolink cannot autodetect when a camera's

_You will have to reconnect to the camera once you have changed the IP address_

### 3. Set the camera's time to your local network time and disable Auto Reboot
_When a camera reboots, it loses its date and time settings. If the camera's time is not set, Neolink will recursively "time out" every one second and will not stream video. Setting the time and disabling auto reboot on the camera is a workaround for [issue #14](https://github.com/thirtythreeforty/neolink/issues/14)._
1. In the Reolink PC app, login to your camera.
2. Click "Device Settings" -> "System General" -> "Synchronize Local Time."
3. Click "Ok."

4. Click "Device Settings" -> "Maintenance."

5. Uncheck "Enable Auto Reboot."

### 4. Set a Password
### 3. Set a Password
_It's recommended that you set a password for each of your cameras. If you want to use the Reolink Mobile App, it makes you set a password for each camera anyway._
1. In the Reolink PC app, login to your camera.
2. Click "Device Settings" -> "Manage User."
Expand Down
2 changes: 2 additions & 0 deletions src/bc/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub(super) const MAGIC_HEADER: u32 = 0xabcdef0;
pub const MSG_ID_LOGIN: u32 = 1;
pub const MSG_ID_VIDEO: u32 = 3;
pub const MSG_ID_PING: u32 = 93;
pub const MSG_ID_GET_GENERAL: u32 = 104;
pub const MSG_ID_SET_GENERAL: u32 = 105;

pub const EMPTY_LEGACY_PASSWORD: &str =
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
Expand Down
35 changes: 35 additions & 0 deletions src/bc/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub struct BcXml {
pub device_info: Option<DeviceInfo>,
#[yaserde(rename = "Preview")]
pub preview: Option<Preview>,
#[yaserde(rename = "SystemGeneral")]
pub system_general: Option<SystemGeneral>,
#[yaserde(rename = "Norm")]
pub norm: Option<Norm>,
}

impl AllTopXmls {
Expand Down Expand Up @@ -125,6 +129,37 @@ pub struct Extension {
pub binary_data: u32,
}

#[derive(PartialEq, Eq, Default, Debug, YaDeserialize, YaSerialize)]
pub struct SystemGeneral {
#[yaserde(attribute)]
pub version: String,

#[yaserde(rename = "timeZone")]
pub time_zone: Option<i32>,
pub year: Option<i32>,
pub month: Option<u8>,
pub day: Option<u8>,
pub hour: Option<u8>,
pub minute: Option<u8>,
pub second: Option<u8>,

#[yaserde(rename = "osdFormat")]
pub osd_format: Option<String>,
#[yaserde(rename = "timeFormat")]
pub time_format: Option<u8>,

pub language: Option<String>,
#[yaserde(rename = "deviceName")]
pub device_name: Option<String>,
}

#[derive(PartialEq, Eq, Default, Debug, YaDeserialize, YaSerialize)]
pub struct Norm {
#[yaserde(attribute)]
pub version: String,
norm: String,
}

pub fn xml_ver() -> String {
"1.1".to_string()
}
Expand Down
19 changes: 8 additions & 11 deletions src/bc_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ use std::time::Duration;
use Md5Trunc::*;

mod connection;
mod time;

pub struct BcCamera {
address: SocketAddr,
connection: Option<BcConnection>,
logged_in: bool,
rx_timeout: Duration,
}

use crate::Never;

type Result<T> = std::result::Result<T, Error>;

const RX_TIMEOUT: Duration = Duration::from_secs(5);

#[derive(Debug, Error)]
pub enum Error {
#[error(display = "Communication error")]
Expand Down Expand Up @@ -70,16 +72,11 @@ impl BcCamera {
address,
connection: None,
logged_in: false,
rx_timeout: Duration::from_secs(1),
})
}

pub fn set_rx_timeout(&mut self, timeout: Duration) {
self.rx_timeout = timeout;
}

pub fn connect(&mut self) -> Result<()> {
self.connection = Some(BcConnection::new(self.address, self.rx_timeout)?);
self.connection = Some(BcConnection::new(self.address, RX_TIMEOUT)?);
Ok(())
}

Expand Down Expand Up @@ -126,7 +123,7 @@ impl BcCamera {

sub_login.send(legacy_login)?;

let legacy_reply = sub_login.rx.recv_timeout(self.rx_timeout)?;
let legacy_reply = sub_login.rx.recv_timeout(RX_TIMEOUT)?;
let nonce;
match legacy_reply.body {
BcBody::ModernMsg(ModernMsg {
Expand Down Expand Up @@ -180,7 +177,7 @@ impl BcCamera {
);

sub_login.send(modern_login)?;
let modern_reply = sub_login.rx.recv_timeout(self.rx_timeout)?;
let modern_reply = sub_login.rx.recv_timeout(RX_TIMEOUT)?;

let device_info;
match modern_reply.body {
Expand Down Expand Up @@ -237,7 +234,7 @@ impl BcCamera {

sub_ping.send(ping)?;

sub_ping.rx.recv_timeout(self.rx_timeout)?;
sub_ping.rx.recv_timeout(RX_TIMEOUT)?;

Ok(())
}
Expand Down Expand Up @@ -271,7 +268,7 @@ impl BcCamera {

loop {
trace!("Getting video message...");
let msg = sub_video.rx.recv_timeout(self.rx_timeout)?;
let msg = sub_video.rx.recv_timeout(RX_TIMEOUT)?;
if let BcBody::ModernMsg(ModernMsg {
binary: Some(binary),
..
Expand Down
132 changes: 132 additions & 0 deletions src/bc_protocol/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use super::{BcCamera, Error, Result, RX_TIMEOUT};
use crate::bc::{model::*, xml::*};
use time::{date, Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};

impl BcCamera {
pub fn get_time(&self) -> Result<Option<OffsetDateTime>> {
let connection = self
.connection
.as_ref()
.expect("Must be connected to get time");
let sub_get_general = connection.subscribe(MSG_ID_GET_GENERAL)?;
let get = Bc {
meta: BcMeta {
msg_id: MSG_ID_GET_GENERAL,
client_idx: 0,
encrypted: true,
class: 0x6414,
},
body: BcBody::ModernMsg(ModernMsg::default()),
};

sub_get_general.send(get)?;
let msg = sub_get_general.rx.recv_timeout(RX_TIMEOUT)?;

if let BcBody::ModernMsg(ModernMsg {
xml:
Some(BcXml {
system_general:
Some(SystemGeneral {
time_zone: Some(time_zone),
year: Some(year),
month: Some(month),
day: Some(day),
hour: Some(hour),
minute: Some(minute),
second: Some(second),
..
}),
..
}),
..
}) = msg.body
{
let datetime = match try_build_timestamp(
time_zone, year, month, day, hour, minute, second
) {
Ok(dt) => dt,
Err(e) => return Err(Error::UnintelligibleReply {
reply: msg,
why: "Could not parse date",
})
};

// This code was written in 2020; I'm trying to catch all the possible epochs that
// cameras might reset themselves to. My B800 resets to Jan 1, 1999, but I can't
// guarantee that Reolink won't pick some newer date. Therefore, last year ought
// to be new enough, yet still distant enough that it won't interfere with anything
const BOUNDARY: Date = date!(2019-01-01);
thirtythreeforty marked this conversation as resolved.
Show resolved Hide resolved

// detect if no time is actually set, and return Ok(None): that is, operation
// succeeded, and there is no time set
if datetime.date() < BOUNDARY {
Ok(None)
} else {
Ok(Some(datetime))
}
} else {
Err(Error::UnintelligibleReply {
reply: msg,
why: "Reply did not contain SystemGeneral with all time fields filled out",
})
}
}

pub fn set_time(&self, timestamp: OffsetDateTime) -> Result<()> {
thirtythreeforty marked this conversation as resolved.
Show resolved Hide resolved
let connection = self
.connection
.as_ref()
.expect("Must be connected to set time");
let sub_set_general = connection.subscribe(MSG_ID_SET_GENERAL)?;
let set = Bc::new_from_xml(
BcMeta {
msg_id: MSG_ID_SET_GENERAL,
client_idx: 0,
encrypted: true,
class: 0x6414,
},
BcXml {
system_general: Some(SystemGeneral {
version: xml_ver(),
//osd_format: Some("MDY".to_string()),
time_format: Some(0),
// Reolink uses positive seconds to indicate a negative UTC offset:
time_zone: Some(-timestamp.offset().as_seconds()),
year: Some(timestamp.year()),
month: Some(timestamp.month()),
day: Some(timestamp.day()),
hour: Some(timestamp.hour()),
minute: Some(timestamp.minute()),
second: Some(timestamp.second()),
..Default::default()
}),
..Default::default()
},
);

sub_set_general.send(set)?;
let msg = sub_set_general.rx.recv_timeout(RX_TIMEOUT)?;

Ok(())
}
}

fn try_build_timestamp(
timezone: i32,
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
) -> std::result::Result<OffsetDateTime, time::ComponentRangeError> {
let date = Date::try_from_ymd(year, month, day)?;
let time = Time::try_from_hms(hour, minute, second)?;
let offset = if timezone > 0 {
UtcOffset::west_seconds(timezone as u32)
} else {
UtcOffset::east_seconds(-timezone as u32)
};

Ok(PrimitiveDateTime::new(date, time).assume_offset(offset))
}
1 change: 1 addition & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub struct CameraConfig {
pub username: String,
pub password: Option<String>,

// no longer used, but still here so we can warn users:
pub timeout: Option<Duration>,
thirtythreeforty marked this conversation as resolved.
Show resolved Hide resolved

#[validate(regex(
Expand Down
32 changes: 29 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ fn camera_main(
let mut connected = false;
(|| {
let mut camera = BcCamera::new_with_addr(camera_config.camera_addr)?;
if let Some(timeout) = camera_config.timeout {
camera.set_rx_timeout(timeout);
if let Some(_) = camera_config.timeout {
warn!("The undocumented `timeout` config option has been removed and is no longer needed.");
warn!("Please update your config file.");
}

info!(
Expand All @@ -208,9 +209,34 @@ fn camera_main(
camera.login(&camera_config.username, camera_config.password.as_deref())?;

connected = true;
info!("{}: Connected and logged in", camera_config.name);

let cam_time = camera.get_time()?;
if let Some(time) = cam_time {
info!(
"{}: Camera time is already set: {}",
camera_config.name, time
);
} else {
let new_time = time::OffsetDateTime::now_local();
warn!(
"{}: Camera has no time set, setting to {}",
camera_config.name, new_time
);
camera.set_time(new_time)?;
let cam_time = camera.get_time()?;
if let Some(time) = cam_time {
info!(
"{}: Camera time is now set: {}",
camera_config.name, time
);
} else {
error!("{}: Camera did not accept new time (is {} an admin?)", camera_config.name, camera_config.username);
}
}

info!(
"{}: Connected to camera, starting video stream {}",
"{}: Starting video stream {}",
camera_config.name, stream_name
);
camera.start_video(output, stream_name)
Expand Down