read-only signals support (#28)

This is mostly untested and useless by itself, but it's a starting
point. In particular:

* there's no way to set up signals or add/remove/update events yet
  except by manual changes to the database.
* if you associate a signal with a camera then remove the camera,
  hitting /api/ will error out.
This commit is contained in:
Scott Lamb
2019-06-06 16:18:13 -07:00
parent cb1bb5d810
commit 6f2c63ffac
9 changed files with 833 additions and 47 deletions

View File

@@ -31,7 +31,7 @@
use db::auth::SessionHash;
use failure::{Error, format_err};
use serde::Serialize;
use serde::ser::{SerializeMap, SerializeSeq, Serializer};
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
use std::collections::BTreeMap;
use std::ops::Not;
use uuid::Uuid;
@@ -46,7 +46,14 @@ pub struct TopLevel<'a> {
#[serde(serialize_with = "TopLevel::serialize_cameras")]
pub cameras: (&'a db::LockedDatabase, bool),
#[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<Session>,
#[serde(serialize_with = "TopLevel::serialize_signals")]
pub signals: (&'a db::LockedDatabase, bool),
#[serde(serialize_with = "TopLevel::serialize_signal_types")]
pub signal_types: &'a db::LockedDatabase,
}
#[derive(Debug, Serialize)]
@@ -94,6 +101,44 @@ pub struct Stream<'a> {
pub days: Option<&'a BTreeMap<db::StreamDayKey, db::StreamDayValue>>,
}
#[derive(Serialize)]
#[serde(rename_all="camelCase")]
pub struct Signal<'a> {
#[serde(serialize_with = "Signal::serialize_cameras")]
pub cameras: (&'a db::Signal, &'a db::LockedDatabase),
pub source: Uuid,
pub type_: Uuid,
pub short_name: &'a str,
}
#[derive(Default, Serialize)]
#[serde(rename_all="camelCase")]
pub struct Signals {
pub times_90k: Vec<i64>,
pub signal_ids: Vec<u32>,
pub states: Vec<u16>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
pub struct SignalType<'a> {
pub uuid: Uuid,
#[serde(serialize_with = "SignalType::serialize_states")]
pub states: &'a db::signal::Type,
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
pub struct SignalTypeState<'a> {
value: u16,
name: &'a str,
#[serde(skip_serializing_if = "Not::not")]
motion: bool,
color: &'a str,
}
impl<'a> Camera<'a> {
pub fn wrap(c: &'a db::Camera, db: &'a db::LockedDatabase, include_days: bool) -> Result<Self, Error> {
Ok(Camera {
@@ -158,6 +203,66 @@ impl<'a> Stream<'a> {
}
}
impl<'a> Signal<'a> {
pub fn wrap(s: &'a db::Signal, db: &'a db::LockedDatabase, _include_days: bool) -> Self {
Signal {
cameras: (s, db),
source: s.source,
type_: s.type_,
short_name: &s.short_name,
}
}
fn serialize_cameras<S>(cameras: &(&db::Signal, &db::LockedDatabase),
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
let (s, db) = cameras;
let mut map = serializer.serialize_map(Some(s.cameras.len()))?;
for sc in &s.cameras {
let c = db.cameras_by_id()
.get(&sc.camera_id)
.ok_or_else(|| S::Error::custom(format!("signal has missing camera id {}",
sc.camera_id)))?;
map.serialize_key(&c.uuid)?;
map.serialize_value(match sc.type_ {
db::signal::SignalCameraType::Direct => "direct",
db::signal::SignalCameraType::Indirect => "indirect",
})?;
}
map.end()
}
}
impl<'a> SignalType<'a> {
pub fn wrap(uuid: Uuid, type_: &'a db::signal::Type) -> Self {
SignalType {
uuid,
states: type_,
}
}
fn serialize_states<S>(type_: &db::signal::Type,
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
let mut seq = serializer.serialize_seq(Some(type_.states.len()))?;
for s in &type_.states {
seq.serialize_element(&SignalTypeState::wrap(s))?;
}
seq.end()
}
}
impl<'a> SignalTypeState<'a> {
pub fn wrap(s: &'a db::signal::TypeState) -> Self {
SignalTypeState {
value: s.value,
name: &s.name,
motion: s.motion,
color: &s.color,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
struct StreamDayValue {
@@ -175,7 +280,33 @@ impl<'a> TopLevel<'a> {
let cs = db.cameras_by_id();
let mut seq = serializer.serialize_seq(Some(cs.len()))?;
for (_, c) in cs {
seq.serialize_element(&Camera::wrap(c, db, include_days).unwrap())?; // TODO: no unwrap.
seq.serialize_element(
&Camera::wrap(c, db, include_days).map_err(|e| S::Error::custom(e))?)?;
}
seq.end()
}
/// Serializes signals as a list (rather than a map), optionally including the `days` field.
fn serialize_signals<S>(signals: &(&db::LockedDatabase, bool),
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
let (db, include_days) = *signals;
let ss = db.signals_by_id();
let mut seq = serializer.serialize_seq(Some(ss.len()))?;
for (_, s) in ss {
seq.serialize_element(&Signal::wrap(s, db, include_days))?;
}
seq.end()
}
/// Serializes signals as a list (rather than a map), optionally including the `days` field.
fn serialize_signal_types<S>(db: &db::LockedDatabase,
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
let ss = db.signal_types_by_uuid();
let mut seq = serializer.serialize_seq(Some(ss.len()))?;
for (u, t) in ss {
seq.serialize_element(&SignalType::wrap(*u, t))?;
}
seq.end()
}

View File

@@ -74,6 +74,7 @@ enum Path {
Request, // "/api/request"
InitSegment([u8; 20], bool), // "/api/init/<sha1>.mp4{.txt}"
Camera(Uuid), // "/api/cameras/<uuid>/"
Signals, // "/api/signals"
StreamRecordings(Uuid, db::StreamType), // "/api/cameras/<uuid>/<type>/recordings"
StreamViewMp4(Uuid, db::StreamType, bool), // "/api/cameras/<uuid>/<type>/view.mp4{.txt}"
StreamViewMp4Segment(Uuid, db::StreamType, bool), // "/api/cameras/<uuid>/<type>/view.m4s{.txt}"
@@ -94,9 +95,10 @@ impl Path {
return Path::TopLevel;
}
match path {
"/request" => return Path::Request,
"/login" => return Path::Login,
"/logout" => return Path::Logout,
"/request" => return Path::Request,
"/signals" => return Path::Signals,
_ => {},
};
if path.starts_with("/init/") {
@@ -251,6 +253,16 @@ struct ServiceInner {
type ResponseResult = Result<Response<Body>, Response<Body>>;
fn serve_json<T: serde::ser::Serialize>(req: &Request<hyper::Body>, out: &T) -> ResponseResult {
let (mut resp, writer) = http_serve::streaming_body(&req).build();
resp.headers_mut().insert(header::CONTENT_TYPE,
HeaderValue::from_static("application/json"));
if let Some(mut w) = writer {
serde_json::to_writer(&mut w, out).map_err(internal_server_err)?;
}
Ok(resp)
}
impl ServiceInner {
fn top_level(&self, req: &Request<::hyper::Body>, session: Option<json::Session>)
-> ResponseResult {
@@ -265,34 +277,21 @@ impl ServiceInner {
}
}
let (mut resp, writer) = http_serve::streaming_body(&req).build();
resp.headers_mut().insert(header::CONTENT_TYPE,
HeaderValue::from_static("application/json"));
if let Some(mut w) = writer {
let db = self.db.lock();
serde_json::to_writer(&mut w, &json::TopLevel {
time_zone_name: &self.time_zone_name,
cameras: (&db, days),
session,
}).map_err(internal_server_err)?;
}
Ok(resp)
let db = self.db.lock();
serve_json(req, &json::TopLevel {
time_zone_name: &self.time_zone_name,
cameras: (&db, days),
session,
signals: (&db, days),
signal_types: &db,
})
}
fn camera(&self, req: &Request<::hyper::Body>, uuid: Uuid) -> ResponseResult {
let (mut resp, writer) = http_serve::streaming_body(&req).build();
resp.headers_mut().insert(header::CONTENT_TYPE,
HeaderValue::from_static("application/json"));
if let Some(mut w) = writer {
let db = self.db.lock();
let camera = db.get_camera(uuid)
.ok_or_else(|| not_found(format!("no such camera {}", uuid)))?;
serde_json::to_writer(
&mut w,
&json::Camera::wrap(camera, &db, true).map_err(internal_server_err)?
).map_err(internal_server_err)?
};
Ok(resp)
let db = self.db.lock();
let camera = db.get_camera(uuid)
.ok_or_else(|| not_found(format!("no such camera {}", uuid)))?;
serve_json(req, &json::Camera::wrap(camera, &db, true).map_err(internal_server_err)?)
}
fn stream_recordings(&self, req: &Request<::hyper::Body>, uuid: Uuid, type_: db::StreamType)
@@ -351,13 +350,7 @@ impl ServiceInner {
Ok(())
}).map_err(internal_server_err)?;
}
let (mut resp, writer) = http_serve::streaming_body(&req).build();
resp.headers_mut().insert(header::CONTENT_TYPE,
HeaderValue::from_static("application/json"));
if let Some(mut w) = writer {
serde_json::to_writer(&mut w, &out).map_err(internal_server_err)?
};
Ok(resp)
serve_json(req, &out)
}
fn init_segment(&self, sha1: [u8; 20], debug: bool, req: &Request<::hyper::Body>)
@@ -636,6 +629,34 @@ impl ServiceInner {
Ok(res)
}
fn signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
let mut time = recording::Time(i64::min_value()) .. recording::Time(i64::max_value());
if let Some(q) = req.uri().query() {
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
let (key, value) = (key.borrow(), value.borrow());
match key {
"startTime90k" => {
time.start = recording::Time::parse(value)
.map_err(|_| bad_req("unparseable startTime90k"))?
},
"endTime90k" => {
time.end = recording::Time::parse(value)
.map_err(|_| bad_req("unparseable endTime90k"))?
},
_ => {},
}
}
}
let mut signals = json::Signals::default();
self.db.lock().list_changes_by_time(time, &mut |c: &db::signal::ListStateChangesRow| {
signals.times_90k.push(c.when.0);
signals.signal_ids.push(c.signal);
signals.states.push(c.state);
}).map_err(internal_server_err)?;
serve_json(req, &signals)
}
fn authenticated(&self, req: &Request<hyper::Body>) -> Result<Option<json::Session>, Error> {
if let Some(sid) = extract_sid(req) {
let authreq = self.authreq(req);
@@ -950,6 +971,7 @@ impl ::hyper::service::Service for Service {
let s = self.clone();
move |(req, b)| { s.0.logout(&req, b) }
})),
Path::Signals => wrap_r(true, self.0.signals(&req)),
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
}
}
@@ -1098,6 +1120,7 @@ mod tests {
Path::NotFound);
assert_eq!(Path::decode("/api/login"), Path::Login);
assert_eq!(Path::decode("/api/logout"), Path::Logout);
assert_eq!(Path::decode("/api/signals"), Path::Signals);
assert_eq!(Path::decode("/api/junk"), Path::NotFound);
}