diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 483c21e2..114e574f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ jobs: name: native runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-22.04, windows-2022, macos-12] steps: diff --git a/README.md b/README.md index 55927713..0d621dd8 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ name = "Camera02" username = "admin" password = "password" uid = "BCDEF0123456789A" +address = "192.168.1.10" ``` Create a text file with called `neolink.toml` in the same folder as the neolink binary. With your config options. @@ -93,6 +94,40 @@ using the terminal in the same folder the neolink binary is in. ``` +### Discovery + +To connect to a camera using a UID we need to find the IP address of the camera with that UID + +The IP is discovered with four methods + +1. Local discovery: Here we send a broadcast on all visible networks asking the local + network if there is a camera with this UID. This only works if the network supports broadcasts + + If you know the ip address you can put it into the `address` field of the config and attempt a + direct connection without broadcasts. This requires a route from neolink to the camera. + +2. Remote discovery: Here a ask the reolink servers what the IP address is. This requires that + we contact reolink and provide some basic information like the UID. Once we have this information + we connect directly to the local IP address. This requires a route from neolink to the camera and + for the camera to be able to contact the reolink IPs. + +3. Map discovery: In this case we register our IP address with reolink and ask the camera to connect to us. + Once the camera either polls/recives a connect request from the reolink servers the camera will then + initiate a connect to neolink. This requires that our IP and the reolink IPs are reacable from the camera. + +4. Relay: In this case we request that reolink relay our connection. Neolink nor the camera need to be able to + direcly contact each other. But both neolink and the camera need to be able to contact reolink. + +This can be controlled with the config + +```toml +discovery = "local" +``` + +In the `[[cameras]]` section of the toml. + +Possible values are `local`, `remote`, `map`, `relay` later values implictly enable prior methods. + ### MQTT To use mqtt you will to adjust your config file as such: @@ -125,7 +160,8 @@ Control messages: - `/control/led [on|off]` Turns status LED on/off - `/control/ir [on|off|auto]` Turn IR lights on/off or automatically via light detection - `/control/reboot` Reboot the camera -- `/control/ptz` [up|down|left|right|in|out] (amount) Control the PTZ movements, amount defaults to 32.0 +- `/control/ptz [up|down|left|right|in|out]` (amount) Control the PTZ movements, amount defaults to 32.0 +- `/control/pir [on|off]` Status Messages: `/status offline` Sent when the neolink goes offline this is a LastWill message diff --git a/crates/core/src/bc_protocol.rs b/crates/core/src/bc_protocol.rs index 3699f7b3..338ec7b2 100644 --- a/crates/core/src/bc_protocol.rs +++ b/crates/core/src/bc_protocol.rs @@ -2,7 +2,7 @@ use crate::bc; use futures::stream::StreamExt; use log::*; use serde::{Deserialize, Serialize}; -use std::net::ToSocketAddrs; +use std::net::{IpAddr, SocketAddr}; use std::{ collections::HashMap, sync::atomic::{AtomicBool, AtomicU16, Ordering}, @@ -32,7 +32,7 @@ mod time; mod version; pub(crate) use connection::*; -pub(crate) use credentials::*; +pub use credentials::*; pub use errors::Error; pub use ledstate::LightState; pub use login::MaxEncryption; @@ -65,7 +65,31 @@ pub struct BcCamera { abilities: RwLock>, } -/// Used to choode the print format of various status messages like battery levels +/// Options used to construct a camera +#[derive(Debug)] +pub struct BcCameraOpt { + /// Name, mostly used for message logs + pub name: String, + /// Channel the camera is on 0 unless using a NVR + pub channel_id: u8, + /// IPs of the camera + pub addrs: Vec, + /// The UID of the camera + pub uid: Option, + /// Port to try optional. When not given all known BC ports will be tried + /// When given all known bc port AND the given port will be tried + pub port: Option, + /// Protocol decides if UDP/TCP are used for the camera + pub protocol: ConnectionProtocol, + /// Discovery method to allow + pub discovery: DiscoveryMethods, + /// Printing format for auxilaary data such as battery levels + pub aux_printing: PrintFormat, + /// Credentials for login + pub credentials: Credentials, +} + +/// Used to choose the print format of various status messages like battery levels /// /// Currently this is just the format of battery levels but if we ever got more status /// messages then they will also use this information @@ -79,262 +103,221 @@ pub enum PrintFormat { Xml, } +/// Type of connection to try +#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)] +pub enum ConnectionProtocol { + /// TCP and UDP + #[default] + TcpUdp, + /// TCP only + Tcp, + /// Udp only + Udp, +} + +enum CameraLocation { + Tcp(SocketAddr), + Udp(DiscoveryResult), +} + impl BcCamera { - /// - /// Create a new camera interface with this address and channel ID - /// - /// # Parameters - /// - /// * `host` - The address of the camera either ip address or hostname string - /// - /// * `channel_id` - The channel ID this is usually zero unless using a NVR - /// - /// # Returns - /// - /// returns either an error or the camera - /// - pub async fn new_with_addr, W: Into>( - host: U, - channel_id: u8, - username: V, - passwd: Option, - aux_info_format: PrintFormat, - ) -> Result { - let username: String = username.into(); - let passwd: Option = passwd.map(|t| t.into()); - let addr_iter = match host.to_socket_addrs() { - Ok(iter) => iter, - Err(_) => return Err(Error::AddrResolutionError), - }; - for addr in addr_iter { - if let Ok(cam) = Self::new( - SocketAddrOrUid::SocketAddr(addr), - channel_id, - &username, - passwd.as_ref(), - aux_info_format, - ) - .await - { - return Ok(cam); + /// Try to connect to the camera via appropaite methods and return + /// the location that should be used + async fn find_camera(options: &BcCameraOpt) -> Result { + let mut tcp_set = tokio::task::JoinSet::new(); + let mut local_set = tokio::task::JoinSet::new(); + let mut remote_set = tokio::task::JoinSet::new(); + let mut map_set = tokio::task::JoinSet::new(); + let mut relay_set = tokio::task::JoinSet::new(); + + if let ConnectionProtocol::Tcp | ConnectionProtocol::TcpUdp = options.protocol { + let mut sockets = vec![]; + match options.port { + Some(9000) | None => { + for addr in options.addrs.iter() { + sockets.push(SocketAddr::new(*addr, 9000)); + } + } + Some(n) => { + for addr in options.addrs.iter() { + sockets.push(SocketAddr::new(*addr, n)); + sockets.push(SocketAddr::new(*addr, 9000)); + } + } + } + for socket in sockets.drain(..) { + let channel_id: u8 = options.channel_id; + let name: String = options.name.clone(); + tcp_set.spawn(async move { + Discovery::check_tcp(socket, channel_id).await.map(|_| { + info!("{}: TCP Discovery success at {:?}", name, &socket); + socket + }) + }); } } - Err(Error::CannotInitCamera) - } - - /// - /// Create a new camera interface with this uid and channel ID - /// - /// # Parameters - /// - /// * `uid` - The uid of the camera - /// - /// * `channel_id` - The channel ID this is usually zero unless using a NVR - /// - /// # Returns - /// - /// returns either an error or the camera - /// - pub async fn new_with_uid, V: Into>( - uid: &str, - channel_id: u8, - username: U, - passwd: Option, - discovery_method: DiscoveryMethods, - aux_info_format: PrintFormat, - ) -> Result { - Self::new( - SocketAddrOrUid::Uid(uid.to_string(), discovery_method), - channel_id, - username, - passwd, - aux_info_format, - ) - .await - } + if let (Some(uid), ConnectionProtocol::Udp | ConnectionProtocol::TcpUdp) = + (options.uid.as_ref(), options.protocol) + { + let mut sockets = vec![]; + match options.port { + None | Some(2015) | Some(2018) => { + for addr in options.addrs.iter() { + sockets.push(SocketAddr::new(*addr, 2018)); + sockets.push(SocketAddr::new(*addr, 2015)); + } + } + Some(n) => { + for addr in options.addrs.iter() { + sockets.push(SocketAddr::new(*addr, n)); + sockets.push(SocketAddr::new(*addr, 2015)); + sockets.push(SocketAddr::new(*addr, 2018)); + } + } + } + let (allow_local, allow_remote, allow_map, allow_relay) = match options.discovery { + DiscoveryMethods::None => (false, false, false, false), + DiscoveryMethods::Local => (true, false, false, false), + DiscoveryMethods::Remote => (true, true, false, false), + DiscoveryMethods::Map => (true, true, true, false), + DiscoveryMethods::Relay => (true, true, true, true), + DiscoveryMethods::Debug => (false, false, false, true), + }; - /// - /// Create a new camera interface with this address/uid and channel ID - /// - /// This method will first perform hostname resolution on the address - /// then fallback to uid if that resolution fails. - /// - /// Be aware that it is possible (although unlikely) that there is - /// a dns entry with the same address as the uid. If uncertain use - /// one of the other methods. - /// - /// # Parameters - /// - /// * `host` - The address of the camera either ip address, hostname string, or uid - /// - /// * `channel_id` - The channel ID this is usually zero unless using a NVR - /// - /// # Returns - /// - /// returns either an error or the camera - /// - pub async fn new_with_addr_or_uid, W: Into>( - host: U, - channel_id: u8, - username: V, - passwd: Option, - aux_info_format: PrintFormat, - ) -> Result { - let addr_iter = match host.to_socket_addrs_or_uid() { - Ok(iter) => iter, - Err(_) => return Err(Error::AddrResolutionError), - }; - let username: String = username.into(); - let passwd: Option = passwd.map(|t| t.into()); - for addr_or_uid in addr_iter { - if let Ok(cam) = Self::new( - addr_or_uid, - channel_id, - &username, - passwd.as_ref(), - aux_info_format, - ) - .await - { - return Ok(cam); + if allow_local { + let uid_local = uid.clone(); + let name: String = options.name.clone(); + local_set.spawn(async move { + trace!("{}: Starting Local discovery", name); + let result = Discovery::local(&uid_local, Some(sockets)).await; + if let Ok(disc) = &result { + info!( + "{}: Local discovery success {} at {}", + name, + uid_local, + disc.get_addr() + ); + } + result + }); + } + if allow_remote { + let uid_remote = uid.clone(); + let name: String = options.name.clone(); + remote_set.spawn(async move { + trace!("Starting Remote discovery"); + let result = Discovery::remote(&uid_remote).await; + if let Ok(disc) = &result { + info!( + "{}: Remote discovery success {} at {}", + name, + uid_remote, + disc.get_addr() + ); + } + result + }); + } + if allow_map { + let uid_map = uid.clone(); + let name: String = options.name.clone(); + map_set.spawn(async move { + trace!("Starting Map"); + let result = Discovery::map(&uid_map).await; + if let Ok(disc) = &result { + info!("{}: Map success {} at {}", name, uid_map, disc.get_addr()); + } + result + }); + } + if allow_relay { + let uid_relay = uid.clone(); + let name: String = options.name.clone(); + relay_set.spawn(async move { + trace!("Starting Relay"); + let result = Discovery::relay(&uid_relay).await; + if let Ok(disc) = &result { + info!( + "{}: Relay success {} at {}", + name, + uid_relay, + disc.get_addr() + ); + } + result + }); } } + // Wait for all TCP to finish + while let Some(result) = tcp_set.join_next().await { + if let Ok(Ok(addr)) = result { + return Ok(CameraLocation::Tcp(addr)); + } + } + // Tcp failed see if Local faired any better + // Wait for all local to finish + while let Some(result) = local_set.join_next().await { + if let Ok(Ok(discovery)) = result { + return Ok(CameraLocation::Udp(discovery)); + } + } + // Local failed see if Remote faired any better + // Wait for all remote to finish + while let Some(result) = remote_set.join_next().await { + if let Ok(Ok(discovery)) = result { + return Ok(CameraLocation::Udp(discovery)); + } + } + // Remote failed see if Map faired any better + // Wait for all Map to finish + while let Some(result) = map_set.join_next().await { + if let Ok(Ok(discovery)) = result { + return Ok(CameraLocation::Udp(discovery)); + } + } + // Map failed see if Relay faired any better + // Wait for all Relay to finish + while let Some(result) = relay_set.join_next().await { + if let Ok(Ok(discovery)) = result { + return Ok(CameraLocation::Udp(discovery)); + } + } + // Nothing works Err(Error::CannotInitCamera) } /// - /// Create a new camera interface with this address/uid and channel ID + /// Create a new camera interface /// /// # Parameters /// - /// * `addr` - An enum of [`SocketAddrOrUid`] that contains the address - /// - /// * `channel_id` - The channel ID this is usually zero unless using a NVR - /// - /// * `username` - The username to login with - /// - /// * `passed` - The password to login with required for AES encrypted camera + /// * `options` - Camera information see [`BcCameraOpt] /// /// # Returns /// /// returns either an error or the camera /// - pub async fn new, V: Into>( - addr: SocketAddrOrUid, - channel_id: u8, - username: U, - passwd: Option, - aux_info_format: PrintFormat, - ) -> Result { - let username: String = username.into(); - let passwd: Option = passwd.map(|t| t.into()); - - let (sink, source): (BcConnSink, BcConnSource) = match addr { - SocketAddrOrUid::SocketAddr(addr) => { - trace!("Trying address {}", addr); - let (x, r) = TcpSource::new(addr, &username, passwd.as_ref()) - .await? - .split(); - (Box::new(x), Box::new(r)) - } - SocketAddrOrUid::Uid(uid, method) => { - trace!("Trying uid {}", uid); - // TODO Make configurable - let (allow_local, allow_remote, allow_map, allow_relay) = match method { - DiscoveryMethods::None => (false, false, false, false), - DiscoveryMethods::Local => (true, false, false, false), - DiscoveryMethods::Remote => (true, true, false, false), - DiscoveryMethods::Map => (true, true, true, false), - DiscoveryMethods::Relay => (true, true, true, true), - DiscoveryMethods::Debug => (false, false, false, true), - }; + pub async fn new(options: BcCameraOpt) -> Result { + let username: String = options.credentials.username.clone(); + let passwd: Option = options.credentials.password.clone(); - let discovery = { - let mut set = tokio::task::JoinSet::new(); - if allow_local { - let uid_local = uid.clone(); - set.spawn(async move { - trace!("Starting Local discovery"); - let result = Discovery::local(&uid_local).await; - if let Ok(disc) = &result { - info!( - "Local discovery success {} at {}", - uid_local, - disc.get_addr() - ); - } - result - }); - } - if allow_remote { - let uid_remote = uid.clone(); - set.spawn(async move { - trace!("Starting Remote discovery"); - let result = Discovery::remote(&uid_remote).await; - if let Ok(disc) = &result { - info!( - "Remote discovery success {} at {}", - uid_remote, - disc.get_addr() - ); - } - result - }); - } - if allow_map { - let uid_relay = uid.clone(); - set.spawn(async move { - trace!("Starting Map"); - let result = Discovery::map(&uid_relay).await; - if let Ok(disc) = &result { - info!("Map success {} at {}", uid_relay, disc.get_addr()); - } - result - }); - } - if allow_relay { - let uid_relay = uid.clone(); - set.spawn(async move { - trace!("Starting Relay"); - let result = Discovery::relay(&uid_relay).await; - if let Ok(disc) = &result { - info!("Relay success {} at {}", uid_relay, disc.get_addr()); - } - result - }); - } - - let last_result; - loop { - match set.join_next().await { - Some(Ok(Ok(disc))) => { - last_result = Ok(disc); - break; - } - Some(Ok(Err(e))) => { - debug!("Discovery Error: {:?}", e); - } - Some(Err(join_error)) => { - last_result = Err(Error::OtherString(format!( - "Panic while joining Discovery threads: {:?}", - join_error - ))); - break; - } - None => { - last_result = Err(Error::DiscoveryTimeout); - break; - } - } - } - last_result - }?; - let (x, r) = UdpSource::new_from_discovery(discovery, &username, passwd.as_ref()) - .await? - .split(); - (Box::new(x), Box::new(r)) + let (sink, source): (BcConnSink, BcConnSource) = { + match BcCamera::find_camera(&options).await? { + CameraLocation::Tcp(addr) => { + let (x, r) = TcpSource::new(addr, &username, passwd.as_ref()) + .await? + .split(); + (Box::new(x), Box::new(r)) + } + CameraLocation::Udp(discovery) => { + let (x, r) = + UdpSource::new_from_discovery(discovery, &username, passwd.as_ref()) + .await? + .split(); + (Box::new(x), Box::new(r)) + } } }; @@ -344,13 +327,13 @@ impl BcCamera { let me = Self { connection: Arc::new(conn), message_num: AtomicU16::new(0), - channel_id, + channel_id: options.channel_id, logged_in: AtomicBool::new(false), credentials: Credentials::new(username, passwd), abilities: Default::default(), }; me.keepalive().await?; - if let Err(e) = me.monitor_battery(aux_info_format).await { + if let Err(e) = me.monitor_battery(options.aux_printing).await { warn!("Could not monitor battery: {:?}", e); } Ok(me) diff --git a/crates/core/src/bc_protocol/connection/discovery.rs b/crates/core/src/bc_protocol/connection/discovery.rs index 25c273a3..acf205f2 100644 --- a/crates/core/src/bc_protocol/connection/discovery.rs +++ b/crates/core/src/bc_protocol/connection/discovery.rs @@ -3,6 +3,8 @@ //! Given a UID find the associated IP //! use super::DiscoveryResult; +use crate::bc::model::*; +use crate::bc_protocol::{md5_string, Md5Trunc, TcpSource}; use crate::bcudp::codex::BcUdpCodex; use crate::bcudp::model::*; use crate::bcudp::xml::*; @@ -25,7 +27,7 @@ use tokio::{ Mutex, RwLock, }, task::JoinSet, - time::{interval, Duration}, + time::{interval, timeout, Duration}, }; use tokio_stream::wrappers::ReceiverStream; use tokio_util::udp::UdpFramed; @@ -70,6 +72,10 @@ lazy_static! { "p2p14.reolink.com", "p2p15.reolink.com", ]; + /// Maximum wait for a reply + static ref MAXIMUM_WAIT: Duration = Duration::from_secs(5); + /// How long to wait before resending + static ref RESEND_WAIT: Duration = Duration::from_millis(500); } type Subscriber = Arc>>>>; @@ -234,9 +240,7 @@ impl Discoverer { let mut reply = ReceiverStream::new(self.subscribe(target_tid).await?); let msg = BcUdp::Discovery(disc); - let maximum_wait = Duration::from_secs(60); - let resend_wait = Duration::from_millis(500); - let mut inter = interval(resend_wait); + let mut inter = interval(*RESEND_WAIT); let result = tokio::select! { v = async { @@ -261,7 +265,7 @@ impl Discoverer { } => {Err::(v)}, _ = { // Sleep then emit Timeout - tokio::time::sleep(maximum_wait) + tokio::time::sleep(*MAXIMUM_WAIT) } => { Err::(Error::DiscoveryTimeout) } @@ -760,8 +764,48 @@ impl Discoverer { } impl Discovery { + // Check if TCP is possible + // + // To do this we send a dummy login and see if it replies with any BC packet + pub(crate) async fn check_tcp(addr: SocketAddr, channel_id: u8) -> Result<()> { + let username = "admin"; + let password = Some("123456"); + let mut tcp_source = + timeout(*MAXIMUM_WAIT, TcpSource::new(addr, username, password)).await??; + + let md5_username = md5_string(username, Md5Trunc::ZeroLast); + let md5_password = password + .map(|p| md5_string(p, Md5Trunc::ZeroLast)) + .unwrap_or_else(|| EMPTY_LEGACY_PASSWORD.to_owned()); + + tcp_source + .send(Bc { + meta: BcMeta { + msg_id: MSG_ID_LOGIN, + channel_id, + msg_num: 0, + stream_type: 0, + response_code: 0x00, + class: 0x6514, + }, + body: BcBody::LegacyMsg(LegacyMsg::LoginMsg { + username: md5_username, + password: md5_password, + }), + }) + .await?; + + let _bc: Bc = timeout(*MAXIMUM_WAIT, tcp_source.next()) + .await? + .ok_or(Error::CannotInitCamera)??; // Successful recv should mean a Bc packet if not then deser will fail + Ok(()) + } + // Perform UDP broadcast lookup and connection - pub(crate) async fn local(uid: &str) -> Result { + pub(crate) async fn local( + uid: &str, + mut optional_addrs: Option>, + ) -> Result { let discoverer = Discoverer::new().await?; let client_id = generate_cid(); @@ -784,7 +828,11 @@ impl Discovery { }, }; - let dests = get_broadcasts(&[2015, 2018])?; + let mut dests = get_broadcasts(&[2015, 2018])?; + if let Some(mut optional_addrs) = optional_addrs.take() { + trace!("Also sending to {:?}", optional_addrs); + dests.append(&mut optional_addrs); + } let (camera_address, camera_id) = discoverer .retry_send_multi(msg, &dests, |bc, addr| match bc { UdpDiscovery { diff --git a/crates/core/src/bc_protocol/credentials.rs b/crates/core/src/bc_protocol/credentials.rs index 6a0c1e80..ccff7e23 100644 --- a/crates/core/src/bc_protocol/credentials.rs +++ b/crates/core/src/bc_protocol/credentials.rs @@ -2,11 +2,13 @@ use std::convert::TryInto; -/// Used for caching the credentials +/// Used for caching and supplying the credentials #[derive(Clone)] -pub(crate) struct Credentials { - pub(crate) username: String, - pub(crate) password: Option, +pub struct Credentials { + /// The username to login to the camera with + pub username: String, + /// The password to use for login. Some camera allow this to be ommited + pub password: Option, } impl Default for Credentials { diff --git a/crates/core/src/bc_protocol/resolution.rs b/crates/core/src/bc_protocol/resolution.rs index 5c169cc0..f4422098 100644 --- a/crates/core/src/bc_protocol/resolution.rs +++ b/crates/core/src/bc_protocol/resolution.rs @@ -1,6 +1,7 @@ //! This is a helper module to resolve either to a UID or a SockerAddr use log::*; +use serde::{Deserialize, Serialize}; use std::{ io::Error, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}, @@ -10,29 +11,35 @@ use std::{ /// /// This is used for UID lookup, it is unused with /// TPC/known ip address cameras -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub enum DiscoveryMethods { /// Forbid all discovery methods. Only TCP connections with known addresses will work + #[serde(alias = "none")] None, /// Allow local discovery on the local network using broadcasts /// This method does NOT contact reolink servers + #[serde(alias = "local")] Local, /// Allow contact with the reolink servers to learn the ip address but DO NOT /// allow the camera/clinet to communicate through the reolink servers. /// /// **This also enabled `Local` discovery** + #[serde(alias = "remote")] Remote, /// Allow contact with the reolink servers to learn the ip address and map the connection /// from dev to client through those servers. /// /// **This also enabled `Local` and `Remote` discovery** + #[serde(alias = "map")] Map, /// Allow contact with the reolink servers to learn the ip address and relay the connection /// client to dev through those servers. /// /// **This also enabled `Local`, `Map` and `Remote` discovery** + #[serde(alias = "relay")] Relay, #[doc(hidden)] + #[serde(alias = "debug")] /// Used for debugging it is set to whatever the dev is currently testing Debug, } @@ -42,7 +49,7 @@ pub enum SocketAddrOrUid { /// When the result is a addr it will be this SocketAddr(SocketAddr), /// When the result is a UID - Uid(String, DiscoveryMethods), + Uid(String, Option>, DiscoveryMethods), } /// An extension of ToSocketAddrs that will also resolve to a camera UID @@ -83,6 +90,7 @@ impl ToSocketAddrsOrUid for str { if re.is_match(self) { Ok(vec![SocketAddrOrUid::Uid( self.to_string(), + None, DiscoveryMethods::Local, )] .into_iter()) @@ -110,6 +118,7 @@ impl ToSocketAddrsOrUid for String { if re.is_match(self) { Ok(vec![SocketAddrOrUid::Uid( self.to_string(), + None, DiscoveryMethods::Local, )] .into_iter()) diff --git a/src/config.rs b/src/config.rs index a6a8243e..41736bf2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ use lazy_static::lazy_static; -use neolink_core::bc_protocol::PrintFormat; +use neolink_core::bc_protocol::{DiscoveryMethods, PrintFormat}; use regex::Regex; use serde::Deserialize; use std::clone::Clone; @@ -11,8 +11,6 @@ lazy_static! { Regex::new(r"^(mainStream|subStream|externStream|both|all)$").unwrap(); static ref RE_TLS_CLIENT_AUTH: Regex = Regex::new(r"^(none|request|require)$").unwrap(); static ref RE_PAUSE_MODE: Regex = Regex::new(r"^(black|still|test|none)$").unwrap(); - static ref RE_DISC_SRC: Regex = - Regex::new(r"^([nN]one|[lL]ocal|[rR]emote|[Mm]ap|[rR]elay|[Dd]ebug)$").unwrap(); static ref RE_MAXENC_SRC: Regex = Regex::new(r"^([nN]one|[Aa][Ee][Ss]|[Bb][Cc][Ee][Nn][Cc][Rr][Yy][Pp][Tt])$").unwrap(); } @@ -82,12 +80,7 @@ pub(crate) struct CameraConfig { pub(crate) pause: PauseConfig, #[serde(default = "default_discovery")] - #[validate(regex( - path = "RE_DISC_SRC", - message = "Invalid discovery method", - code = "discovery" - ))] - pub(crate) discovery: String, + pub(crate) discovery: DiscoveryMethods, #[serde(default = "default_maxenc")] #[validate(regex( @@ -151,8 +144,8 @@ fn default_print() -> PrintFormat { PrintFormat::None } -fn default_discovery() -> String { - "Relay".to_string() +fn default_discovery() -> DiscoveryMethods { + DiscoveryMethods::Relay } fn default_maxenc() -> String { @@ -248,9 +241,6 @@ fn validate_camera_config(camera_config: &CameraConfig) -> Result<(), Validation (None, None) => Err(ValidationError::new( "Either camera address or uid must be given", )), - (Some(_), Some(_)) => Err(ValidationError::new( - "Must provide either camera address or uid not both", - )), _ => Ok(()), } } diff --git a/src/mqtt/event_cam.rs b/src/mqtt/event_cam.rs index c2fa8129..5ce2cc65 100644 --- a/src/mqtt/event_cam.rs +++ b/src/mqtt/event_cam.rs @@ -158,12 +158,7 @@ impl EventCam { camera_config.name, camera_addr ); let camera = camera_addr - .connect_camera( - camera_config.channel_id, - &camera_config.username, - camera_config.password.as_ref(), - camera_config.print_format, - ) + .connect_camera(camera_config) .await .with_context(|| { format!( diff --git a/src/rtsp/states/mod.rs b/src/rtsp/states/mod.rs index 3cfe7c0e..6f3daa70 100644 --- a/src/rtsp/states/mod.rs +++ b/src/rtsp/states/mod.rs @@ -62,20 +62,12 @@ impl RtspCamera { })?; info!("{}: Connecting to camera at {}", config.name, camera_addr); - let camera = camera_addr - .connect_camera( - config.channel_id, - &config.username, - config.password.as_ref(), - config.print_format, + let camera = camera_addr.connect_camera(config).await.with_context(|| { + format!( + "Failed to connect to camera {} at {} on channel {}", + config.name, camera_addr, config.channel_id ) - .await - .with_context(|| { - format!( - "Failed to connect to camera {} at {} on channel {}", - config.name, camera_addr, config.channel_id - ) - })?; + })?; let mut streams: HashSet = Default::default(); if ["all", "both", "mainStream", "mainstream"] diff --git a/src/utils.rs b/src/utils.rs index 58f2a56f..2d1e7d14 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,17 +4,27 @@ use log::*; use super::config::{CameraConfig, Config}; use anyhow::{anyhow, Context, Error, Result}; -use neolink_core::bc_protocol::{BcCamera, DiscoveryMethods, MaxEncryption, PrintFormat}; -use std::fmt::{Display, Error as FmtError, Formatter}; +use neolink_core::bc_protocol::{ + BcCamera, BcCameraOpt, ConnectionProtocol, Credentials, DiscoveryMethods, MaxEncryption, +}; +use std::{ + fmt::{Display, Error as FmtError, Formatter}, + net::{IpAddr, ToSocketAddrs}, + str::FromStr, +}; pub(crate) enum AddressOrUid { Address(String), Uid(String, DiscoveryMethods), + AddressWithUid(String, String, DiscoveryMethods), } impl Display for AddressOrUid { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { match self { + AddressOrUid::AddressWithUid(addr, uid, _) => { + write!(f, "Address: {}, UID: {}", addr, uid) + } AddressOrUid::Address(host) => write!(f, "Address: {}", host), AddressOrUid::Uid(host, _) => write!(f, "UID: {}", host), } @@ -26,55 +36,66 @@ impl AddressOrUid { pub(crate) fn new( address: &Option, uid: &Option, - disc_method: &str, + method: &DiscoveryMethods, ) -> Result { match (address, uid) { (None, None) => Err(anyhow!("Neither address or uid given")), - (Some(_), Some(_)) => Err(anyhow!("Either address or uid should be given not both")), + (Some(host), Some(uid)) => Ok(AddressOrUid::AddressWithUid( + host.clone(), + uid.clone(), + *method, + )), (Some(host), None) => Ok(AddressOrUid::Address(host.clone())), - (None, Some(host)) => { - let method = match disc_method.to_lowercase().as_str() { - "none" => DiscoveryMethods::None, - "local" => DiscoveryMethods::Local, - "remote" => DiscoveryMethods::Remote, - "map" => DiscoveryMethods::Map, - "relay" => DiscoveryMethods::Relay, - "debug" => DiscoveryMethods::Debug, - n => { - warn!("Unrecognised discovery method: {}. Using Local", n); - DiscoveryMethods::Local - } - }; - Ok(AddressOrUid::Uid(host.clone(), method)) - } + (None, Some(host)) => Ok(AddressOrUid::Uid(host.clone(), *method)), } } // Convience method to get the BcCamera with the appropiate method - pub(crate) async fn connect_camera, U: Into>( + // from a camera_config + pub(crate) async fn connect_camera( &self, - channel_id: u8, - username: T, - passwd: Option, - aux_print_format: PrintFormat, + camera_config: &CameraConfig, ) -> Result { - match self { - AddressOrUid::Address(host) => { - Ok( - BcCamera::new_with_addr(host, channel_id, username, passwd, aux_print_format) - .await?, - ) + let (port, addrs) = { + if let Some(addr_str) = camera_config.camera_addr.as_ref() { + match addr_str.to_socket_addrs() { + Ok(addr_iter) => { + let mut port = None; + let mut ipaddrs = vec![]; + for addr in addr_iter { + port = Some(addr.port()); + ipaddrs.push(addr.ip()); + } + Ok((port, ipaddrs)) + } + Err(_) => match IpAddr::from_str(addr_str) { + Ok(ip) => Ok((None, vec![ip])), + Err(_) => Err(anyhow!("Could not parse address in config")), + }, + } + } else { + Ok((None, vec![])) } - AddressOrUid::Uid(host, method) => Ok(BcCamera::new_with_uid( - host, - channel_id, - username, - passwd, - *method, - aux_print_format, - ) - .await?), - } + }?; + + let options = BcCameraOpt { + name: camera_config.name.clone(), + channel_id: camera_config.channel_id, + addrs, + port, + uid: camera_config.camera_uid.clone(), + protocol: ConnectionProtocol::TcpUdp, + discovery: camera_config.discovery, + aux_printing: camera_config.print_format, + credentials: Credentials { + username: camera_config.username.clone(), + password: camera_config.password.clone(), + }, + }; + + trace!("Camera Info: {:?}", options); + + Ok(BcCamera::new(options).await?) } } @@ -96,12 +117,7 @@ pub(crate) async fn connect_and_login(camera_config: &CameraConfig) -> Result