v2
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
*.old
|
||||
.env
|
||||
5798
Cargo.lock
generated
Normal file
5798
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["dashboard","trekstor"]
|
||||
default-members = ["dashboard"]
|
||||
|
||||
[workspace.dependencies]
|
||||
slint = { version = "1.17.1", default-features = false, features = ["std", "compat-1-2", "libm", "renderer-software", "unsafe-single-threaded"] }
|
||||
8
build
Executable file
8
build
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
cargo b -r
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
scp target/release/dashboard trekstor:~/dashboard
|
||||
ssh -t trekstor -- "systemctl --user restart dashboard"
|
||||
1
dashboard/.gitignore
vendored
Normal file
1
dashboard/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
20
dashboard/Cargo.toml
Normal file
20
dashboard/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "dashboard"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
trekstor.path = "../trekstor"
|
||||
slint.workspace = true
|
||||
hass-rs = "0.5.0"
|
||||
serde_json = "1.0.151"
|
||||
tokio = { version = "1.53.1", features = ["full"] }
|
||||
dotenvy = "0.15.7"
|
||||
url = "2.5.8"
|
||||
chrono = "0.4.45"
|
||||
|
||||
[features]
|
||||
dev = ["slint/backend-winit-x11"]
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.17.1"
|
||||
9
dashboard/build.rs
Normal file
9
dashboard/build.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
fn main() {
|
||||
slint_build::compile_with_config(
|
||||
"ui/main.slint",
|
||||
slint_build::CompilerConfiguration::new()
|
||||
.with_style("material-dark".into())
|
||||
.embed_resources(slint_build::EmbedResourcesKind::EmbedFiles),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
78
dashboard/src/ha_ext.rs
Normal file
78
dashboard/src/ha_ext.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use crate::*;
|
||||
use hass_rs::HassEntity;
|
||||
use slint::ToSharedString;
|
||||
|
||||
pub trait HassEntityExt {
|
||||
fn is_on(&self) -> bool;
|
||||
fn available(&self) -> bool;
|
||||
fn friendly_name(&self) -> &str;
|
||||
fn unit_of_measurement(&self) -> &str;
|
||||
}
|
||||
|
||||
impl HassEntityExt for HassEntity {
|
||||
fn is_on(&self) -> bool {
|
||||
self.state == "on"
|
||||
}
|
||||
|
||||
fn available(&self) -> bool {
|
||||
self.state != "unavailable"
|
||||
}
|
||||
|
||||
fn friendly_name(&self) -> &str {
|
||||
self.attributes
|
||||
.get("friendly_name")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or(&self.entity_id)
|
||||
}
|
||||
|
||||
fn unit_of_measurement(&self) -> &str {
|
||||
self.attributes
|
||||
.get("unit_of_measurement")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&HassEntity> for LightData {
|
||||
fn from(value: &HassEntity) -> Self {
|
||||
Self {
|
||||
id: value.entity_id.clone().into(),
|
||||
name: value.friendly_name().into(),
|
||||
available: value.available(),
|
||||
on: value.is_on(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&HassEntity> for ThermometerData {
|
||||
fn from(value: &HassEntity) -> Self {
|
||||
let v: f32 = value.state.parse().unwrap_or_default();
|
||||
|
||||
Self {
|
||||
id: value.entity_id.clone().into(),
|
||||
name: value.friendly_name().into(),
|
||||
value: format!("{:.1}", v).into(),
|
||||
unit: value.unit_of_measurement().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&HassEntity> for DateTimeData {
|
||||
fn from(value: &HassEntity) -> Self {
|
||||
if let Ok(datetime) =
|
||||
chrono::NaiveDateTime::parse_from_str(&value.state, "%Y-%m-%dT%H:%M:%S")
|
||||
{
|
||||
return Self {
|
||||
id: value.entity_id.clone().into(),
|
||||
date: datetime.format("%a %m %b").to_shared_string(),
|
||||
time: datetime.format("%H:%M").to_shared_string(),
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
id: value.entity_id.clone().into(),
|
||||
date: "0-0-0000".into(),
|
||||
time: "00:00".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
208
dashboard/src/main.rs
Normal file
208
dashboard/src/main.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
mod ha_ext;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use dotenvy::var;
|
||||
use hass_rs::{HassClient, HassEntity};
|
||||
use serde_json::{Value, json};
|
||||
use slint::{ModelRc, VecModel, Weak};
|
||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
#[derive(Clone)]
|
||||
enum C2sMessage {
|
||||
CallService(String, String, Option<Value>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum S2cMessage {
|
||||
EntityUpdated,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
slint::platform::set_platform(Box::new(trekstor::Trekstor::new())).expect("set platform");
|
||||
|
||||
let app = AppWindow::new()?;
|
||||
|
||||
let state = State::default();
|
||||
let (c2s_tx, c2s_rx) = mpsc::unbounded_channel::<C2sMessage>();
|
||||
let (s2c_tx, mut s2c_rx) = mpsc::unbounded_channel::<S2cMessage>();
|
||||
|
||||
hass(s2c_tx, c2s_rx, state.clone()).await?;
|
||||
let app_weak = app.as_weak();
|
||||
apply_state(&app_weak, state.clone());
|
||||
|
||||
tokio::spawn({
|
||||
let app_weak = app.as_weak();
|
||||
let state = state.clone();
|
||||
async move {
|
||||
loop {
|
||||
if let Some(S2cMessage::EntityUpdated) = s2c_rx.recv().await {
|
||||
apply_state(&app_weak, state.clone());
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.on_toggle_light({
|
||||
let state = state.clone();
|
||||
move |entity_id| {
|
||||
let id = entity_id.to_string();
|
||||
|
||||
let payload = Some(json!({
|
||||
"entity_id": id
|
||||
}));
|
||||
|
||||
if let Some(entity) = state.get(&id) {
|
||||
let action = if entity.state == "on" {
|
||||
"turn_off"
|
||||
} else {
|
||||
"turn_on"
|
||||
};
|
||||
c2s_tx
|
||||
.send(C2sMessage::CallService(
|
||||
"light".into(),
|
||||
action.into(),
|
||||
payload.clone(),
|
||||
))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.run()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_state(app: &Weak<AppWindow>, state: State) {
|
||||
app.upgrade_in_event_loop(move |app| {
|
||||
// Lights
|
||||
let lights = state
|
||||
.domain("light")
|
||||
.iter()
|
||||
.filter_map(|light| {
|
||||
if light.entity_id.ends_with("screen")
|
||||
|| light.entity_id.find(|c: char| c.is_numeric()).is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(light.into())
|
||||
})
|
||||
.collect::<Vec<LightData>>();
|
||||
app.set_lights(ModelRc::new(VecModel::from(lights)));
|
||||
|
||||
// Thermometers
|
||||
let badges = state
|
||||
.domain("sensor")
|
||||
.iter()
|
||||
.filter_map(|sensor| {
|
||||
if sensor.entity_id.ends_with("thermometer_temperature") {
|
||||
return Some(sensor.into());
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect::<Vec<ThermometerData>>();
|
||||
app.set_badges(ModelRc::new(VecModel::from(badges)));
|
||||
|
||||
// Clock
|
||||
if let Some(date_time_iso) = state.get("sensor.date_time_iso").as_ref() {
|
||||
app.set_date_time(date_time_iso.into());
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct State(Arc<RwLock<Vec<HassEntity>>>);
|
||||
|
||||
impl State {
|
||||
pub fn update(&self, entity: &HassEntity) {
|
||||
let mut entities = self.0.write().unwrap();
|
||||
|
||||
if let Some(existing) = entities
|
||||
.iter_mut()
|
||||
.find(|e| e.entity_id == entity.entity_id)
|
||||
{
|
||||
*existing = entity.clone();
|
||||
} else {
|
||||
entities.push(entity.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn domain(&self, domain: &str) -> Vec<HassEntity> {
|
||||
let entities = self.0.read().unwrap();
|
||||
|
||||
entities
|
||||
.iter()
|
||||
.filter(|e| e.entity_id.starts_with(domain))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn get(&self, entity_id: &str) -> Option<HassEntity> {
|
||||
let entities = self.0.read().unwrap();
|
||||
|
||||
entities.iter().find(|e| e.entity_id == entity_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
async fn hass(
|
||||
s2c_tx: UnboundedSender<S2cMessage>,
|
||||
mut c2s_rx: UnboundedReceiver<C2sMessage>,
|
||||
state: State,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Ok(url) = var("HASS_URL") else {
|
||||
return Err("NO HASS_URL SET".into());
|
||||
};
|
||||
|
||||
let Ok(hass_token) = var("HASS_TOKEN") else {
|
||||
return Err("NO HASS_TOKEN SET".into());
|
||||
};
|
||||
|
||||
let Ok(url) = url::Url::parse(&url) else {
|
||||
return Err("INVALID HASS_URL".into());
|
||||
};
|
||||
|
||||
let url = format!("ws://{}/api/websocket", url.authority());
|
||||
|
||||
let mut client = HassClient::new(&url).await?;
|
||||
client.auth_with_longlivedtoken(&hass_token).await?;
|
||||
|
||||
let states = client.get_states().await?;
|
||||
|
||||
states.iter().for_each(|entity| {
|
||||
state.update(entity);
|
||||
});
|
||||
|
||||
let mut event_receiver = client.subscribe_event("state_changed").await?;
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = event_receiver.recv().await {
|
||||
if let Some(entity) = &message.event.data.new_state {
|
||||
state.update(entity);
|
||||
s2c_tx.send(S2cMessage::EntityUpdated).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if let Some(C2sMessage::CallService(domain, service, service_data)) =
|
||||
c2s_rx.recv().await
|
||||
{
|
||||
client
|
||||
.call_service(domain, service, service_data)
|
||||
.await
|
||||
.unwrap();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
201
dashboard/ui/main.slint
Normal file
201
dashboard/ui/main.slint
Normal file
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
Palette,
|
||||
StyleMetrics,
|
||||
Button,
|
||||
ScrollView,
|
||||
VerticalBox,
|
||||
HorizontalBox,
|
||||
StandardTableView,
|
||||
} from "std-widgets.slint";
|
||||
|
||||
export struct LightData {
|
||||
id: string,
|
||||
name: string,
|
||||
on: bool,
|
||||
available: bool,
|
||||
}
|
||||
|
||||
export struct ThermometerData {
|
||||
id: string,
|
||||
name: string,
|
||||
value: string,
|
||||
unit: string,
|
||||
}
|
||||
|
||||
export struct DateTimeData {
|
||||
id: string,
|
||||
date: string,
|
||||
time: string,
|
||||
}
|
||||
|
||||
component Clock {
|
||||
in property <DateTimeData> date-time;
|
||||
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
padding-left: 5px;
|
||||
padding-right: StyleMetrics.layout-padding;
|
||||
|
||||
Text {
|
||||
text: date-time.date;
|
||||
height: 16px;
|
||||
font-weight: 100;
|
||||
horizontal-alignment: right;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: date-time.time;
|
||||
font-size: 19px;
|
||||
height: 19px;
|
||||
font-weight: 500;
|
||||
horizontal-alignment: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Thermometer inherits Rectangle {
|
||||
in property <ThermometerData> badge;
|
||||
border-radius: 16px + 19px;
|
||||
border-width: 2px;
|
||||
border-color: Palette.alternate-background;
|
||||
background: Palette.alternate-background;
|
||||
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
padding-left: parent.border-radius / 2;
|
||||
padding-right: parent.border-radius / 2;
|
||||
|
||||
Text {
|
||||
text: badge.name;
|
||||
height: 16px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "\{badge.value} \{badge.unit}";
|
||||
height: 19px;
|
||||
font-size: 19px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component AppWindow inherits Window {
|
||||
width: 1024px;
|
||||
height: 600px;
|
||||
default-font-size: 16px;
|
||||
|
||||
property <int> page: 0;
|
||||
|
||||
in property <DateTimeData> date-time;
|
||||
in property <[ThermometerData]> badges;
|
||||
|
||||
in property <[LightData]> lights;
|
||||
callback toggle-light(string);
|
||||
|
||||
VerticalLayout {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
height: 64px;
|
||||
spacing: 15px;
|
||||
padding: StyleMetrics.layout-padding / 2;
|
||||
|
||||
Rectangle {
|
||||
preferred-width: parent.width;
|
||||
height: parent.height;
|
||||
}
|
||||
|
||||
for badge[index] in root.badges: Thermometer {
|
||||
badge: badge;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
preferred-width: parent.width;
|
||||
height: parent.height;
|
||||
}
|
||||
|
||||
Clock {
|
||||
date-time: date-time;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
height: root.height - 64px;
|
||||
Rectangle {
|
||||
preferred-width: root.width;
|
||||
VerticalLayout {
|
||||
if root.page == 0: Rectangle {
|
||||
StandardTableView {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
columns: [
|
||||
{ title: "Header 1" },
|
||||
{ title: "Header 2" },
|
||||
];
|
||||
rows: [
|
||||
[
|
||||
{ text: "Item 1" },
|
||||
{ text: "Item 2" },
|
||||
],
|
||||
[
|
||||
{ text: "Item 1" },
|
||||
{ text: "Item 2" },
|
||||
],
|
||||
[
|
||||
{ text: "Item 1" },
|
||||
{ text: "Item 2" },
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if root.page == 1: Rectangle {
|
||||
background: red;
|
||||
// pages go here
|
||||
}
|
||||
|
||||
if root.page == 2: Rectangle {
|
||||
background: yellow;
|
||||
// pages go here
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 64px;
|
||||
HorizontalBox {
|
||||
padding-top: 0;
|
||||
Button {
|
||||
text: "Climate";
|
||||
clicked => { root.page = 0; }
|
||||
primary: root.page == 0;
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Calendar";
|
||||
clicked => { root.page = 1; }
|
||||
primary: root.page == 1;
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Energy";
|
||||
clicked => { root.page = 2; }
|
||||
primary: root.page == 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalBox {
|
||||
preferred-width: 0;
|
||||
for light[index] in root.lights: Button {
|
||||
text: light.name;
|
||||
primary: light.on;
|
||||
enabled: light.available;
|
||||
clicked => { toggle-light(light.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
samples
Normal file
152
samples
Normal file
@@ -0,0 +1,152 @@
|
||||
Status Sample name
|
||||
[G...] aarch64-ol7u9-linux-gnu
|
||||
[G...] aarch64-ol8u10-linux-gnu
|
||||
[G...] aarch64-ol8u6-linux-gnu
|
||||
[G...] aarch64-ol8u7-linux-gnu
|
||||
[G...] aarch64-ol8u8-linux-gnu
|
||||
[G...] aarch64-ol8u9-linux-gnu
|
||||
[G...] aarch64-ol9u2-linux-gnu
|
||||
[G...] aarch64-ol9u3-linux-gnu
|
||||
[G...] aarch64-ol9u4-linux-gnu
|
||||
[G...] aarch64-ol9u5-linux-gnu
|
||||
[G...] aarch64-rpi3-linux-gnu
|
||||
[G...] aarch64-rpi4-linux-gnu
|
||||
[G...] aarch64-unknown-linux-gnu
|
||||
[G...] aarch64-unknown-linux-musl
|
||||
[G...] aarch64-unknown-linux-uclibc
|
||||
[G...] alphaev56-unknown-linux-gnu
|
||||
[G...] alphaev67-unknown-linux-gnu
|
||||
[G...] arc-arc700-linux-uclibc
|
||||
[G...] arc-archs-linux-gnu
|
||||
[G...] arc-multilib-elf32
|
||||
[G...] arc-multilib-linux-gnu
|
||||
[G...] arc-multilib-linux-uclibc
|
||||
[G...] arm-bare_newlib_cortex_m3_nommu-eabi
|
||||
[G...] arm-cortex_a15-linux-gnueabihf
|
||||
[G..X] arm-cortexa5-linux-uclibcgnueabihf
|
||||
[G...] arm-cortex_a8-linux-gnueabi
|
||||
[G..X] arm-cortexa9_neon-linux-gnueabihf
|
||||
[G..X] x86_64-w64-mingw32,arm-cortexa9_neon-linux-gnueabihf
|
||||
[G...] armeb-unknown-eabi
|
||||
[G...] armeb-unknown-linux-gnueabi
|
||||
[G...] armeb-unknown-linux-uclibcgnueabi
|
||||
[G...] arm-multilib-linux-uclibcgnueabi
|
||||
[G...] arm-nano-eabi
|
||||
[G...] arm-none-eabi
|
||||
[G...] arm-ol7u9-linux-gnueabi
|
||||
[G...] arm-ol7u9-linux-gnueabihf
|
||||
[G..X] arm-picolibc-default
|
||||
[G..X] arm-picolibc-eabi
|
||||
[G..X] x86_64-unknown-linux-gnu,arm-picolibc-eabi
|
||||
[G...] arm-unknown-eabi
|
||||
[G...] arm-unknown-linux-gnueabi
|
||||
[G..X] arm-unknown-linux-musleabi
|
||||
[G...] arm-unknown-linux-uclibcgnueabi
|
||||
[G..X] arm-unknown-linux-uclibcgnueabihf
|
||||
[G...] armv6-nommu-linux-uclibcgnueabi
|
||||
[G...] armv6-unknown-linux-gnueabi
|
||||
[G...] armv6-unknown-linux-gnueabihf
|
||||
[G...] armv7-rpi2-linux-gnueabihf
|
||||
[G...] armv8-rpi3-linux-gnueabihf
|
||||
[G...] armv8-rpi4-linux-gnueabihf
|
||||
[G...] avr
|
||||
[G...] bpf-unknown-none
|
||||
[G..X] hppa-unknown-linux-gnu
|
||||
[G...] i586-geode-linux-uclibc
|
||||
[G...] i686-centos6-linux-gnu
|
||||
[G...] i686-centos7-linux-gnu
|
||||
[G...] i686-nptl-linux-gnu
|
||||
[G...] i686-ol8u6-linux-gnu
|
||||
[G...] i686-ol8u7-linux-gnu
|
||||
[G...] i686-ol8u8-linux-gnu
|
||||
[G...] i686-ol9u2-linux-gnu
|
||||
[G...] i686-ubuntu14.04-linux-gnu
|
||||
[G...] i686-ubuntu16.04-linux-gnu
|
||||
[G..X] i686-w64-mingw32
|
||||
[G..X] lm32-unknown-elf
|
||||
[G..X] loongarch64-unknown-linux-gnu
|
||||
[G..X] loongarch64-unknown-linux-musl
|
||||
[G...] m68k-unknown-elf
|
||||
[G...] m68k-unknown-linux-gnu
|
||||
[G...] m68k-unknown-uclinux-uclibc
|
||||
[G...] powerpc-unknown-linux-uclibc,m68k-unknown-uclinux-uclibc
|
||||
[G...] mips64el-multilib-linux-uclibc
|
||||
[G...] mips64-ol9u5-linux-gnu
|
||||
[G...] mips64-unknown-linux-gnu
|
||||
[G...] mips-ar2315-linux-gnu
|
||||
[G...] mipsel-multilib-linux-gnu
|
||||
[G...] mipsel-sde-elf
|
||||
[G...] mipsel-unknown-linux-gnu
|
||||
[G...] mips-malta-linux-gnu
|
||||
[G...] mips-unknown-elf
|
||||
[G...] mips-unknown-linux-gnu
|
||||
[G...] mips-unknown-linux-uclibc
|
||||
[G..X] moxie-unknown-elf
|
||||
[G..X] moxie-unknown-moxiebox
|
||||
[G..X] x86_64-multilib-linux-uclibc,moxie-unknown-moxiebox
|
||||
[G..X] msp430-unknown-elf
|
||||
[G...] nios2-altera-linux-gnu
|
||||
[G..X] i686-w64-mingw32,nios2-spico-elf
|
||||
[G...] nios2-unknown-elf
|
||||
[G..X] or1k-unknown-elf
|
||||
[G..X] or1k-unknown-linux-gnu
|
||||
[G..X] or1k-unknown-linux-musl
|
||||
[G...] powerpc-405-linux-gnu
|
||||
[G...] powerpc64le-unknown-linux-gnu
|
||||
[G...] powerpc64-multilib-linux-gnu
|
||||
[G...] powerpc64-unknown-linux-gnu
|
||||
[G...] powerpc64-unknown-linux-musl
|
||||
[G...] powerpc-8540-linux-gnu
|
||||
[G...] powerpc-860-linux-gnu
|
||||
[G...] powerpc-e300c3-linux-gnu
|
||||
[G...] powerpc-e500v2-linux-gnuspe
|
||||
[G...] x86_64-multilib-linux-uclibc,powerpc-unknown-elf
|
||||
[G...] powerpc-unknown-linux-gnu
|
||||
[G...] powerpc-unknown-linux-uclibc
|
||||
[G...] powerpc-unknown_nofpu-linux-gnu
|
||||
[G...] pru
|
||||
[G...] riscv32-hifive1-elf
|
||||
[G...] riscv32-picolibc-elf
|
||||
[G...] riscv32-unknown-elf
|
||||
[G...] riscv64-multilib-elf
|
||||
[G...] riscv64-unknown-elf
|
||||
[G...] riscv64-unknown-linux-gnu
|
||||
[G..X] rx-unknown-elf
|
||||
[G..X] s390-ibm-linux-gnu
|
||||
[G..X] s390-unknown-linux-gnu
|
||||
[G...] s390x-ibm-linux-gnu
|
||||
[G...] s390x-unknown-linux-gnu
|
||||
[G...] sh4-multilib-linux-gnu
|
||||
[G...] sh4-multilib-linux-uclibc
|
||||
[G...] sh-unknown-elf
|
||||
[G...] sparc64-multilib-linux-gnu
|
||||
[G...] sparc-leon-linux-uclibc
|
||||
[G...] sparc-unknown-linux-gnu
|
||||
[G..X] tic6x-uclinux
|
||||
[G..X] tricore-none-eabi
|
||||
[G...] x86_64-centos7-linux-gnu
|
||||
[G...] x86_64-multilib-linux-gnu
|
||||
[G...] x86_64-multilib-linux-musl
|
||||
[G...] x86_64-multilib-linux-uclibc
|
||||
[G...] x86_64-ol8u10-linux-gnu
|
||||
[G...] x86_64-ol8u6-linux-gnu
|
||||
[G...] x86_64-ol8u7-linux-gnu
|
||||
[G...] x86_64-ol8u8-linux-gnu
|
||||
[G...] x86_64-ol8u9-linux-gnu
|
||||
[G...] x86_64-ol9u2-linux-gnu
|
||||
[G...] x86_64-ol9u3-linux-gnu
|
||||
[G...] x86_64-ol9u4-linux-gnu
|
||||
[G...] x86_64-ol9u5-linux-gnu
|
||||
[G..X] x86_64-w64-mingw32,x86_64-pc-linux-gnu
|
||||
[G...] x86_64-ubuntu14.04-linux-gnu
|
||||
[G...] x86_64-ubuntu16.04-linux-gnu
|
||||
[G...] x86_64-unknown-linux-gnu
|
||||
[G...] x86_64-unknown-linux-uclibc
|
||||
[G..X] x86_64-w64-mingw32
|
||||
[G..X] xtensa-fsf-elf
|
||||
[G...] xtensa-fsf-linux-uclibc
|
||||
L (Local) : sample was found in current directory
|
||||
G (Global) : sample was installed with crosstool-NG
|
||||
X (EXPERIMENTAL): sample may use EXPERIMENTAL features
|
||||
B (BROKEN) : sample is currently broken
|
||||
O (OBSOLETE) : sample needs to be upgraded
|
||||
293
tree
Normal file
293
tree
Normal file
@@ -0,0 +1,293 @@
|
||||
arrayvec v0.7.8
|
||||
├── kurbo v0.13.1
|
||||
│ ├── svgtypes v0.16.1
|
||||
│ │ ├── resvg v0.47.0
|
||||
│ │ │ ├── i-slint-common v1.17.1
|
||||
│ │ │ │ ├── i-slint-core v1.17.1
|
||||
│ │ │ │ │ ├── i-slint-backend-selector v1.17.1
|
||||
│ │ │ │ │ │ └── slint v1.17.1
|
||||
│ │ │ │ │ │ ├── dashboard v0.1.0 (/home/avii/source/rust/trekstor/dashboard)
|
||||
│ │ │ │ │ │ └── trekstor v0.1.0 (/home/avii/source/rust/trekstor/trekstor)
|
||||
│ │ │ │ │ │ └── dashboard v0.1.0 (/home/avii/source/rust/trekstor/dashboard)
|
||||
│ │ │ │ │ ├── i-slint-renderer-software v1.17.1
|
||||
│ │ │ │ │ │ └── slint v1.17.1 (*)
|
||||
│ │ │ │ │ └── slint v1.17.1 (*)
|
||||
│ │ │ │ ├── i-slint-renderer-software v1.17.1 (*)
|
||||
│ │ │ │ └── slint v1.17.1 (*)
|
||||
│ │ │ └── i-slint-core v1.17.1 (*)
|
||||
│ │ └── usvg v0.47.0
|
||||
│ │ └── resvg v0.47.0 (*)
|
||||
│ └── usvg v0.47.0 (*)
|
||||
├── lyon_geom v1.0.19
|
||||
│ ├── i-slint-core v1.17.1 (*)
|
||||
│ └── lyon_path v1.0.19
|
||||
│ ├── i-slint-core v1.17.1 (*)
|
||||
│ ├── i-slint-renderer-software v1.17.1 (*)
|
||||
│ ├── lyon_algorithms v1.0.20
|
||||
│ │ └── i-slint-core v1.17.1 (*)
|
||||
│ └── lyon_extra v1.1.0
|
||||
│ └── i-slint-core v1.17.1 (*)
|
||||
├── polycool v0.4.0
|
||||
│ └── kurbo v0.13.1 (*)
|
||||
├── taffy v0.10.1
|
||||
│ └── i-slint-core v1.17.1 (*)
|
||||
└── tiny-skia v0.12.0
|
||||
└── resvg v0.47.0 (*)
|
||||
|
||||
arrayvec v0.7.8
|
||||
└── lyon_geom v1.0.19
|
||||
└── lyon_path v1.0.19
|
||||
├── i-slint-compiler v1.17.1
|
||||
│ └── slint-macros v1.17.1 (proc-macro)
|
||||
│ └── slint v1.17.1 (*)
|
||||
└── lyon_extra v1.1.0
|
||||
└── i-slint-compiler v1.17.1 (*)
|
||||
|
||||
euclid v0.22.14
|
||||
├── i-slint-core v1.17.1 (*)
|
||||
├── i-slint-renderer-software v1.17.1 (*)
|
||||
└── lyon_geom v1.0.19 (*)
|
||||
|
||||
euclid v0.22.14
|
||||
└── lyon_geom v1.0.19 (*)
|
||||
|
||||
font-types v0.11.3
|
||||
└── read-fonts v0.39.2
|
||||
├── fontique v0.10.0
|
||||
│ ├── i-slint-common v1.17.1 (*)
|
||||
│ └── parley v0.10.0
|
||||
│ └── i-slint-core v1.17.1 (*)
|
||||
├── harfrust v0.8.4
|
||||
│ └── parley v0.10.0 (*)
|
||||
└── skrifa v0.42.1
|
||||
├── i-slint-common v1.17.1 (*)
|
||||
├── i-slint-core v1.17.1 (*)
|
||||
├── i-slint-renderer-software v1.17.1 (*)
|
||||
└── parley v0.10.0 (*)
|
||||
|
||||
font-types v0.12.2
|
||||
└── read-fonts v0.41.0
|
||||
└── skrifa v0.44.0
|
||||
└── swash v0.2.10
|
||||
├── i-slint-core v1.17.1 (*)
|
||||
└── i-slint-renderer-software v1.17.1 (*)
|
||||
|
||||
hashbrown v0.14.5
|
||||
└── rowan v0.16.1
|
||||
└── i-slint-compiler v1.17.1 (*)
|
||||
|
||||
hashbrown v0.16.1
|
||||
└── clru v0.6.3
|
||||
├── i-slint-core v1.17.1 (*)
|
||||
└── i-slint-renderer-software v1.17.1 (*)
|
||||
|
||||
hashbrown v0.17.1
|
||||
└── indexmap v2.14.0
|
||||
└── toml_edit v0.25.13+spec-1.1.0
|
||||
└── proc-macro-crate v3.5.0
|
||||
└── num_enum_derive v0.7.6 (proc-macro)
|
||||
└── num_enum v0.7.6
|
||||
└── i-slint-compiler v1.17.1 (*)
|
||||
|
||||
hashbrown v0.17.1
|
||||
├── fontique v0.10.0 (*)
|
||||
└── parley v0.10.0 (*)
|
||||
|
||||
i-slint-common v1.17.1 (*)
|
||||
|
||||
i-slint-common v1.17.1
|
||||
└── i-slint-compiler v1.17.1 (*)
|
||||
[build-dependencies]
|
||||
└── i-slint-backend-selector v1.17.1 (*)
|
||||
|
||||
lyon_extra v1.1.0 (*)
|
||||
|
||||
lyon_extra v1.1.0 (*)
|
||||
|
||||
lyon_geom v1.0.19 (*)
|
||||
|
||||
lyon_geom v1.0.19 (*)
|
||||
|
||||
lyon_path v1.0.19 (*)
|
||||
|
||||
lyon_path v1.0.19 (*)
|
||||
|
||||
num-traits v0.2.19
|
||||
├── chrono v0.4.45
|
||||
│ └── i-slint-core v1.17.1 (*)
|
||||
├── euclid v0.22.14 (*)
|
||||
├── i-slint-core v1.17.1 (*)
|
||||
├── i-slint-renderer-software v1.17.1 (*)
|
||||
├── image v0.25.10
|
||||
│ └── i-slint-core v1.17.1 (*)
|
||||
├── integer-sqrt v0.1.5
|
||||
│ └── i-slint-renderer-software v1.17.1 (*)
|
||||
├── lyon_algorithms v1.0.20 (*)
|
||||
├── lyon_geom v1.0.19 (*)
|
||||
├── lyon_path v1.0.19 (*)
|
||||
├── moxcms v0.8.1
|
||||
│ └── image v0.25.10 (*)
|
||||
└── slint v1.17.1 (*)
|
||||
|
||||
num-traits v0.2.19
|
||||
├── euclid v0.22.14 (*)
|
||||
├── lyon_geom v1.0.19 (*)
|
||||
└── lyon_path v1.0.19 (*)
|
||||
|
||||
read-fonts v0.39.2 (*)
|
||||
|
||||
read-fonts v0.41.0 (*)
|
||||
|
||||
skrifa v0.42.1 (*)
|
||||
|
||||
skrifa v0.44.0 (*)
|
||||
|
||||
smallvec v1.15.2
|
||||
└── idna v1.1.0
|
||||
└── url v2.5.8
|
||||
└── i-slint-compiler v1.17.1 (*)
|
||||
|
||||
smallvec v1.15.2
|
||||
├── fixed_decimal v0.7.2
|
||||
│ └── icu_decimal v2.2.0
|
||||
│ └── i-slint-common v1.17.1 (*)
|
||||
├── fontique v0.10.0 (*)
|
||||
├── harfrust v0.8.4 (*)
|
||||
├── icu_normalizer v2.2.0
|
||||
│ ├── i-slint-compiler v1.17.1 (*)
|
||||
│ ├── i-slint-core v1.17.1 (*)
|
||||
│ ├── idna_adapter v1.2.2
|
||||
│ │ └── idna v1.1.0 (*)
|
||||
│ └── parley v0.10.0 (*)
|
||||
├── kurbo v0.13.1 (*)
|
||||
└── rustybuzz v0.20.1
|
||||
└── usvg v0.47.0 (*)
|
||||
|
||||
syn v2.0.119
|
||||
├── const-field-offset-macro v0.2.0 (proc-macro)
|
||||
│ └── const-field-offset v0.2.0
|
||||
│ ├── i-slint-core v1.17.1 (*)
|
||||
│ ├── slint v1.17.1 (*)
|
||||
│ └── vtable v0.4.0
|
||||
│ ├── i-slint-core v1.17.1 (*)
|
||||
│ └── slint v1.17.1 (*)
|
||||
├── derive_more-impl v2.1.1 (proc-macro)
|
||||
│ └── derive_more v2.1.1
|
||||
│ ├── i-slint-common v1.17.1 (*)
|
||||
│ ├── i-slint-common v1.17.1 (*)
|
||||
│ ├── i-slint-compiler v1.17.1 (*)
|
||||
│ ├── i-slint-core v1.17.1 (*)
|
||||
│ └── i-slint-renderer-software v1.17.1 (*)
|
||||
├── i-slint-core-macros v1.17.1 (proc-macro)
|
||||
│ ├── i-slint-backend-selector v1.17.1 (*)
|
||||
│ ├── i-slint-core v1.17.1 (*)
|
||||
│ └── slint v1.17.1 (*)
|
||||
├── num_enum_derive v0.7.6 (proc-macro) (*)
|
||||
├── pin-project-internal v1.1.13 (proc-macro)
|
||||
│ └── pin-project v1.1.13
|
||||
│ └── i-slint-core v1.17.1 (*)
|
||||
├── strum_macros v0.28.0 (proc-macro)
|
||||
│ └── strum v0.28.0
|
||||
│ ├── i-slint-compiler v1.17.1 (*)
|
||||
│ └── i-slint-core v1.17.1 (*)
|
||||
├── synstructure v0.13.2
|
||||
│ ├── yoke-derive v0.8.2 (proc-macro)
|
||||
│ │ └── yoke v0.8.3
|
||||
│ │ ├── icu_collections v2.2.0
|
||||
│ │ │ ├── icu_locale v2.2.0
|
||||
│ │ │ │ ├── icu_decimal v2.2.0 (*)
|
||||
│ │ │ │ └── icu_segmenter v2.2.0
|
||||
│ │ │ │ └── parley v0.10.0 (*)
|
||||
│ │ │ ├── icu_normalizer v2.2.0 (*)
|
||||
│ │ │ ├── icu_properties v2.2.0
|
||||
│ │ │ │ ├── idna_adapter v1.2.2 (*)
|
||||
│ │ │ │ ├── parley v0.10.0 (*)
|
||||
│ │ │ │ └── parley_data v0.10.0
|
||||
│ │ │ │ └── parley v0.10.0 (*)
|
||||
│ │ │ └── icu_segmenter v2.2.0 (*)
|
||||
│ │ ├── icu_provider v2.2.0
|
||||
│ │ │ ├── i-slint-common v1.17.1 (*)
|
||||
│ │ │ ├── icu_decimal v2.2.0 (*)
|
||||
│ │ │ ├── icu_locale v2.2.0 (*)
|
||||
│ │ │ ├── icu_normalizer v2.2.0 (*)
|
||||
│ │ │ ├── icu_properties v2.2.0 (*)
|
||||
│ │ │ └── icu_segmenter v2.2.0 (*)
|
||||
│ │ ├── zerotrie v0.2.4
|
||||
│ │ │ ├── icu_properties v2.2.0 (*)
|
||||
│ │ │ └── icu_provider v2.2.0 (*)
|
||||
│ │ └── zerovec v0.11.6
|
||||
│ │ ├── icu_collections v2.2.0 (*)
|
||||
│ │ ├── icu_decimal v2.2.0 (*)
|
||||
│ │ ├── icu_locale v2.2.0 (*)
|
||||
│ │ ├── icu_locale_core v2.2.0
|
||||
│ │ │ ├── i-slint-common v1.17.1 (*)
|
||||
│ │ │ ├── icu_decimal v2.2.0 (*)
|
||||
│ │ │ ├── icu_locale v2.2.0 (*)
|
||||
│ │ │ ├── icu_properties v2.2.0 (*)
|
||||
│ │ │ └── icu_provider v2.2.0 (*)
|
||||
│ │ ├── icu_normalizer v2.2.0 (*)
|
||||
│ │ ├── icu_properties v2.2.0 (*)
|
||||
│ │ ├── icu_provider v2.2.0 (*)
|
||||
│ │ ├── icu_segmenter v2.2.0 (*)
|
||||
│ │ ├── potential_utf v0.1.5
|
||||
│ │ │ ├── icu_collections v2.2.0 (*)
|
||||
│ │ │ ├── icu_locale v2.2.0 (*)
|
||||
│ │ │ └── icu_segmenter v2.2.0 (*)
|
||||
│ │ └── tinystr v0.8.3
|
||||
│ │ ├── icu_locale v2.2.0 (*)
|
||||
│ │ └── icu_locale_core v2.2.0 (*)
|
||||
│ └── zerofrom-derive v0.1.7 (proc-macro)
|
||||
│ └── zerofrom v0.1.8
|
||||
│ ├── icu_collections v2.2.0 (*)
|
||||
│ ├── icu_provider v2.2.0 (*)
|
||||
│ ├── yoke v0.8.3 (*)
|
||||
│ ├── zerotrie v0.2.4 (*)
|
||||
│ └── zerovec v0.11.6 (*)
|
||||
├── vtable-macro v0.4.0 (proc-macro)
|
||||
│ └── vtable v0.4.0 (*)
|
||||
├── yoke-derive v0.8.2 (proc-macro) (*)
|
||||
├── zerofrom-derive v0.1.7 (proc-macro) (*)
|
||||
└── zerovec-derive v0.11.3 (proc-macro)
|
||||
└── zerovec v0.11.6 (*)
|
||||
|
||||
syn v3.0.3
|
||||
├── auto_enums v0.8.10 (proc-macro)
|
||||
│ └── i-slint-core v1.17.1 (*)
|
||||
├── bytemuck_derive v1.12.0 (proc-macro)
|
||||
│ └── bytemuck v1.25.2
|
||||
│ ├── font-types v0.11.3 (*)
|
||||
│ ├── font-types v0.12.2 (*)
|
||||
│ ├── harfrust v0.8.4 (*)
|
||||
│ ├── i-slint-renderer-software v1.17.1 (*)
|
||||
│ ├── image v0.25.10 (*)
|
||||
│ ├── read-fonts v0.39.2 (*)
|
||||
│ ├── read-fonts v0.41.0 (*)
|
||||
│ ├── rgb v0.8.53
|
||||
│ │ ├── i-slint-core v1.17.1 (*)
|
||||
│ │ └── resvg v0.47.0 (*)
|
||||
│ ├── rustybuzz v0.20.1 (*)
|
||||
│ ├── skrifa v0.42.1 (*)
|
||||
│ ├── skrifa v0.44.0 (*)
|
||||
│ ├── tiny-skia v0.12.0 (*)
|
||||
│ └── tiny-skia-path v0.12.0
|
||||
│ ├── tiny-skia v0.12.0 (*)
|
||||
│ └── usvg v0.47.0 (*)
|
||||
├── derive_utils v0.16.0
|
||||
│ └── auto_enums v0.8.10 (proc-macro) (*)
|
||||
├── displaydoc v0.2.7 (proc-macro)
|
||||
│ ├── fixed_decimal v0.7.2 (*)
|
||||
│ ├── icu_collections v2.2.0 (*)
|
||||
│ ├── icu_decimal v2.2.0 (*)
|
||||
│ ├── icu_locale_core v2.2.0 (*)
|
||||
│ ├── icu_provider v2.2.0 (*)
|
||||
│ ├── tinystr v0.8.3 (*)
|
||||
│ └── zerotrie v0.2.4 (*)
|
||||
└── thiserror-impl v2.0.19 (proc-macro)
|
||||
├── thiserror v2.0.19
|
||||
│ └── lyon_extra v1.1.0 (*)
|
||||
└── thiserror v2.0.19
|
||||
└── lyon_extra v1.1.0 (*)
|
||||
|
||||
thiserror v2.0.19 (*)
|
||||
|
||||
thiserror v2.0.19 (*)
|
||||
1
trekstor/.gitignore
vendored
Normal file
1
trekstor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
4747
trekstor/Cargo.lock
generated
Normal file
4747
trekstor/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
trekstor/Cargo.toml
Normal file
10
trekstor/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "trekstor"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
slint.workspace = true
|
||||
|
||||
evdev = "0.13.2"
|
||||
libc = "0.2.189"
|
||||
74
trekstor/src/framebuffer/ffi.rs
Normal file
74
trekstor/src/framebuffer/ffi.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! Kernel ABI: framebuffer + Kindle EPDC structs and ioctl numbers.
|
||||
|
||||
// Standard Linux framebuffer ioctl numbers (see <linux/fb.h>).
|
||||
// Typed as c_ulong (not libc::Ioctl) so the crate still type-checks on
|
||||
// non-Linux dev hosts where libc::Ioctl isn't defined, like macos
|
||||
pub(super) const FBIOGET_VSCREENINFO: libc::c_ulong = 0x4600;
|
||||
pub(super) const FBIOGET_FSCREENINFO: libc::c_ulong = 0x4602;
|
||||
|
||||
// These structs mirror the kernel's `fb_var_screeninfo` and `fb_fix_screeninfo`.
|
||||
// We only read from them, fields we care about are `xres`, `yres` (visible
|
||||
// resolution) and `line_length` (stride in bytes per row, which may be larger
|
||||
// than xres due to alignment padding).
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
pub(super) struct FbBitfield {
|
||||
pub(super) offset: u32,
|
||||
pub(super) length: u32,
|
||||
pub(super) msb_right: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
pub(super) struct FbVarScreeninfo {
|
||||
pub(super) xres: u32,
|
||||
pub(super) yres: u32,
|
||||
pub(super) xres_virtual: u32,
|
||||
pub(super) yres_virtual: u32,
|
||||
pub(super) xoffset: u32,
|
||||
pub(super) yoffset: u32,
|
||||
pub(super) bits_per_pixel: u32,
|
||||
pub(super) grayscale: u32,
|
||||
pub(super) red: FbBitfield,
|
||||
pub(super) green: FbBitfield,
|
||||
pub(super) blue: FbBitfield,
|
||||
pub(super) transp: FbBitfield,
|
||||
pub(super) nonstd: u32,
|
||||
pub(super) activate: u32,
|
||||
pub(super) height: u32,
|
||||
pub(super) width: u32,
|
||||
pub(super) accel_flags: u32,
|
||||
pub(super) pixclock: u32,
|
||||
pub(super) left_margin: u32,
|
||||
pub(super) right_margin: u32,
|
||||
pub(super) upper_margin: u32,
|
||||
pub(super) lower_margin: u32,
|
||||
pub(super) hsync_len: u32,
|
||||
pub(super) vsync_len: u32,
|
||||
pub(super) sync: u32,
|
||||
pub(super) vmode: u32,
|
||||
pub(super) rotate: u32,
|
||||
pub(super) colorspace: u32,
|
||||
pub(super) reserved: [u32; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
pub(super) struct FbFixScreeninfo {
|
||||
pub(super) id: [u8; 16],
|
||||
pub(super) smem_start: libc::c_ulong,
|
||||
pub(super) smem_len: u32,
|
||||
pub(super) type_: u32,
|
||||
pub(super) type_aux: u32,
|
||||
pub(super) visual: u32,
|
||||
pub(super) xpanstep: u16,
|
||||
pub(super) ypanstep: u16,
|
||||
pub(super) ywrapstep: u16,
|
||||
pub(super) line_length: u32,
|
||||
pub(super) mmio_start: libc::c_ulong,
|
||||
pub(super) mmio_len: u32,
|
||||
pub(super) accel: u32,
|
||||
pub(super) capabilities: u16,
|
||||
pub(super) reserved: [u16; 2],
|
||||
}
|
||||
135
trekstor/src/framebuffer/mod.rs
Normal file
135
trekstor/src/framebuffer/mod.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
mod ffi;
|
||||
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
use ffi::{FBIOGET_FSCREENINFO, FBIOGET_VSCREENINFO, FbFixScreeninfo, FbVarScreeninfo};
|
||||
|
||||
/// Memory-mapped handle to the Kindle's e-ink framebuffer.
|
||||
///
|
||||
/// Pixel format is 8-bit grayscale (one byte per pixel). The `stride` may be
|
||||
/// wider than `width` due to hardware alignment requirements.
|
||||
pub(crate) struct Framebuffer {
|
||||
map: *mut u8,
|
||||
len: usize,
|
||||
pub(crate) width: u32,
|
||||
pub(crate) height: u32,
|
||||
}
|
||||
|
||||
// SAFETY: The mmap is process-wide and we only access it from the event loop thread.
|
||||
unsafe impl Send for Framebuffer {}
|
||||
|
||||
impl Framebuffer {
|
||||
/// Open the framebuffer device and query its geometry from the kernel.
|
||||
///
|
||||
/// This works on any Kindle model - the resolution and stride are read at
|
||||
/// runtime rather than being hardcoded.
|
||||
pub(crate) fn open() -> std::io::Result<Self> {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/fb0")?;
|
||||
|
||||
let fd = file.as_raw_fd();
|
||||
|
||||
let mut vinfo = FbVarScreeninfo::default();
|
||||
if unsafe {
|
||||
libc::ioctl(
|
||||
fd,
|
||||
FBIOGET_VSCREENINFO as _,
|
||||
&mut vinfo as *mut _ as *mut libc::c_void,
|
||||
)
|
||||
} == -1
|
||||
{
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let mut finfo = FbFixScreeninfo::default();
|
||||
if unsafe {
|
||||
libc::ioctl(
|
||||
fd,
|
||||
FBIOGET_FSCREENINFO as _,
|
||||
&mut finfo as *mut _ as *mut libc::c_void,
|
||||
)
|
||||
} == -1
|
||||
{
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let width = vinfo.xres;
|
||||
let height = vinfo.yres;
|
||||
let stride = finfo.line_length as usize;
|
||||
|
||||
// The whole render path treats the mmap as four bytes per pixel. A
|
||||
// different depth would silently produce garbled output, so reject it
|
||||
// with a clear error instead.
|
||||
if vinfo.bits_per_pixel != 32 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"unsupported framebuffer depth: {} bpp (expected 8-bit grayscale)",
|
||||
vinfo.bits_per_pixel
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if width == 0 || height == 0 || stride < width as usize {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("invalid framebuffer geometry: {width}x{height}, stride={stride}"),
|
||||
));
|
||||
}
|
||||
|
||||
let len = stride * height as usize;
|
||||
|
||||
let map = unsafe {
|
||||
libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
len,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_SHARED,
|
||||
fd,
|
||||
0,
|
||||
)
|
||||
};
|
||||
|
||||
if map == libc::MAP_FAILED {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
map: map as *mut u8,
|
||||
len,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn write_buffer(
|
||||
&mut self,
|
||||
pixels: &[slint::platform::software_renderer::Rgb565Pixel],
|
||||
) {
|
||||
let dst = unsafe { std::slice::from_raw_parts_mut(self.map as *mut u32, pixels.len()) };
|
||||
|
||||
for (dst, src) in dst.iter_mut().zip(pixels) {
|
||||
let c = src.0; // assuming Rgb565Pixel(pub u16)
|
||||
|
||||
let r5 = ((c >> 11) & 0x1f) as u8;
|
||||
let g6 = ((c >> 5) & 0x3f) as u8;
|
||||
let b5 = (c & 0x1f) as u8;
|
||||
|
||||
// Expand to 8 bits.
|
||||
let r = (r5 << 3) | (r5 >> 2);
|
||||
let g = (g6 << 2) | (g6 >> 4);
|
||||
let b = (b5 << 3) | (b5 >> 2);
|
||||
|
||||
// BGRA8888
|
||||
*dst = u32::from_le_bytes([b, g, r, 0xff]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Framebuffer {
|
||||
fn drop(&mut self) {
|
||||
unsafe { libc::munmap(self.map.cast::<libc::c_void>(), self.len) };
|
||||
}
|
||||
}
|
||||
101
trekstor/src/input.rs
Normal file
101
trekstor/src/input.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use evdev::{AbsoluteAxisCode, Device, EventSummary, KeyCode};
|
||||
|
||||
pub struct InputState {
|
||||
device: Option<Device>,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
keys_down: HashSet<KeyCode>,
|
||||
keys_pressed_this_update: HashSet<KeyCode>,
|
||||
keys_released_this_update: HashSet<KeyCode>,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub fn new(w: u32, h: u32) -> Self {
|
||||
let device = evdev::enumerate()
|
||||
.find(|(_, device)| device.name() == Some("silead_ts"))
|
||||
.map(|(_, d)| {
|
||||
d.set_nonblocking(true).ok();
|
||||
d
|
||||
});
|
||||
|
||||
Self {
|
||||
device,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w.cast_signed(),
|
||||
height: h.cast_signed(),
|
||||
keys_down: HashSet::new(),
|
||||
keys_pressed_this_update: HashSet::new(),
|
||||
keys_released_this_update: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn finish(&mut self) {
|
||||
self.keys_pressed_this_update.clear();
|
||||
self.keys_released_this_update.clear();
|
||||
}
|
||||
|
||||
pub(super) fn handle_event(&mut self) {
|
||||
let Some(device) = &mut self.device else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(events) = device.fetch_events() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for event in events {
|
||||
match event.destructure() {
|
||||
EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_X, value) => {
|
||||
self.x = ((((value - 13) as f32 / 882.) * (self.width as f32)) as i32)
|
||||
.max(0)
|
||||
.min(self.width);
|
||||
}
|
||||
EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_Y, value) => {
|
||||
self.y = ((((value - 13) as f32 / 624.) * (self.height as f32)) as i32)
|
||||
.max(0)
|
||||
.min(self.width);
|
||||
}
|
||||
EventSummary::Key(_, key, value) => match value {
|
||||
1 => {
|
||||
if !self.keys_down.contains(&key) {
|
||||
self.keys_pressed_this_update.insert(key);
|
||||
}
|
||||
self.keys_down.insert(key);
|
||||
}
|
||||
0 => {
|
||||
if self.keys_down.contains(&key) {
|
||||
self.keys_released_this_update.insert(key);
|
||||
}
|
||||
self.keys_down.remove(&key);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl InputState {
|
||||
pub fn is_just_pressed(&self, key: KeyCode) -> bool {
|
||||
self.keys_pressed_this_update.contains(&key)
|
||||
}
|
||||
|
||||
pub fn is_down(&self, key: KeyCode) -> bool {
|
||||
self.keys_down.contains(&key)
|
||||
}
|
||||
|
||||
pub fn is_just_released(&self, key: KeyCode) -> bool {
|
||||
self.keys_released_this_update.contains(&key)
|
||||
}
|
||||
|
||||
pub fn is_up(&self, key: KeyCode) -> bool {
|
||||
!self.is_down(key)
|
||||
}
|
||||
}
|
||||
153
trekstor/src/lib.rs
Normal file
153
trekstor/src/lib.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
#![allow(clippy::borrow_as_ptr)]
|
||||
|
||||
mod framebuffer;
|
||||
mod input;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use evdev::KeyCode;
|
||||
use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType, Rgb565Pixel};
|
||||
use slint::platform::{EventLoopProxy, Platform, WindowEvent};
|
||||
use slint::{EventLoopError, LogicalPosition, PhysicalSize, PlatformError, WindowSize};
|
||||
|
||||
use self::framebuffer::Framebuffer;
|
||||
use self::input::InputState;
|
||||
|
||||
pub struct Trekstor {
|
||||
window: Rc<MinimalSoftwareWindow>,
|
||||
queue: Queue,
|
||||
quit_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Trekstor {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Trekstor {
|
||||
fn default() -> Self {
|
||||
let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer);
|
||||
|
||||
Self {
|
||||
window,
|
||||
queue: Arc::new(Mutex::new(Vec::new())),
|
||||
quit_flag: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Closure = Box<dyn FnOnce() + Send>;
|
||||
type Queue = Arc<Mutex<Vec<Closure>>>;
|
||||
|
||||
struct TrekstorEventProxy {
|
||||
queue: Queue,
|
||||
quit_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl EventLoopProxy for TrekstorEventProxy {
|
||||
fn quit_event_loop(&self) -> Result<(), EventLoopError> {
|
||||
self.quit_flag.store(true, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn invoke_from_event_loop(&self, event: Closure) -> Result<(), EventLoopError> {
|
||||
// Don't queue if the loop is shutting down, the closure would never
|
||||
// run, and any caller blocking on a channel send inside it would hang.
|
||||
if self.quit_flag.load(Ordering::SeqCst) {
|
||||
return Err(EventLoopError::EventLoopTerminated);
|
||||
}
|
||||
self.queue
|
||||
.lock()
|
||||
.expect("event loop closure queue poisoned")
|
||||
.push(event);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for Trekstor {
|
||||
fn create_window_adapter(
|
||||
&self,
|
||||
) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
|
||||
Ok(self.window.clone())
|
||||
}
|
||||
|
||||
fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
|
||||
let mut frame_buffer = Framebuffer::open()
|
||||
.map_err(|e| PlatformError::Other(format!("failed to open /dev/fb0: {e}")))?;
|
||||
|
||||
let mut input = InputState::new(frame_buffer.width, frame_buffer.height);
|
||||
|
||||
#[allow(clippy::large_stack_arrays)] // causes stack overflow if rgba8
|
||||
let mut buffer1 = [Rgb565Pixel(0); 614_400];
|
||||
|
||||
self.window.set_size(WindowSize::Physical(PhysicalSize::new(
|
||||
frame_buffer.width,
|
||||
frame_buffer.height,
|
||||
)));
|
||||
|
||||
loop {
|
||||
input.handle_event();
|
||||
|
||||
slint::platform::update_timers_and_animations();
|
||||
|
||||
if input.is_just_pressed(KeyCode::BTN_TOUCH) {
|
||||
self.window
|
||||
.try_dispatch_event(WindowEvent::PointerPressed {
|
||||
position: LogicalPosition::new(input.x as f32, input.y as f32),
|
||||
button: slint::platform::PointerEventButton::Left,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if input.is_just_released(KeyCode::BTN_TOUCH) {
|
||||
self.window
|
||||
.try_dispatch_event(WindowEvent::PointerReleased {
|
||||
position: LogicalPosition::new(input.x as f32, input.y as f32),
|
||||
button: slint::platform::PointerEventButton::Left,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if input.is_down(KeyCode::BTN_TOUCH) {
|
||||
self.window
|
||||
.try_dispatch_event(WindowEvent::PointerMoved {
|
||||
position: LogicalPosition::new(input.x as f32, input.y as f32),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if self.quit_flag.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.window.draw_if_needed(|renderer| {
|
||||
renderer.render(&mut buffer1, frame_buffer.width as usize);
|
||||
frame_buffer.write_buffer(&buffer1);
|
||||
});
|
||||
|
||||
input.finish();
|
||||
|
||||
let pending: Vec<_> = self
|
||||
.queue
|
||||
.lock()
|
||||
.expect("event loop closure queue poisoned")
|
||||
.drain(..)
|
||||
.collect();
|
||||
for c in pending {
|
||||
c();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
|
||||
Some(Box::new(TrekstorEventProxy {
|
||||
queue: self.queue.clone(),
|
||||
quit_flag: self.quit_flag.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user