//! The session bus side.
//!
//! `io.neiam.ApollosCrib` on the session bus:
//!
//! ```text
//! /io/neiam/ApollosCrib io.neiam.ApollosCrib1
//! org.freedesktop.DBus.ObjectManager
//! /io/neiam/ApollosCrib/query/gtfs_12 io.neiam.ApollosCrib1.Query
//! ```
//!
//! Everything is a property, and every change is a `PropertiesChanged`, since
//! that is what Plasma's QML `DBusProperties` binds to: a widget names a path
//! and its labels follow. Structured values are JSON strings, because a D-Bus
//! struct reaches QML as an opaque `QDBusArgument` and a string reaches it as
//! something `JSON.parse` can read.
use std::collections::HashMap;
use serde_json::Value;
use tokio::sync::mpsc;
use zbus::object_server::SignalEmitter;
use zbus::{fdo, interface};
use crate::source::Command;
pub const NAME: &str = "io.neiam.ApollosCrib";
pub const ROOT: &str = "/io/neiam/ApollosCrib";
/// A query's object path.
///
/// A path element allows only `[A-Za-z0-9_]`, and a key is `<type>-<id>`
/// with an id that may be anything a feed calls a stop. The type's dash
/// becomes `_`, so the common `gtfs-12` reads as `gtfs_12`; every other
/// character outside the alphabet becomes `_` and its UTF-8 bytes in hex,
/// so `gtfs-4:place-sstat` is `gtfs_4_3aplace_2dsstat` and two stops told
/// apart only by punctuation stay two objects.
pub fn query_path(key: &str) -> String {
let (kind, id) = key.split_once('-').unwrap_or((key, ""));
let escape = |part: &str| -> String {
let mut out = String::new();
for c in part.chars() {
if c.is_ascii_alphanumeric() {
out.push(c);
} else {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).bytes() {
out.push_str(&format!("_{b:02x}"));
}
}
}
out
};
format!("{ROOT}/query/{}_{}", escape(kind), escape(id))
}
/// Where a key came from, so a refresh of it goes to the right place.
#[derive(Debug, Clone)]
pub struct Origin {
pub source: String,
pub query_id: u32,
}
pub struct Root {
pub board: String,
pub points: String,
pub queries: Vec<String>,
pub sources: String,
pub connected: bool,
pub origins: HashMap<String, Origin>,
pub commands: HashMap<String, (bool, mpsc::Sender<Command>)>,
}
#[interface(name = "io.neiam.ApollosCrib1")]
impl Root {
/// Every query, keyed `<type>-<id>`: `{name, type, id, query_id, source,
/// updated, stale, data, meta, plus, points}`. Everything at once, for a
/// widget that would rather bind one property than one path per query.
#[zbus(property)]
fn board(&self) -> String {
self.board.clone()
}
/// Every query said in a few words, in key order: `[{key, name, type,
/// text, color?, stale}]`. For a wallpaper, a tooltip, a ticker.
#[zbus(property)]
fn points(&self) -> String {
self.points.clone()
}
/// The keys there are objects for.
#[zbus(property)]
fn queries(&self) -> Vec<String> {
self.queries.clone()
}
/// Each source and whether it is connected: `[{name, connected, error?}]`.
#[zbus(property)]
fn sources(&self) -> String {
self.sources.clone()
}
/// Whether any source is connected.
#[zbus(property)]
fn connected(&self) -> bool {
self.connected
}
/// Ask every source for fresh data.
async fn refresh(&self) {
for (_, tx) in self.commands.values() {
let _ = tx.send(Command::Refresh(None)).await;
}
}
/// Ask for one query afresh. A Keryx refreshes just that query; an
/// Ankyra has only whole boards to give.
async fn refresh_query(&self, key: &str) -> fdo::Result<()> {
let origin = self
.origins
.get(key)
.ok_or_else(|| fdo::Error::InvalidArgs(format!("no query {key}")))?;
let (keryx, tx) = self
.commands
.get(&origin.source)
.ok_or_else(|| fdo::Error::Failed(format!("{} is not configured", origin.source)))?;
let q = keryx.then_some(origin.query_id).filter(|q| *q != 0);
tx.send(Command::Refresh(q))
.await
.map_err(|e| fdo::Error::Failed(e.to_string()))
}
/// A query has new data. The properties say so too; this is for a
/// consumer that wants one signal to hang an animation off.
#[zbus(signal)]
pub async fn query_updated(emitter: &SignalEmitter<'_>, key: &str) -> zbus::Result<()>;
}
pub struct Query {
pub key: String,
pub kind: String,
pub id: String,
pub query_id: u32,
pub name: String,
pub source: String,
pub updated: i64,
pub stale: bool,
pub data: Value,
pub meta: Value,
pub plus: Value,
pub points: Vec<String>,
}
#[interface(name = "io.neiam.ApollosCrib1.Query")]
impl Query {
#[zbus(property)]
fn key(&self) -> String {
self.key.clone()
}
/// The source type: `gtfs`, `weather`, ...
#[zbus(property, name = "Type")]
fn kind(&self) -> String {
self.kind.clone()
}
/// Everything in the key after the type: `12`, or for a Plani broken out
/// by stop, `4:place-sstat`.
#[zbus(property)]
fn id(&self) -> String {
self.id.clone()
}
/// The id as a query id; 0 where it is not one.
#[zbus(property)]
fn query_id(&self) -> u32 {
self.query_id
}
/// The query's name in Apollo's Crib, or its key if it was sent bare.
#[zbus(property)]
fn name(&self) -> String {
self.name.clone()
}
/// `ankyra:<name>` or `keryx:<name>`.
#[zbus(property)]
fn source(&self) -> String {
self.source.clone()
}
/// When it last arrived, in seconds since the epoch.
#[zbus(property)]
fn updated(&self) -> i64 {
self.updated
}
/// Unheard from for longer than the configured `stale_after_secs`.
#[zbus(property)]
fn stale(&self) -> bool {
self.stale
}
/// The condensed reading as JSON, in the apollos-types shape for its type.
#[zbus(property)]
fn data(&self) -> String {
self.data.to_string()
}
/// The query's `meta`, as JSON.
#[zbus(property)]
fn meta(&self) -> String {
self.meta.to_string()
}
/// The Plus reading as JSON, `null` where there is none.
#[zbus(property)]
fn plus(&self) -> String {
self.plus.to_string()
}
/// The reading in a few words, a line each.
#[zbus(property)]
fn points(&self) -> Vec<String> {
self.points.clone()
}
}
/// Every property of a query, announced. One `PropertiesChanged` each:
/// zbus has no batch, and a reading rarely changes less than all of them.
pub async fn announce_query(q: &Query, e: &SignalEmitter<'_>) -> zbus::Result<()> {
q.name_changed(e).await?;
q.source_changed(e).await?;
q.updated_changed(e).await?;
q.stale_changed(e).await?;
q.data_changed(e).await?;
q.meta_changed(e).await?;
q.plus_changed(e).await?;
q.points_changed(e).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_key_becomes_a_legal_path() {
assert_eq!(query_path("gtfs-12"), "/io/neiam/ApollosCrib/query/gtfs_12");
assert_eq!(
query_path("gtfs-4:place-sstat"),
"/io/neiam/ApollosCrib/query/gtfs_4_3aplace_2dsstat"
);
assert_ne!(query_path("gtfs-4:a-b"), query_path("gtfs-4:a_b"));
}
}
neiam
/apollos-dbus
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
7.4 KiB · text
History
4f1f7f4