mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-11 23:13:23 -05:00
refine timestamps in json signals api
* API change: in update signals, allow setting a start time relative to now. This is an accuracy improvement in the case where the client has been retrying an initial request for a while. Kind of an obscure corner case but easy enough to address. And use a more convenient enum representation. * in update signals, choose `now` before acquiring the database lock. If lock acquisition takes a long time, this more accurately reflects the time the caller intended. * in general, make Time and Duration (de)serializable and use them in json types. This makes the types more self-describing, with better debug printing on both the server side and on the client library (in moonfire-playground). To make this work, base has to import serde which initially seemed like poor layering to me, but serde seems to be imported in some pretty foundational Rust crates for this reason. I'll go with it.
This commit is contained in:
parent
5da5494dfb
commit
1e314e09d0
@ -658,13 +658,12 @@ make. It should be a dict with these attributes:
|
||||
|
||||
* `signalIds`: a list of signal ids to change. Must be sorted.
|
||||
* `states`: a list (one per `signalIds` entry) of states to set.
|
||||
* `startTime90k`: (optional) The start of the observation in 90 kHz units
|
||||
since 1970-01-01 00:00:00 UTC; commonly taken from an earlier response. If
|
||||
absent, assumed to be now.
|
||||
* `endBase`: if `epoch`, `relEndTime90k` is relative to 1970-01-01 00:00:00
|
||||
UTC. If `now`, epoch is relative to the current time.
|
||||
* `relEndTime90k` (optional): The end of the observation, relative to the
|
||||
specified base. Note this time is allowed to be in the future.
|
||||
* `start`: the starting time of the change, as a dict of the form
|
||||
`{'base': 'epoch', 'rel90k': t}` or `{'base': 'now', 'rel90k': t}`. In
|
||||
the `epoch` form, `rel90k` is 90 kHz units since 1970-01-01 00:00:00 UTC.
|
||||
In the `now` form, `rel90k` is relative to current time and may be
|
||||
negative.
|
||||
* `end`: the ending time of the change, in the same form as `start`.
|
||||
|
||||
The response will be an `application/json` body dict with the following
|
||||
attributes:
|
||||
@ -687,8 +686,8 @@ Request:
|
||||
{
|
||||
"signalIds": [1],
|
||||
"states": [2],
|
||||
"endBase": "now",
|
||||
"relEndTime90k": 5400000
|
||||
"start": {"base": "now", "rel90k": 0},
|
||||
"end": {"base": "now", "rel90k": 5400000}
|
||||
}
|
||||
```
|
||||
|
||||
@ -711,8 +710,8 @@ Request:
|
||||
{
|
||||
"signalIds": [1],
|
||||
"states": [2],
|
||||
"endBase": "now",
|
||||
"relEndTime90k": 5400000
|
||||
"start": {"base": "epoch", "rel90k": 140067468000000},
|
||||
"end": {"base": "now", "rel90k": 5400000}
|
||||
}
|
||||
```
|
||||
|
||||
@ -735,8 +734,8 @@ Request:
|
||||
{
|
||||
"signalIds": [1],
|
||||
"states": [2],
|
||||
"endBase": "now",
|
||||
"relEndTime90k": 5400000
|
||||
"start": {"base": "now", "rel90k": 0},
|
||||
"end": {"base": "now", "rel90k": 5400000}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
2
server/Cargo.lock
generated
2
server/Cargo.lock
generated
@ -1165,6 +1165,8 @@ dependencies = [
|
||||
"log",
|
||||
"nom",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
]
|
||||
|
||||
|
@ -19,4 +19,6 @@ libc = "0.2"
|
||||
log = "0.4"
|
||||
parking_lot = { version = "0.11.1", features = [] }
|
||||
nom = "6.0.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
time = "0.1"
|
||||
|
@ -9,6 +9,7 @@ use nom::branch::alt;
|
||||
use nom::bytes::complete::{tag, take_while_m_n};
|
||||
use nom::combinator::{map, map_res, opt};
|
||||
use nom::sequence::{preceded, tuple};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::ops;
|
||||
use std::str::FromStr;
|
||||
@ -19,7 +20,7 @@ type IResult<'a, I, O> = nom::IResult<I, O, nom::error::VerboseError<&'a str>>;
|
||||
pub const TIME_UNITS_PER_SEC: i64 = 90_000;
|
||||
|
||||
/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
|
||||
#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[derive(Clone, Copy, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct Time(pub i64);
|
||||
|
||||
/// Returns a parser for a `len`-digit non-negative number which fits into an i32.
|
||||
@ -221,7 +222,7 @@ impl fmt::Display for Time {
|
||||
/// A duration specified in 1/90,000ths of a second.
|
||||
/// Durations are typically non-negative, but a `moonfire_db::db::CameraDayValue::duration` may be
|
||||
/// negative.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[derive(Clone, Copy, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct Duration(pub i64);
|
||||
|
||||
impl Duration {
|
||||
@ -230,6 +231,13 @@ impl Duration {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Duration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// Write both the raw and display forms.
|
||||
write!(f, "{} /* {} */", self.0, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Duration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut seconds = self.0 / TIME_UNITS_PER_SEC;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
||||
|
||||
use base::time::{Duration, Time};
|
||||
use db::auth::SessionHash;
|
||||
use failure::{format_err, Error};
|
||||
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
|
||||
@ -77,9 +78,9 @@ pub struct CameraConfig<'a> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Stream<'a> {
|
||||
pub retain_bytes: i64,
|
||||
pub min_start_time_90k: Option<i64>,
|
||||
pub max_end_time_90k: Option<i64>,
|
||||
pub total_duration_90k: i64,
|
||||
pub min_start_time_90k: Option<Time>,
|
||||
pub max_end_time_90k: Option<Time>,
|
||||
pub total_duration_90k: Duration,
|
||||
pub total_sample_file_bytes: i64,
|
||||
pub fs_bytes: i64,
|
||||
|
||||
@ -113,10 +114,10 @@ pub struct Signal<'a> {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PostSignalsEndBase {
|
||||
Epoch,
|
||||
Now,
|
||||
#[serde(tag = "base", content = "rel90k", rename_all = "camelCase")]
|
||||
pub enum PostSignalsTimeBase {
|
||||
Epoch(Time),
|
||||
Now(Duration),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -137,21 +138,20 @@ pub struct LogoutRequest<'a> {
|
||||
pub struct PostSignalsRequest {
|
||||
pub signal_ids: Vec<u32>,
|
||||
pub states: Vec<u16>,
|
||||
pub start_time_90k: Option<i64>,
|
||||
pub end_base: PostSignalsEndBase,
|
||||
pub rel_end_time_90k: Option<i64>,
|
||||
pub start: PostSignalsTimeBase,
|
||||
pub end: PostSignalsTimeBase,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PostSignalsResponse {
|
||||
pub time_90k: i64,
|
||||
pub time_90k: Time,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Signals {
|
||||
pub times_90k: Vec<i64>,
|
||||
pub times_90k: Vec<Time>,
|
||||
pub signal_ids: Vec<u32>,
|
||||
pub states: Vec<u16>,
|
||||
}
|
||||
@ -238,9 +238,9 @@ impl<'a> Stream<'a> {
|
||||
.ok_or_else(|| format_err!("missing stream {}", id))?;
|
||||
Ok(Some(Stream {
|
||||
retain_bytes: s.retain_bytes,
|
||||
min_start_time_90k: s.range.as_ref().map(|r| r.start.0),
|
||||
max_end_time_90k: s.range.as_ref().map(|r| r.end.0),
|
||||
total_duration_90k: s.duration.0,
|
||||
min_start_time_90k: s.range.as_ref().map(|r| r.start),
|
||||
max_end_time_90k: s.range.as_ref().map(|r| r.end),
|
||||
total_duration_90k: s.duration,
|
||||
total_sample_file_bytes: s.sample_file_bytes,
|
||||
fs_bytes: s.fs_bytes,
|
||||
days: if include_days { Some(s.days()) } else { None },
|
||||
@ -269,9 +269,9 @@ impl<'a> Stream<'a> {
|
||||
map.serialize_key(k.as_ref())?;
|
||||
let bounds = k.bounds();
|
||||
map.serialize_value(&StreamDayValue {
|
||||
start_time_90k: bounds.start.0,
|
||||
end_time_90k: bounds.end.0,
|
||||
total_duration_90k: v.duration.0,
|
||||
start_time_90k: bounds.start,
|
||||
end_time_90k: bounds.end,
|
||||
total_duration_90k: v.duration,
|
||||
})?;
|
||||
}
|
||||
map.end()
|
||||
@ -328,8 +328,8 @@ impl<'a> Signal<'a> {
|
||||
map.serialize_key(k.as_ref())?;
|
||||
let bounds = k.bounds();
|
||||
map.serialize_value(&SignalDayValue {
|
||||
start_time_90k: bounds.start.0,
|
||||
end_time_90k: bounds.end.0,
|
||||
start_time_90k: bounds.start,
|
||||
end_time_90k: bounds.end,
|
||||
states: &v.states[..],
|
||||
})?;
|
||||
}
|
||||
@ -371,16 +371,16 @@ impl<'a> SignalTypeState<'a> {
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StreamDayValue {
|
||||
pub start_time_90k: i64,
|
||||
pub end_time_90k: i64,
|
||||
pub total_duration_90k: i64,
|
||||
pub start_time_90k: Time,
|
||||
pub end_time_90k: Time,
|
||||
pub total_duration_90k: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SignalDayValue<'a> {
|
||||
pub start_time_90k: i64,
|
||||
pub end_time_90k: i64,
|
||||
pub start_time_90k: Time,
|
||||
pub end_time_90k: Time,
|
||||
pub states: &'a [u64],
|
||||
}
|
||||
|
||||
|
@ -1182,22 +1182,19 @@ impl Service {
|
||||
let r = extract_json_body(&mut req).await?;
|
||||
let r: json::PostSignalsRequest =
|
||||
serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
|
||||
let mut l = self.db.lock();
|
||||
let now = recording::Time::new(self.db.clocks().realtime());
|
||||
let start = r.start_time_90k.map(recording::Time).unwrap_or(now);
|
||||
let end = match r.end_base {
|
||||
json::PostSignalsEndBase::Epoch => {
|
||||
recording::Time(r.rel_end_time_90k.ok_or_else(|| {
|
||||
bad_req("must specify rel_end_time_90k when end_base is epoch")
|
||||
})?)
|
||||
}
|
||||
json::PostSignalsEndBase::Now => {
|
||||
now + recording::Duration(r.rel_end_time_90k.unwrap_or(0))
|
||||
}
|
||||
let mut l = self.db.lock();
|
||||
let start = match r.start {
|
||||
json::PostSignalsTimeBase::Epoch(t) => t,
|
||||
json::PostSignalsTimeBase::Now(d) => now + d,
|
||||
};
|
||||
let end = match r.end {
|
||||
json::PostSignalsTimeBase::Epoch(t) => t,
|
||||
json::PostSignalsTimeBase::Now(d) => now + d,
|
||||
};
|
||||
l.update_signals(start..end, &r.signal_ids, &r.states)
|
||||
.map_err(from_base_error)?;
|
||||
serve_json(&req, &json::PostSignalsResponse { time_90k: now.0 })
|
||||
serve_json(&req, &json::PostSignalsResponse { time_90k: now })
|
||||
}
|
||||
|
||||
fn get_signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
|
||||
@ -1223,7 +1220,7 @@ impl Service {
|
||||
self.db
|
||||
.lock()
|
||||
.list_changes_by_time(time, &mut |c: &db::signal::ListStateChangesRow| {
|
||||
signals.times_90k.push(c.when.0);
|
||||
signals.times_90k.push(c.when);
|
||||
signals.signal_ids.push(c.signal);
|
||||
signals.states.push(c.state);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user