Compare commits
4 Commits
4156a10685
...
17d4f2a143
Author | SHA1 | Date |
---|---|---|
|
17d4f2a143 | |
|
faad1615ed | |
|
56ffd60d5f | |
|
e53c147dfa |
|
@ -0,0 +1,59 @@
|
||||||
|
// vim: set sw=4 ts=4 sts=4 et :
|
||||||
|
pipeline {
|
||||||
|
agent none
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Build') {
|
||||||
|
parallel {
|
||||||
|
/*
|
||||||
|
stage('Build x86_64') {
|
||||||
|
agent {
|
||||||
|
kubernetes {
|
||||||
|
containerTemplate {
|
||||||
|
name 'build'
|
||||||
|
image 'git.pyrocufflink.net/containerimages/build/browserhud'
|
||||||
|
command 'cat'
|
||||||
|
ttyEnabled true
|
||||||
|
}
|
||||||
|
nodeSelector 'kubernetes.io/arch=amd64'
|
||||||
|
defaultContainer 'build'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh '. ci/build.sh'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
archiveArtifacts 'x86_64/mqttmarionette'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
stage('Build aarch64') {
|
||||||
|
agent {
|
||||||
|
kubernetes {
|
||||||
|
containerTemplate {
|
||||||
|
name 'build'
|
||||||
|
image 'git.pyrocufflink.net/containerimages/build/browserhud'
|
||||||
|
command 'cat'
|
||||||
|
ttyEnabled true
|
||||||
|
}
|
||||||
|
nodeSelector 'kubernetes.io/arch=arm64'
|
||||||
|
defaultContainer 'build'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh '. ci/build.sh'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
archiveArtifacts 'aarch64/mqttmarionette'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
ln -sr target/release $(uname -m)
|
|
@ -41,7 +41,7 @@ pub struct Capabilities {
|
||||||
pub webdriver_click: bool,
|
pub webdriver_click: bool,
|
||||||
pub page_load_strategy: String,
|
pub page_load_strategy: String,
|
||||||
pub platform_name: String,
|
pub platform_name: String,
|
||||||
pub platform_version: String,
|
pub platform_version: Option<String>,
|
||||||
// proxy:
|
// proxy:
|
||||||
pub set_window_rect: bool,
|
pub set_window_rect: bool,
|
||||||
pub timeouts: Timeouts,
|
pub timeouts: Timeouts,
|
||||||
|
@ -155,4 +155,6 @@ pub enum Command {
|
||||||
FullscreenWindow,
|
FullscreenWindow,
|
||||||
#[serde(rename = "WebDriver:SetWindowRect")]
|
#[serde(rename = "WebDriver:SetWindowRect")]
|
||||||
SetWindowRect(WindowRect),
|
SetWindowRect(WindowRect),
|
||||||
|
#[serde(rename = "WebDriver:Refresh")]
|
||||||
|
Refresh,
|
||||||
}
|
}
|
||||||
|
|
|
@ -271,6 +271,13 @@ impl Marionette {
|
||||||
Ok(res.handle)
|
Ok(res.handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn refresh(&mut self) -> Result<(), CommandError> {
|
||||||
|
let res: serde_json::Value =
|
||||||
|
self.conn.send_message(Command::Refresh).await?.unwrap();
|
||||||
|
debug!("Reeived message: {:?}", res);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn set_window_rect(
|
pub async fn set_window_rect(
|
||||||
&mut self,
|
&mut self,
|
||||||
x: Option<i32>,
|
x: Option<i32>,
|
||||||
|
|
16
src/mqtt.rs
16
src/mqtt.rs
|
@ -15,16 +15,26 @@ use tracing::{debug, error, info, trace, warn};
|
||||||
use crate::config::Configuration;
|
use crate::config::Configuration;
|
||||||
use crate::hass::{self, HassConfig};
|
use crate::hass::{self, HassConfig};
|
||||||
|
|
||||||
|
/// Callback methods invoked to hanle incoming MQTT messages
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait MessageHandler {
|
pub trait MessageHandler {
|
||||||
|
/// Navigate to the specified URL
|
||||||
async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message);
|
async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message);
|
||||||
|
/// Turn on or off the display(s)
|
||||||
async fn power(&mut self, publisher: &MqttPublisher, msg: &Message);
|
async fn power(&mut self, publisher: &MqttPublisher, msg: &Message);
|
||||||
|
/// Refresh the current page
|
||||||
|
async fn refresh(&mut self, publisher: &MqttPublisher, msg: &Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MQTT message types
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum MessageType {
|
pub enum MessageType {
|
||||||
|
/// Navigate to the specified URL
|
||||||
Navigate,
|
Navigate,
|
||||||
|
/// Turn on or off the display(s)
|
||||||
PowerState,
|
PowerState,
|
||||||
|
/// Refresh the current page
|
||||||
|
Refresh,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MqttClient<'a> {
|
pub struct MqttClient<'a> {
|
||||||
|
@ -72,10 +82,13 @@ impl<'a> MqttClient<'a> {
|
||||||
let client = self.client.lock().await;
|
let client = self.client.lock().await;
|
||||||
let prefix = &self.config.mqtt.topic_prefix;
|
let prefix = &self.config.mqtt.topic_prefix;
|
||||||
let t_nav = format!("{}/+/navigate", prefix);
|
let t_nav = format!("{}/+/navigate", prefix);
|
||||||
|
let t_ref = format!("{}/+/refresh", prefix);
|
||||||
let t_power = format!("{}/power", prefix);
|
let t_power = format!("{}/power", prefix);
|
||||||
client.subscribe(&t_nav, 0).await?;
|
client.subscribe(&t_nav, 0).await?;
|
||||||
|
client.subscribe(&t_ref, 0).await?;
|
||||||
client.subscribe(&t_power, 0).await?;
|
client.subscribe(&t_power, 0).await?;
|
||||||
self.topics.insert(t_nav, MessageType::Navigate);
|
self.topics.insert(t_nav, MessageType::Navigate);
|
||||||
|
self.topics.insert(t_ref, MessageType::Refresh);
|
||||||
self.topics.insert(t_power, MessageType::PowerState);
|
self.topics.insert(t_power, MessageType::PowerState);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -125,6 +138,9 @@ impl<'a> MqttClient<'a> {
|
||||||
MessageType::PowerState => {
|
MessageType::PowerState => {
|
||||||
handler.power(&publisher, &msg).await;
|
handler.power(&publisher, &msg).await;
|
||||||
}
|
}
|
||||||
|
MessageType::Refresh => {
|
||||||
|
handler.refresh(&publisher, &msg).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
112
src/session.rs
112
src/session.rs
|
@ -181,38 +181,36 @@ pub struct MessageHandler<'a> {
|
||||||
windows: HashMap<String, String>,
|
windows: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
impl<'a> MessageHandler<'a> {
|
||||||
impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> {
|
/// Switch to the window shown on the specified screen
|
||||||
async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message) {
|
///
|
||||||
let url = msg.payload_str();
|
/// This method switches to the active window of the Marionette
|
||||||
let parts: Vec<&str> = msg.topic().split('/').rev().collect();
|
/// session to the one displayed on the specified screen. It must
|
||||||
let screen = match parts.get(1) {
|
/// be called before performing any screen-specific operation,
|
||||||
Some(&"") | None => {
|
/// such as navigating to a URL or refreshing the page.
|
||||||
warn!("Invalid navigate request: no screen");
|
async fn switch_window(&mut self, screen: &str) -> Result<(), String> {
|
||||||
return;
|
let Some(window) = self.windows.get(screen) else {
|
||||||
}
|
return Err(format!("Unknown screen {}", screen));
|
||||||
Some(s) => s,
|
|
||||||
};
|
};
|
||||||
if let Some(window) = self.windows.get(*screen) {
|
|
||||||
debug!("Switching to window {}", window);
|
debug!("Switching to window {}", window);
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
self.marionette.switch_to_window(window.into(), false).await
|
self.marionette.switch_to_window(window.into(), false).await
|
||||||
{
|
{
|
||||||
error!(
|
return Err(e.to_string());
|
||||||
"Failed to switch to window on screen {}: {}",
|
|
||||||
screen, e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
Ok(())
|
||||||
error!("Invalid navigate request: unknown screen {}", screen);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
debug!("Handling navigate request: {}", url);
|
|
||||||
info!("Navigate screen {} to {}", screen, url);
|
|
||||||
if let Err(e) = self.marionette.navigate(url.to_string()).await {
|
|
||||||
error!("Failed to navigate: {}", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Publish the current page info via MQTT
|
||||||
|
///
|
||||||
|
/// This method retrieves the current title and URL of the page
|
||||||
|
/// displayed in the window visible on the specified screen, then
|
||||||
|
/// publishes the values to their respective MQTT topics.
|
||||||
|
async fn publish_info(
|
||||||
|
&mut self,
|
||||||
|
screen: &str,
|
||||||
|
publisher: &MqttPublisher<'_>,
|
||||||
|
) {
|
||||||
match self.marionette.get_current_url().await {
|
match self.marionette.get_current_url().await {
|
||||||
Ok(u) => {
|
Ok(u) => {
|
||||||
if let Err(e) = publisher.publish_url(screen, &u).await {
|
if let Err(e) = publisher.publish_url(screen, &u).await {
|
||||||
|
@ -230,6 +228,27 @@ impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> {
|
||||||
Err(e) => error!("Error getting title: {}", e),
|
Err(e) => error!("Error getting title: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> {
|
||||||
|
async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message) {
|
||||||
|
let url = msg.payload_str();
|
||||||
|
let Some(screen) = screen_from_topic(msg.topic()) else {
|
||||||
|
warn!("Invalid navigate request: no screen");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Err(e) = self.switch_window(screen).await {
|
||||||
|
error!("Failed to switch to window on screen {}: {}", screen, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debug!("Handling navigate request: {}", url);
|
||||||
|
info!("Navigate screen {} to {}", screen, url);
|
||||||
|
if let Err(e) = self.marionette.navigate(url.to_string()).await {
|
||||||
|
error!("Failed to navigate: {}", e);
|
||||||
|
}
|
||||||
|
self.publish_info(screen, publisher).await;
|
||||||
|
}
|
||||||
|
|
||||||
async fn power(&mut self, publisher: &MqttPublisher, msg: &Message) {
|
async fn power(&mut self, publisher: &MqttPublisher, msg: &Message) {
|
||||||
match msg.payload_str().as_ref() {
|
match msg.payload_str().as_ref() {
|
||||||
|
@ -243,6 +262,30 @@ impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> {
|
||||||
error!("Failed to publish power state: {}", e);
|
error!("Failed to publish power state: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refresh the page currently displayd on the specified screen
|
||||||
|
///
|
||||||
|
/// This method is called by the MQTT receiver when a
|
||||||
|
/// [`crate::mqtt::MessageType::Refresh`] message is received from
|
||||||
|
/// the broker. It calls `WebDriver:Refresh` for the window
|
||||||
|
/// displayed on the screen specified in the message topic, then
|
||||||
|
/// publishes the page URL and title, after the page load completes.
|
||||||
|
async fn refresh(&mut self, publisher: &MqttPublisher, msg: &Message) {
|
||||||
|
let Some(screen) = screen_from_topic(msg.topic()) else {
|
||||||
|
warn!("Invalid refresh request: no screen");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Err(e) = self.switch_window(screen).await {
|
||||||
|
error!("Failed to switch to window on screen {}: {}", screen, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info!("Refreshing screen: {}", screen);
|
||||||
|
if let Err(e) = self.marionette.refresh().await {
|
||||||
|
error!("Failed to refresh page: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.publish_info(screen, publisher).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@ -272,10 +315,7 @@ fn turn_screen_on() {
|
||||||
error!("Failed to turn on display \"{}\"", Display::name());
|
error!("Failed to turn on display \"{}\"", Display::name());
|
||||||
}
|
}
|
||||||
if !dpms::disable(&display) {
|
if !dpms::disable(&display) {
|
||||||
error!(
|
error!("Failed to disable DPMS on display \"{}\"", Display::name());
|
||||||
"Failed to disable DPMS on display \"{}\"",
|
|
||||||
Display::name()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,10 +329,7 @@ fn turn_screen_off() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if !dpms::enable(&display) {
|
if !dpms::enable(&display) {
|
||||||
error!(
|
error!("Failed to enable DPMS on display \"{}\"", Display::name());
|
||||||
"Failed to enable DPMS on display \"{}\"",
|
|
||||||
Display::name()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if !dpms::force_level(&display, dpms::DpmsPowerLevel::Off) {
|
if !dpms::force_level(&display, dpms::DpmsPowerLevel::Off) {
|
||||||
error!("Failed to turn off display \"{}\"", Display::name());
|
error!("Failed to turn off display \"{}\"", Display::name());
|
||||||
|
@ -316,3 +353,12 @@ fn is_screen_on() -> bool {
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract the screen name from the topic
|
||||||
|
///
|
||||||
|
/// The screen name is the penultimate segment of the topic path, e.g.
|
||||||
|
/// .../HDMI-1/navigate (HDMI-1) or .../eDP-1/refresh (eDP-1).
|
||||||
|
fn screen_from_topic(topic: &str) -> Option<&str> {
|
||||||
|
let topic = topic.get(..topic.rfind('/')?)?;
|
||||||
|
topic.get(topic.rfind('/')? + 1..)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue