Init
This commit is contained in:
commit
76b43a0386
38 changed files with 5003 additions and 0 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[target.armv7-unknown-linux-musleabihf]
|
||||
rustflags = ["-C", "target-cpu=cortex-a8", "-C", "target-feature=-crt-static"]
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
3204
Cargo.lock
generated
Normal file
3204
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 = "2"
|
||||
members = ["kdash_client", "kdash_protocol", "kdash_server"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
2
Cross.toml
Normal file
2
Cross.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[build]
|
||||
default-target = "armv7-unknown-linux-musleabihf"
|
29
Dockerfile
Normal file
29
Dockerfile
Normal file
|
@ -0,0 +1,29 @@
|
|||
ARG RUST_VERSION="1.82.0"
|
||||
FROM git.dergrimm.net/dergrimm/muslrust:${RUST_VERSION} AS chef
|
||||
|
||||
FROM chef AS planner
|
||||
WORKDIR /usr/src
|
||||
RUN mkdir -p kdash_client/src kdash_protocol/src kdash_server/src && touch kdash_client/src/main.rs kdash_protocol/src/lib.rs kdash_server/src/main.rs
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY kdash_client/Cargo.toml kdash_client/Cargo.toml
|
||||
COPY kdash_protocol/Cargo.toml kdash_protocol/Cargo.toml
|
||||
COPY kdash_server/Cargo.toml kdash_server/Cargo.toml
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
WORKDIR /usr/src
|
||||
RUN mkdir bin/
|
||||
COPY .cargo .cargo
|
||||
COPY --from=planner /usr/src/recipe.json .
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
COPY kdash_client kdash_client
|
||||
COPY kdash_protocol kdash_protocol
|
||||
COPY kdash_server kdash_server
|
||||
RUN cargo build --package kdash_server --release && mv target/x86_64-unknown-linux-musl/release/kdash_server bin/kdash_server
|
||||
|
||||
FROM docker.io/alpine:3
|
||||
WORKDIR /usr/src
|
||||
COPY --from=builder /usr/src/bin/kdash_server .
|
||||
ENTRYPOINT [ "/usr/src/kdash_server" ]
|
||||
EXPOSE 8080
|
12
README.md
Normal file
12
README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# kdash
|
||||
|
||||
kdash is a dashboard management system for jailbroken Kindles. It consists of two main components:
|
||||
|
||||
- **`kdash_client`**: Runs on the kindle, polls config and the current dashboard image
|
||||
- **`kdash_server`**: Manages where to get the dashboard image and the Kindle device config
|
||||
|
||||
It is currently only tested against the Kindle 4 NT (ARM Cortex A8).
|
||||
|
||||
<hr>
|
||||
|
||||
*It works™*
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
services:
|
||||
kdash:
|
||||
image: git.dergrimm.net/dergrimm/kdash:latest
|
||||
build: .
|
||||
restart: always
|
||||
command: --config /config/config.json --private-key /config/private.key --public-key /config/public.crt start
|
||||
environment:
|
||||
- RUST_BACKTRACE=full
|
||||
- RUST_LOG=info
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./config/config.json:/config/config.json:ro
|
||||
- ./config/private.key:/config/private.key:ro
|
||||
- ./config/public.crt:/config/public.crt:ro
|
1
kdash_client/.gitignore
vendored
Normal file
1
kdash_client/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
31
kdash_client/Cargo.toml
Normal file
31
kdash_client/Cargo.toml
Normal file
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "kdash_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.92", features = ["backtrace"] }
|
||||
chrono = "0.4.38"
|
||||
embedded-graphics = "0.8.1"
|
||||
embedded-layout = "0.4.1"
|
||||
env_logger = "0.11.5"
|
||||
envconfig = "0.11.0"
|
||||
framebuffer = "0.3.1"
|
||||
futures = "0.3.31"
|
||||
image = { version = "0.25.5", default-features = false, features = [
|
||||
"png",
|
||||
"bmp",
|
||||
] }
|
||||
kdash_protocol = { path = "../kdash_protocol" }
|
||||
lazy_static = "1.5.0"
|
||||
log = "0.4.22"
|
||||
num-traits = "0.2.19"
|
||||
openlipc_dyn = { git = "https://git.dergrimm.net/dergrimm/openlipc_dyn.git" }
|
||||
reqwest = { version = "0.12.9", default-features = false, features = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
"stream",
|
||||
] }
|
||||
tinybmp = "0.6.0"
|
||||
tokio = { version = "1.41.0", features = ["full"] }
|
||||
url = "2.5.3"
|
67
kdash_client/assets/error.svg
Normal file
67
kdash_client/assets/error.svg
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="300"
|
||||
height="300"
|
||||
viewBox="0 0 79.375 79.375"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
|
||||
sodipodi:docname="error.svg"
|
||||
inkscape:export-filename="error_low_battery.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="0.72515432"
|
||||
inkscape:cx="396.46733"
|
||||
inkscape:cy="339.23814"
|
||||
inkscape:window-width="1904"
|
||||
inkscape:window-height="996"
|
||||
inkscape:window-x="1928"
|
||||
inkscape:window-y="396"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:export-bgcolor="#ffffffff" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g1"
|
||||
inkscape:label="network"
|
||||
style="display:none"
|
||||
transform="scale(0.5)">
|
||||
<path
|
||||
d="m 79.374996,119.0625 q 3.373446,0 5.655472,-2.28204 2.282025,-2.28202 2.282025,-5.65547 0,-3.37344 -2.282025,-5.65546 -2.282026,-2.28204 -5.655472,-2.28204 -3.373436,0 -5.655468,2.28204 -2.282031,2.28202 -2.282031,5.65546 0,3.37345 2.282031,5.65547 2.282032,2.28204 5.655468,2.28204 z M 71.437497,87.312495 H 87.312493 V 39.687499 H 71.437497 Z m 7.937499,71.437495 q -16.470308,0 -30.956247,-6.25078 -14.485938,-6.25078 -25.20156,-16.9664 Q 12.501564,124.81718 6.2507824,110.33125 0,95.84531 0,79.374996 0,62.904684 6.2507824,48.418747 12.501564,33.932811 23.217189,23.217186 33.932811,12.501561 48.418749,6.2507785 62.904688,0 79.374996,0 q 16.470315,0 30.956244,6.2507785 14.48594,6.2507825 25.20158,16.9664075 10.71561,10.715625 16.96638,25.201561 6.2508,14.485937 6.2508,30.956249 0,16.470314 -6.2508,30.956254 -6.25077,14.48593 -16.96638,25.20156 -10.71564,10.71562 -25.20158,16.9664 -14.485929,6.25078 -30.956244,6.25078 z m 0,-15.875 q 26.590634,0 45.045314,-18.45468 18.45468,-18.45469 18.45468,-45.045314 0,-26.590625 -18.45468,-45.045311 -18.45468,-18.454686 -45.045314,-18.454686 -26.590623,0 -45.045309,18.454686 -18.454686,18.454686 -18.454686,45.045311 0,26.590624 18.454686,45.045314 18.454686,18.45468 45.045309,18.45468 z m 0,-63.499994 z"
|
||||
id="path1"
|
||||
style="display:inline;fill:#000000;stroke-width:0.198437"
|
||||
inkscape:label="path1" />
|
||||
</g>
|
||||
<g
|
||||
id="g2"
|
||||
inkscape:label="battery"
|
||||
style="display:inline"
|
||||
transform="scale(0.5)">
|
||||
<path
|
||||
d="m 23.812503,119.0625 q -3.37344,0 -5.655481,-2.28203 -2.28204,-2.28203 -2.28204,-5.65547 V 95.25 H 0 V 63.499997 h 15.874982 v -15.875 q 0,-3.373437 2.28204,-5.655469 2.282041,-2.282031 5.655481,-2.282031 H 150.81251 q 3.37344,0 5.65545,2.282031 2.28204,2.282032 2.28204,5.655469 V 111.125 q 0,3.37344 -2.28204,5.65547 -2.28201,2.28203 -5.65545,2.28203 z m 7.937491,-15.875 H 119.06248 V 55.562497 H 31.749994 Z"
|
||||
id="path1-5"
|
||||
style="display:inline;fill:#000000;stroke-width:0.198437" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
67
kdash_client/daemon.sh
Normal file
67
kdash_client/daemon.sh
Normal file
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
DAEMON_PATH="/mnt/us/kdash"
|
||||
|
||||
DAEMON_ENV_FILE="${DAEMON_PATH}/kdash.env"
|
||||
|
||||
DAEMON="./kdash_client"
|
||||
DAEMONOPTS=""
|
||||
|
||||
NAME="kdash"
|
||||
# DESC="kdash client daemon"
|
||||
PIDFILE="${DAEMON_PATH}/${DAEMON}.pid"
|
||||
# SCRIPTNAME="/etc/init.d/${NAME}"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
printf "%-50s" "Starting $NAME..."
|
||||
cd "$DAEMON_PATH" || exit
|
||||
. "$DAEMON_ENV_FILE"
|
||||
PID=$(
|
||||
RUST_BACKTRACE=full RUST_LOG=debug $DAEMON "$DAEMONOPTS" >/dev/null 2>&1 &
|
||||
echo $!
|
||||
)
|
||||
#echo "Saving PID" $PID " to " $PIDFILE
|
||||
if [ -z "$PID" ]; then
|
||||
printf "%s\n" "Fail"
|
||||
else
|
||||
echo "$PID" >"$PIDFILE"
|
||||
printf "%s\n" "Ok"
|
||||
fi
|
||||
;;
|
||||
status)
|
||||
printf "%-50s" "Checking $NAME..."
|
||||
if [ -f $PIDFILE ]; then
|
||||
PID=$(cat $PIDFILE)
|
||||
if [ -z "$(ps axf | grep "${PID}" | grep -v grep)" ]; then
|
||||
printf "%s\n" "Process dead but pidfile exists"
|
||||
else
|
||||
echo "Running"
|
||||
fi
|
||||
else
|
||||
printf "%s\n" "Service not running"
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
printf "%-50s" "Stopping $NAME"
|
||||
PID=$(cat $PIDFILE)
|
||||
cd $DAEMON_PATH || exit
|
||||
if [ -f $PIDFILE ]; then
|
||||
kill -HUP "$PID"
|
||||
printf "%s\n" "Ok"
|
||||
rm -f $PIDFILE
|
||||
else
|
||||
printf "%s\n" "pidfile not found"
|
||||
fi
|
||||
;;
|
||||
|
||||
restart)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {status|start|stop|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
81
kdash_client/src/api.rs
Normal file
81
kdash_client/src/api.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use anyhow::{bail, Result};
|
||||
use kdash_protocol::Orientation;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Api {
|
||||
pub jwt: String,
|
||||
pub config_url: Url,
|
||||
pub image_url: Url,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub fn new(jwt: String, url: Url) -> Result<Self> {
|
||||
Ok(Self {
|
||||
jwt,
|
||||
config_url: url.join("config")?,
|
||||
image_url: url.join("image")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn authorization_bearer(bearer: &str) -> String {
|
||||
format!("Bearer {}", bearer)
|
||||
}
|
||||
|
||||
pub async fn fetch_config(&self) -> Result<kdash_protocol::Config, reqwest::Error> {
|
||||
let config = reqwest::Client::new()
|
||||
.get(self.config_url.to_owned())
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
Self::authorization_bearer(&self.jwt),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.json::<kdash_protocol::Config>()
|
||||
.await?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn fetch_image<'a>(
|
||||
&self,
|
||||
size: (u32, u32),
|
||||
orientation: Orientation,
|
||||
) -> Result<image::ImageBuffer<image::Luma<u8>, Vec<u8>>> {
|
||||
let response = reqwest::Client::new()
|
||||
.get(self.image_url.to_owned())
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
Self::authorization_bearer(&self.jwt),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let content_type = match response.headers().get(reqwest::header::CONTENT_TYPE) {
|
||||
Some(value) => value.to_str()?,
|
||||
None => bail!("Image response returned no Content-Type header"),
|
||||
};
|
||||
|
||||
let image_format = match image::ImageFormat::from_mime_type(content_type) {
|
||||
Some(x) => x,
|
||||
None => bail!("Invalid image MIME type: {}", content_type),
|
||||
};
|
||||
|
||||
if image_format != image::ImageFormat::Png && image_format != image::ImageFormat::Bmp {
|
||||
bail!("Invalid image MIME type: {}", image_format.to_mime_type())
|
||||
}
|
||||
|
||||
let data = response.bytes().await?;
|
||||
let reader = image::ImageReader::with_format(std::io::Cursor::new(data), image_format);
|
||||
|
||||
let mut img = reader.decode()?.crop_imm(0, 0, size.0, size.1);
|
||||
match orientation {
|
||||
Orientation::PortraitUp | Orientation::PortraitDown => {}
|
||||
Orientation::LandscapeLeft | Orientation::LandscapeRight => {
|
||||
img = img.rotate270();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(img.into_luma8())
|
||||
}
|
||||
}
|
24
kdash_client/src/app.rs
Normal file
24
kdash_client/src/app.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use std::{net::IpAddr, path::PathBuf, rc::Rc, sync::Mutex};
|
||||
|
||||
use openlipc_dyn::Lipc;
|
||||
|
||||
use crate::api;
|
||||
|
||||
pub struct State {
|
||||
pub api: api::Api,
|
||||
pub app_config: AppConfig,
|
||||
pub config: Rc<Mutex<kdash_protocol::Config>>,
|
||||
pub lipc: Rc<Mutex<Lipc>>,
|
||||
}
|
||||
|
||||
pub struct AppConfig {
|
||||
pub net: String,
|
||||
pub router_ip: IpAddr,
|
||||
pub assets_path: PathBuf,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn asset(&self, path: &str) -> PathBuf {
|
||||
self.assets_path.join(path)
|
||||
}
|
||||
}
|
13
kdash_client/src/assets.rs
Normal file
13
kdash_client/src/assets.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use embedded_graphics::pixelcolor::Gray8;
|
||||
use lazy_static::lazy_static;
|
||||
use tinybmp::Bmp;
|
||||
|
||||
const ERROR_NETWORK_IMAGE_BUF: &[u8] = include_bytes!("assets/error_network.bmp");
|
||||
const ERROR_BATTERY_LOW_IMAGE_BUF: &[u8] = include_bytes!("assets/error_battery_low.bmp");
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ERROR_NETWORK_IMAGE: Bmp<'static, Gray8> =
|
||||
Bmp::from_slice(ERROR_NETWORK_IMAGE_BUF).unwrap();
|
||||
pub static ref ERROR_BATTERY_LOW_IMAGE: Bmp<'static, Gray8> =
|
||||
Bmp::from_slice(ERROR_BATTERY_LOW_IMAGE_BUF).unwrap();
|
||||
}
|
BIN
kdash_client/src/assets/error_battery_low.bmp
Normal file
BIN
kdash_client/src/assets/error_battery_low.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
kdash_client/src/assets/error_network.bmp
Normal file
BIN
kdash_client/src/assets/error_network.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
59
kdash_client/src/battery.rs
Normal file
59
kdash_client/src/battery.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use anyhow::Result;
|
||||
use std::fs;
|
||||
|
||||
use crate::{parse, utils};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BatteryStatus {
|
||||
pub is_charging: bool,
|
||||
pub percentage: u8,
|
||||
pub current: i16,
|
||||
}
|
||||
|
||||
pub fn get_battery_status() -> Result<BatteryStatus> {
|
||||
log::info!("Getting battery status");
|
||||
|
||||
let charging_file = utils::kdb_get("system/driver/charger/SYS_CHARGING_FILE")?;
|
||||
let is_charging_str = fs::read_to_string(charging_file)?;
|
||||
let is_charging = parse::parse_number_from_start_unsigned::<u8>(&is_charging_str)? == 1;
|
||||
|
||||
let charge_str = utils::exec_command("gasgauge-info", &["-c"])?;
|
||||
let charge = parse::parse_number_from_start_unsigned::<u8>(&charge_str)?;
|
||||
|
||||
let charge_current_str = utils::exec_command("gasgauge-info", &["-l"])?;
|
||||
let charge_current = parse::parse_number_from_start_signed::<i16>(&charge_current_str)?;
|
||||
|
||||
let battery = BatteryStatus {
|
||||
is_charging,
|
||||
percentage: charge,
|
||||
current: charge_current,
|
||||
};
|
||||
|
||||
log::info!("Got battery status: {:?}", battery);
|
||||
|
||||
Ok(battery)
|
||||
}
|
||||
|
||||
pub fn restart_powerd() -> Result<()> {
|
||||
log::info!("Restarting powerd");
|
||||
utils::exec_command_discard("/etc/init.d/powerd", &["restart"])
|
||||
}
|
||||
|
||||
pub fn restart_powerd_config_condition(
|
||||
battery: &BatteryStatus,
|
||||
battery_config: &kdash_protocol::BatteryConfig,
|
||||
) -> Result<()> {
|
||||
if battery.is_charging
|
||||
&& battery.percentage <= *battery_config.restart_powerd_threshold
|
||||
&& battery.current <= 0
|
||||
{
|
||||
log::info!(
|
||||
"Battery charge below threshold ({} <= {}): restarting powerd",
|
||||
battery.percentage,
|
||||
*battery_config.restart_powerd_threshold
|
||||
);
|
||||
restart_powerd()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
21
kdash_client/src/config.rs
Normal file
21
kdash_client/src/config.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use envconfig::Envconfig;
|
||||
use std::{net, path::PathBuf};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Envconfig, Debug)]
|
||||
pub struct Config {
|
||||
#[envconfig(from = "KDASH_URL")]
|
||||
pub kdash_url: Url,
|
||||
|
||||
#[envconfig(from = "KDASH_JWT")]
|
||||
pub kdash_jwt: String,
|
||||
|
||||
#[envconfig(from = "ROUTER_IP")]
|
||||
pub router_ip: net::IpAddr,
|
||||
|
||||
#[envconfig(from = "NET")]
|
||||
pub net: String,
|
||||
|
||||
#[envconfig(from = "ASSETS")]
|
||||
pub assets: PathBuf,
|
||||
}
|
204
kdash_client/src/fb/mod.rs
Normal file
204
kdash_client/src/fb/mod.rs
Normal file
|
@ -0,0 +1,204 @@
|
|||
use anyhow::Result;
|
||||
use embedded_graphics::{
|
||||
image::{ImageRaw, ImageRawBE},
|
||||
pixelcolor::Gray8,
|
||||
prelude::*,
|
||||
primitives::Rectangle,
|
||||
};
|
||||
use embedded_layout::View;
|
||||
use std::{fs, io::Write, ops::Range};
|
||||
|
||||
use kdash_protocol::Orientation;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
pub mod widgets;
|
||||
|
||||
pub fn eips_clear() -> Result<()> {
|
||||
utils::exec_command_discard("eips", &["-c"])
|
||||
}
|
||||
|
||||
pub fn image_buf_to_raw<'a>(
|
||||
buf: &'a image::ImageBuffer<image::Luma<u8>, Vec<u8>>,
|
||||
) -> ImageRawBE<'a, Gray8> {
|
||||
ImageRaw::new(buf.as_raw(), buf.dimensions().0)
|
||||
}
|
||||
|
||||
pub const DEFAULT_FB: &str = "/dev/fb0";
|
||||
const EINK_FB_UPDATE_DISPLAY: &str = "/proc/eink_fb/update_display";
|
||||
|
||||
type FbIdxFn = fn(width: usize, height: usize, x: usize, y: usize) -> usize;
|
||||
|
||||
const fn fb_idx_portrait_up(width: usize, _height: usize, x: usize, y: usize) -> usize {
|
||||
(y * width) + x
|
||||
}
|
||||
|
||||
const fn fb_idx_portrait_down(width: usize, height: usize, x: usize, y: usize) -> usize {
|
||||
((height - 1 - y) * width) + (width - 1 - x)
|
||||
}
|
||||
|
||||
const fn fb_idx_landscape_left(width: usize, height: usize, x: usize, y: usize) -> usize {
|
||||
((height - 1 - x) * width) + y
|
||||
}
|
||||
|
||||
const fn fb_idx_landscape_right(width: usize, _height: usize, x: usize, y: usize) -> usize {
|
||||
((x + 1) * width) - 1 - y
|
||||
}
|
||||
|
||||
const FB_IDX_PORTRAIT_UP_FN: FbIdxFn = fb_idx_portrait_up;
|
||||
const FB_IDX_PORTRAIT_DOWN_FN: FbIdxFn = fb_idx_portrait_down;
|
||||
const FB_IDX_LANDSCAPE_LEFT_FN: FbIdxFn = fb_idx_landscape_left;
|
||||
const FB_IDX_LANDSCAPE_RIGHT_FN: FbIdxFn = fb_idx_landscape_right;
|
||||
|
||||
pub struct FramebufferOrientation {
|
||||
pub orientation: Orientation,
|
||||
pub virtual_x: u32,
|
||||
pub virtual_y: u32,
|
||||
range_x: Range<i32>,
|
||||
range_y: Range<i32>,
|
||||
fb_idx_fn: FbIdxFn,
|
||||
}
|
||||
|
||||
impl FramebufferOrientation {
|
||||
pub fn new(fb: &framebuffer::Framebuffer, orientation: Orientation) -> Result<Self> {
|
||||
Ok(match orientation {
|
||||
Orientation::PortraitUp => Self {
|
||||
orientation,
|
||||
virtual_x: fb.var_screen_info.xres_virtual,
|
||||
virtual_y: fb.var_screen_info.yres_virtual,
|
||||
range_x: 0..i32::try_from(fb.var_screen_info.xres_virtual)?,
|
||||
range_y: 0..i32::try_from(fb.var_screen_info.yres_virtual)?,
|
||||
fb_idx_fn: FB_IDX_PORTRAIT_UP_FN,
|
||||
},
|
||||
Orientation::PortraitDown => Self {
|
||||
orientation,
|
||||
virtual_x: fb.var_screen_info.xres_virtual,
|
||||
virtual_y: fb.var_screen_info.yres_virtual,
|
||||
range_x: 0..i32::try_from(fb.var_screen_info.xres_virtual)?,
|
||||
range_y: 0..i32::try_from(fb.var_screen_info.yres_virtual)?,
|
||||
fb_idx_fn: FB_IDX_PORTRAIT_DOWN_FN,
|
||||
},
|
||||
Orientation::LandscapeLeft => Self {
|
||||
orientation,
|
||||
virtual_x: fb.var_screen_info.yres_virtual,
|
||||
virtual_y: fb.var_screen_info.xres_virtual,
|
||||
range_x: 0..i32::try_from(fb.var_screen_info.yres_virtual)?,
|
||||
range_y: 0..i32::try_from(fb.var_screen_info.xres_virtual)?,
|
||||
fb_idx_fn: FB_IDX_LANDSCAPE_LEFT_FN,
|
||||
},
|
||||
Orientation::LandscapeRight => Self {
|
||||
orientation,
|
||||
virtual_x: fb.var_screen_info.yres_virtual,
|
||||
virtual_y: fb.var_screen_info.xres_virtual,
|
||||
range_x: 0..i32::try_from(fb.var_screen_info.yres_virtual)?,
|
||||
range_y: 0..i32::try_from(fb.var_screen_info.xres_virtual)?,
|
||||
fb_idx_fn: FB_IDX_LANDSCAPE_RIGHT_FN,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FramebufferDisplay {
|
||||
pub fb_orientation: FramebufferOrientation,
|
||||
pub fb: framebuffer::Framebuffer,
|
||||
update_display_file: fs::File,
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl FramebufferDisplay {
|
||||
pub fn new(fb_path: &str, orientation: Orientation) -> Result<Self> {
|
||||
let fb = framebuffer::Framebuffer::new(fb_path)?;
|
||||
let update_display_file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(EINK_FB_UPDATE_DISPLAY)?;
|
||||
let fb_orientation = FramebufferOrientation::new(&fb, orientation)?;
|
||||
|
||||
Ok(Self {
|
||||
buf: vec![
|
||||
0u8;
|
||||
(fb.var_screen_info.xres_virtual as usize)
|
||||
* (fb.var_screen_info.yres_virtual as usize)
|
||||
],
|
||||
fb,
|
||||
fb_orientation,
|
||||
update_display_file,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_orientation(&mut self, orientation: Orientation) -> Result<()> {
|
||||
self.fb_orientation = FramebufferOrientation::new(&self.fb, orientation)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn luma_to_epd_color(color: Gray8) -> u8 {
|
||||
0xff - color.luma()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.buf.fill(0);
|
||||
}
|
||||
|
||||
pub fn flush(&mut self, update_display_buf: &[u8]) -> Result<()> {
|
||||
self.fb.write_frame(&self.buf);
|
||||
self.update_display_file.write_all(update_display_buf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn flush_partial_update(&mut self) -> Result<()> {
|
||||
self.flush(b"1")
|
||||
}
|
||||
|
||||
pub fn flush_full_update(&mut self) -> Result<()> {
|
||||
self.flush(b"2")
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawTarget for FramebufferDisplay {
|
||||
type Color = Gray8;
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn draw_iter<I>(&mut self, pixels: I) -> std::result::Result<(), Self::Error>
|
||||
where
|
||||
I: IntoIterator<Item = Pixel<Self::Color>>,
|
||||
{
|
||||
for Pixel(coord, color) in pixels.into_iter() {
|
||||
if self.fb_orientation.range_x.contains(&coord.x)
|
||||
&& self.fb_orientation.range_y.contains(&coord.y)
|
||||
{
|
||||
let idx = (self.fb_orientation.fb_idx_fn)(
|
||||
self.fb.var_screen_info.xres_virtual as usize,
|
||||
self.fb.var_screen_info.yres_virtual as usize,
|
||||
coord.x as usize,
|
||||
coord.y as usize,
|
||||
);
|
||||
self.buf[idx] = Self::luma_to_epd_color(color);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self, color: Self::Color) -> std::result::Result<(), Self::Error> {
|
||||
self.buf.fill(Self::luma_to_epd_color(color));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl OriginDimensions for FramebufferDisplay {
|
||||
fn size(&self) -> Size {
|
||||
Size::new(
|
||||
self.fb_orientation.virtual_x as u32,
|
||||
self.fb_orientation.virtual_y as u32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl View for FramebufferDisplay {
|
||||
fn bounds(&self) -> Rectangle {
|
||||
Rectangle::new(Point::zero(), OriginDimensions::size(self))
|
||||
}
|
||||
|
||||
fn translate_impl(&mut self, _by: Point) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
2
kdash_client/src/fb/widgets/mod.rs
Normal file
2
kdash_client/src/fb/widgets/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod padding;
|
||||
pub mod status_box;
|
79
kdash_client/src/fb/widgets/padding.rs
Normal file
79
kdash_client/src/fb/widgets/padding.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use embedded_graphics::{
|
||||
prelude::*,
|
||||
primitives::{PrimitiveStyleBuilder, Rectangle},
|
||||
};
|
||||
use embedded_layout::prelude::*;
|
||||
|
||||
pub struct Paddding<T, C>
|
||||
where
|
||||
T: Drawable<Color = C> + View,
|
||||
C: PixelColor,
|
||||
{
|
||||
pub bounds: Rectangle,
|
||||
pub color: Option<C>,
|
||||
pub widget: T,
|
||||
}
|
||||
|
||||
impl<T, C> Paddding<T, C>
|
||||
where
|
||||
T: Drawable<Color = C> + View,
|
||||
C: PixelColor,
|
||||
{
|
||||
pub fn new(top_left: Point, padding: Size, color: Option<C>, widget: T) -> Self {
|
||||
let bounds = Rectangle::new(
|
||||
top_left,
|
||||
Size::new(
|
||||
widget.size().width + (2 * padding.width),
|
||||
widget.size().height + (2 * padding.height),
|
||||
),
|
||||
);
|
||||
|
||||
Self {
|
||||
color,
|
||||
widget: widget.align_to(&bounds, horizontal::Center, vertical::Center),
|
||||
bounds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> View for Paddding<T, C>
|
||||
where
|
||||
T: Drawable<Color = C> + View,
|
||||
C: PixelColor,
|
||||
{
|
||||
fn bounds(&self) -> Rectangle {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
fn size(&self) -> Size {
|
||||
self.bounds.size
|
||||
}
|
||||
|
||||
fn translate_impl(&mut self, by: Point) {
|
||||
self.bounds.translate_impl(by);
|
||||
self.widget.translate_impl(by);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> Drawable for Paddding<T, C>
|
||||
where
|
||||
T: Drawable<Color = C> + View,
|
||||
C: PixelColor,
|
||||
{
|
||||
type Color = C;
|
||||
type Output = ();
|
||||
|
||||
fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
|
||||
where
|
||||
D: DrawTarget<Color = Self::Color>,
|
||||
{
|
||||
if let Some(color) = self.color {
|
||||
let style = PrimitiveStyleBuilder::new().fill_color(color).build();
|
||||
self.bounds.into_styled(style).draw(target)?;
|
||||
}
|
||||
|
||||
self.widget.draw(target)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
154
kdash_client/src/fb/widgets/status_box.rs
Normal file
154
kdash_client/src/fb/widgets/status_box.rs
Normal file
|
@ -0,0 +1,154 @@
|
|||
use embedded_graphics::{
|
||||
mono_font::{
|
||||
ascii::{FONT_10X20, FONT_6X12},
|
||||
MonoFont, MonoTextStyle,
|
||||
},
|
||||
pixelcolor::Gray8,
|
||||
prelude::*,
|
||||
primitives::{CornerRadiiBuilder, PrimitiveStyleBuilder, Rectangle, RoundedRectangle},
|
||||
text::Text,
|
||||
};
|
||||
use embedded_layout::prelude::*;
|
||||
|
||||
pub const DEFAULT_PADDING: Size = Size::new(20, 20);
|
||||
|
||||
pub struct StatusBox<'a> {
|
||||
pub text: &'a str,
|
||||
pub font: &'a MonoFont<'a>,
|
||||
pub bounds: Rectangle,
|
||||
pub bar_bounds: Rectangle,
|
||||
pub bar_text_bounds: Rectangle,
|
||||
pub bar_font: &'a MonoFont<'a>,
|
||||
pub bar_text: &'a str,
|
||||
pub inside_bounds: Rectangle,
|
||||
}
|
||||
|
||||
impl<'a> StatusBox<'a> {
|
||||
pub fn new(
|
||||
top_left: Point,
|
||||
padding: Size,
|
||||
border_stroke_width: u32,
|
||||
bar_font: &'a MonoFont,
|
||||
bar_text: &'a str,
|
||||
font: &'a MonoFont,
|
||||
text: &'a str,
|
||||
) -> Self {
|
||||
let text_width = (text.len() as u32) * (font.character_size.width + font.character_spacing);
|
||||
let text_height = font.character_size.width;
|
||||
|
||||
let bar_height = bar_font.character_size.height + (2 * border_stroke_width);
|
||||
|
||||
let bounds = Rectangle::new(
|
||||
top_left,
|
||||
Size::new(
|
||||
text_width + (2 * padding.width),
|
||||
text_height + (2 * padding.height) + bar_height,
|
||||
),
|
||||
);
|
||||
|
||||
Self {
|
||||
text,
|
||||
font,
|
||||
bounds,
|
||||
bar_bounds: Rectangle::new(bounds.top_left, Size::new(bounds.size.width, bar_height)),
|
||||
bar_text_bounds: Rectangle::new(
|
||||
Point::new(
|
||||
bounds.top_left.x + (border_stroke_width as i32),
|
||||
bounds.top_left.y + (border_stroke_width as i32),
|
||||
),
|
||||
Size::new(
|
||||
bounds.size.width - (2 * border_stroke_width),
|
||||
bar_height - (2 * border_stroke_width),
|
||||
),
|
||||
),
|
||||
bar_font,
|
||||
bar_text,
|
||||
inside_bounds: Rectangle::new(
|
||||
Point::new(top_left.x, top_left.y + (bar_height as i32)),
|
||||
Size::new(
|
||||
text_width + (2 * padding.width),
|
||||
text_height + (2 * padding.height),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_default_style(top_left: Point, bar_text: &'a str, text: &'a str) -> Self {
|
||||
Self::new(
|
||||
top_left,
|
||||
Size::new(30, 30),
|
||||
5,
|
||||
&FONT_6X12,
|
||||
bar_text,
|
||||
&FONT_10X20,
|
||||
text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl View for StatusBox<'_> {
|
||||
fn translate_impl(&mut self, by: Point) {
|
||||
Transform::translate_mut(&mut self.bounds, by);
|
||||
Transform::translate_mut(&mut self.bar_bounds, by);
|
||||
Transform::translate_mut(&mut self.bar_text_bounds, by);
|
||||
Transform::translate_mut(&mut self.inside_bounds, by);
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Rectangle {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
impl Drawable for StatusBox<'_> {
|
||||
type Color = Gray8;
|
||||
type Output = ();
|
||||
|
||||
fn draw<D>(&self, target: &mut D) -> std::result::Result<Self::Output, D::Error>
|
||||
where
|
||||
D: DrawTarget<Color = Self::Color>,
|
||||
{
|
||||
let corner_radius = Size::new(10, 10);
|
||||
|
||||
let rect_style = PrimitiveStyleBuilder::new()
|
||||
.stroke_width(5)
|
||||
.stroke_color(Gray8::BLACK)
|
||||
.fill_color(Gray8::WHITE)
|
||||
.build();
|
||||
|
||||
let rect = RoundedRectangle::with_equal_corners(self.bounds, corner_radius)
|
||||
.into_styled(rect_style);
|
||||
|
||||
let bar_style = PrimitiveStyleBuilder::new()
|
||||
.fill_color(Gray8::BLACK)
|
||||
.build();
|
||||
|
||||
let bar_radii = CornerRadiiBuilder::new()
|
||||
.top_left(corner_radius)
|
||||
.top_right(corner_radius)
|
||||
.bottom(Size::zero())
|
||||
.build();
|
||||
|
||||
let bar = RoundedRectangle::new(self.bar_bounds, bar_radii).into_styled(bar_style);
|
||||
|
||||
let bar_text = Text::new(
|
||||
self.bar_text,
|
||||
Point::zero(),
|
||||
MonoTextStyle::new(self.bar_font, Gray8::WHITE),
|
||||
)
|
||||
.align_to(&self.bar_text_bounds, horizontal::Right, vertical::Center);
|
||||
|
||||
let text = Text::new(
|
||||
self.text,
|
||||
Point::zero(),
|
||||
MonoTextStyle::new(self.font, Gray8::BLACK),
|
||||
)
|
||||
.align_to(&self.inside_bounds, horizontal::Center, vertical::Center);
|
||||
|
||||
rect.draw(target)?;
|
||||
bar.draw(target)?;
|
||||
bar_text.draw(target)?;
|
||||
text.draw(target)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
148
kdash_client/src/lib.rs
Normal file
148
kdash_client/src/lib.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use anyhow::Result;
|
||||
use chrono::prelude::*;
|
||||
use embedded_graphics::{image::Image, prelude::*};
|
||||
use embedded_layout::prelude::*;
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
pub mod api;
|
||||
pub mod app;
|
||||
pub mod assets;
|
||||
pub mod battery;
|
||||
pub mod config;
|
||||
pub mod fb;
|
||||
mod parse;
|
||||
pub mod rtc;
|
||||
pub mod utils;
|
||||
pub mod wifi;
|
||||
|
||||
pub async fn run_once(
|
||||
state: &app::State,
|
||||
display: &mut fb::FramebufferDisplay,
|
||||
) -> Result<Option<kdash_protocol::Config>> {
|
||||
let config = state.config.lock().unwrap();
|
||||
|
||||
utils::set_cpu_powersaving()?;
|
||||
utils::prevent_screensaver(&state.lipc.lock().unwrap())?;
|
||||
|
||||
let battery = battery::get_battery_status()?;
|
||||
battery::restart_powerd_config_condition(&battery, &config.battery)?;
|
||||
if battery.percentage <= *config.battery.low {
|
||||
log::info!(
|
||||
"Battery low: {}% <= {}%",
|
||||
battery.percentage,
|
||||
*config.battery.low
|
||||
);
|
||||
|
||||
Image::new(&*assets::ERROR_BATTERY_LOW_IMAGE, Point::zero())
|
||||
.align_to(&*display, horizontal::Center, vertical::Center)
|
||||
.draw(display)?;
|
||||
|
||||
fb::eips_clear()?;
|
||||
display.flush_full_update()?;
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
|
||||
rtc::sleep(config.time.delay_low_battery.to_std()?.as_secs())?;
|
||||
thread::sleep(Duration::from_secs(30));
|
||||
}
|
||||
|
||||
log::info!("Enabling and checking WiFi");
|
||||
wifi::set_wifid_enable(&state.lipc.lock().unwrap(), true)?;
|
||||
let wlan_connected =
|
||||
wifi::check_connection(&state.lipc.lock().unwrap(), &state.app_config.net)?;
|
||||
wifi::log_connection(&state.lipc.lock().unwrap(), &state.app_config.net)?;
|
||||
|
||||
let mut new_config: Option<kdash_protocol::Config> = None;
|
||||
|
||||
if wlan_connected {
|
||||
log::info!("Connected to WiFi");
|
||||
let gateway = wifi::get_gateway(&state.app_config.net)?;
|
||||
log::info!("Found default gateway: {:?}", gateway);
|
||||
|
||||
if gateway.is_none() {
|
||||
log::info!("Default gateway lost after sleep");
|
||||
log::info!("Setting default gateway to: {}", state.app_config.router_ip);
|
||||
wifi::route_add_default_gateway_router_ip(state.app_config.router_ip)?;
|
||||
}
|
||||
|
||||
log::info!("Fetching config");
|
||||
|
||||
let fetch_succ = match state.api.fetch_config().await {
|
||||
Ok(value) => {
|
||||
new_config = Some(value);
|
||||
|
||||
log::info!("Downloading and drawing image");
|
||||
|
||||
match state
|
||||
.api
|
||||
.fetch_image(
|
||||
(
|
||||
display.fb.var_screen_info.xres_virtual,
|
||||
display.fb.var_screen_info.yres_virtual,
|
||||
),
|
||||
display.fb_orientation.orientation,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(buf) => {
|
||||
let raw = fb::image_buf_to_raw(&buf);
|
||||
Image::new(&raw, Point::zero()).draw(display)?;
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Downloaded image with error: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
dbg!(&e);
|
||||
log::error!("Fetched config with error: {}", e);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !fetch_succ {
|
||||
Image::new(&*assets::ERROR_NETWORK_IMAGE, Point::zero())
|
||||
.align_to(&*display, horizontal::Center, vertical::Center)
|
||||
.draw(display)?;
|
||||
}
|
||||
|
||||
if battery.percentage <= *config.battery.alert {
|
||||
let now_str = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
let text = format!("Battery at {}%, please charge!", battery.percentage);
|
||||
let status_box = fb::widgets::status_box::StatusBox::new_with_default_style(
|
||||
Point::zero(),
|
||||
&now_str,
|
||||
&text,
|
||||
);
|
||||
let mut padding = fb::widgets::padding::Paddding::new(
|
||||
Point::zero(),
|
||||
fb::widgets::status_box::DEFAULT_PADDING,
|
||||
None,
|
||||
status_box,
|
||||
);
|
||||
|
||||
match config.device.status_box_location {
|
||||
kdash_protocol::Corner::TopLeft => {}
|
||||
kdash_protocol::Corner::TopRight => {
|
||||
padding.align_to_mut(display, horizontal::Right, vertical::Top);
|
||||
}
|
||||
kdash_protocol::Corner::BottomLeft => {
|
||||
padding.align_to_mut(display, horizontal::Left, vertical::Bottom);
|
||||
}
|
||||
kdash_protocol::Corner::BottomRight => {
|
||||
padding.align_to_mut(display, horizontal::Right, vertical::Bottom);
|
||||
}
|
||||
}
|
||||
|
||||
padding.draw(display)?;
|
||||
}
|
||||
|
||||
if config.device.full_refresh {
|
||||
fb::eips_clear()?;
|
||||
}
|
||||
display.flush_full_update()?;
|
||||
}
|
||||
|
||||
Ok(new_config)
|
||||
}
|
113
kdash_client/src/main.rs
Normal file
113
kdash_client/src/main.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
use anyhow::{bail, Result};
|
||||
use chrono::prelude::*;
|
||||
use envconfig::Envconfig;
|
||||
use openlipc_dyn::Lipc;
|
||||
use std::{rc::Rc, sync::Mutex, thread, time};
|
||||
|
||||
use kdash_client;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let env_config = kdash_client::config::Config::init_from_env().unwrap();
|
||||
|
||||
let api = kdash_client::api::Api::new(env_config.kdash_jwt, env_config.kdash_url)?;
|
||||
let config = api.fetch_config().await?;
|
||||
|
||||
let lipc = Lipc::load(None)?;
|
||||
|
||||
let state = kdash_client::app::State {
|
||||
api,
|
||||
app_config: kdash_client::app::AppConfig {
|
||||
net: env_config.net,
|
||||
router_ip: env_config.router_ip,
|
||||
assets_path: env_config.assets,
|
||||
},
|
||||
config: Rc::new(Mutex::new(config)),
|
||||
lipc: Rc::new(Mutex::new(lipc)),
|
||||
};
|
||||
|
||||
let mut display = kdash_client::fb::FramebufferDisplay::new(
|
||||
kdash_client::fb::DEFAULT_FB,
|
||||
state.config.lock().unwrap().device.orientation,
|
||||
)?;
|
||||
|
||||
kdash_client::utils::kill_kindle()?;
|
||||
|
||||
let mut error_count: usize = 0;
|
||||
loop {
|
||||
display.clear();
|
||||
|
||||
let mut error_suspend = false;
|
||||
|
||||
match kdash_client::run_once(&state, &mut display).await {
|
||||
Ok(Some(new_config)) => {
|
||||
log::info!("Updating config");
|
||||
let mut config = state.config.lock().unwrap();
|
||||
if display.fb_orientation.orientation != new_config.device.orientation {
|
||||
log::info!(
|
||||
"Updating orientation: {:?} -> {:?}",
|
||||
display.fb_orientation.orientation,
|
||||
new_config.device.orientation
|
||||
);
|
||||
display.set_orientation(config.device.orientation)?;
|
||||
}
|
||||
*config = new_config;
|
||||
}
|
||||
Ok(None) => {
|
||||
log::info!("Did not fetch config");
|
||||
error_suspend = true;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error during main loop: {}", e);
|
||||
error_suspend = true;
|
||||
}
|
||||
}
|
||||
|
||||
let config = state.config.lock().unwrap();
|
||||
|
||||
thread::sleep(config.time.delay_before_suspend.to_std()?);
|
||||
|
||||
log::info!("Calculate next timer and going to sleep");
|
||||
|
||||
if error_suspend {
|
||||
error_count += 1;
|
||||
|
||||
log::error!("An error has occured",);
|
||||
|
||||
if error_count >= 10 {
|
||||
log::info!("Reboot because of {} errors", error_count);
|
||||
kdash_client::utils::exec_command_discard("reboot", &[])?;
|
||||
bail!("Rebooting");
|
||||
}
|
||||
|
||||
if config.device.use_rtc {
|
||||
kdash_client::rtc::sleep(config.time.delay_on_error.to_std()?.as_secs())?;
|
||||
} else {
|
||||
thread::sleep(config.time.delay_on_error.to_std()?);
|
||||
}
|
||||
} else {
|
||||
error_count = 0;
|
||||
|
||||
log::info!("Success");
|
||||
|
||||
let upcoming = config
|
||||
.time
|
||||
.update_schedule
|
||||
.upcoming(config.time.timezone)
|
||||
.next();
|
||||
|
||||
let dur = match upcoming {
|
||||
Some(dt) => (dt.to_utc().naive_utc() - Utc::now().naive_utc()).to_std()?,
|
||||
None => time::Duration::from_secs(60),
|
||||
};
|
||||
|
||||
if config.device.use_rtc {
|
||||
kdash_client::rtc::sleep(dur.as_secs())?;
|
||||
} else {
|
||||
thread::sleep(dur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
kdash_client/src/parse.rs
Normal file
33
kdash_client/src/parse.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
pub fn parse_number_from_start<T>(input: &str, signed: bool) -> Result<T, T::Err>
|
||||
where
|
||||
T: num_traits::PrimInt + std::str::FromStr,
|
||||
<T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let end_number_idx = input
|
||||
.chars()
|
||||
.position(if signed {
|
||||
|c: char| !c.is_ascii_digit() && c != '-'
|
||||
} else {
|
||||
|c: char| !c.is_ascii_digit()
|
||||
})
|
||||
.unwrap_or(input.len());
|
||||
let res = input[..end_number_idx].parse::<T>()?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn parse_number_from_start_unsigned<T>(input: &str) -> Result<T, T::Err>
|
||||
where
|
||||
T: num_traits::PrimInt + num_traits::Unsigned + std::str::FromStr,
|
||||
<T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
parse_number_from_start(input, false)
|
||||
}
|
||||
|
||||
pub fn parse_number_from_start_signed<T>(input: &str) -> Result<T, T::Err>
|
||||
where
|
||||
T: num_traits::PrimInt + num_traits::Signed + std::str::FromStr,
|
||||
<T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
parse_number_from_start(input, true)
|
||||
}
|
22
kdash_client/src/rtc.rs
Normal file
22
kdash_client/src/rtc.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use std::{fs, io::Write};
|
||||
|
||||
use crate::parse;
|
||||
|
||||
const RTC: &str = "/sys/devices/platform/mxc_rtc.0/wakeup_enable";
|
||||
const POWER_STATE: &str = "/sys/power/state";
|
||||
|
||||
pub fn sleep(secs: u64) -> Result<()> {
|
||||
let content_str = fs::read_to_string(RTC)?;
|
||||
let content: u64 = parse::parse_number_from_start_unsigned(&content_str)?;
|
||||
|
||||
if content == 0 {
|
||||
let mut rtc_file = fs::OpenOptions::new().write(true).open(RTC)?;
|
||||
rtc_file.write_all(secs.to_string().as_bytes())?;
|
||||
}
|
||||
|
||||
let mut power_state_file = fs::OpenOptions::new().write(true).open(POWER_STATE)?;
|
||||
power_state_file.write_all(b"mem")?;
|
||||
|
||||
Ok(())
|
||||
}
|
90
kdash_client/src/utils.rs
Normal file
90
kdash_client/src/utils.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use anyhow::{bail, Result};
|
||||
use openlipc_dyn::Lipc;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::process;
|
||||
|
||||
pub fn exec_command_discard(cmd: &str, args: &[&str]) -> Result<()> {
|
||||
log::debug!(
|
||||
"Executing command (discarding output): {:?} {:?}",
|
||||
cmd,
|
||||
args
|
||||
);
|
||||
|
||||
let output = process::Command::new(cmd)
|
||||
.args(args)
|
||||
.stdout(process::Stdio::inherit())
|
||||
.stderr(process::Stdio::inherit())
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
bail!("Command threw error: {}", output.status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_command(cmd: &str, args: &[&str]) -> Result<String> {
|
||||
log::debug!("Executing command: {:?} {:?}", cmd, args);
|
||||
|
||||
let output = process::Command::new(cmd).args(args).output()?;
|
||||
if !output.status.success() {
|
||||
bail!("Command threw error: {}", output.status);
|
||||
}
|
||||
|
||||
let s = String::from_utf8(output.stdout)?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn kdb_get(name: &str) -> Result<String> {
|
||||
log::debug!("kdb get: {:?}", name);
|
||||
|
||||
let mut s = exec_command("kdb", &["get", name])?;
|
||||
if s.ends_with('\n') {
|
||||
s = s.trim_end_matches('\n').to_string();
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
const KILL_KINDLE_COMMANDS: &[(&str, &[&str])] = &[
|
||||
("/etc/init.d/framework", &["stop"]),
|
||||
("/etc/init.d/cmd", &["stop"]),
|
||||
("/etc/init.d/phd", &["stop"]),
|
||||
("/etc/init.d/volumd", &["stop"]),
|
||||
("/etc/init.d/tmd", &["stop"]),
|
||||
("/etc/init.d/webreaderd", &["stop"]),
|
||||
];
|
||||
|
||||
const KILL_KINDLE_KILLALL: (&str, &[&str]) = ("killall", &["-q", "lipc-wait-event"]);
|
||||
|
||||
pub fn kill_kindle() -> Result<()> {
|
||||
log::info!("Killing Kindle");
|
||||
|
||||
for (cmd, args) in KILL_KINDLE_COMMANDS {
|
||||
exec_command_discard(cmd, args)?;
|
||||
}
|
||||
let _ = exec_command_discard(KILL_KINDLE_KILLALL.0, KILL_KINDLE_KILLALL.1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const CPU_SCALING_GOVERNOR: &str = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor";
|
||||
const CPU_SCALING_GOVERNOR_VALUE: &[u8] = "powersave".as_bytes();
|
||||
|
||||
pub fn set_cpu_powersaving() -> Result<()> {
|
||||
log::info!("Setting CPU into powersaving mode");
|
||||
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(CPU_SCALING_GOVERNOR)?;
|
||||
file.write_all(CPU_SCALING_GOVERNOR_VALUE)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prevent_screensaver(lipc: &Lipc) -> Result<()> {
|
||||
log::info!("Preventing screensaver");
|
||||
lipc.set_int_prop("com.lab126.powerd", "preventScreenSaver", 1)?;
|
||||
Ok(())
|
||||
}
|
86
kdash_client/src/wifi.rs
Normal file
86
kdash_client/src/wifi.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use anyhow::Result;
|
||||
use openlipc_dyn::Lipc;
|
||||
use std::{net, thread, time};
|
||||
|
||||
use crate::utils;
|
||||
|
||||
pub fn set_wifid_enable(lipc: &Lipc, value: bool) -> Result<()> {
|
||||
log::info!("Enabling wifi");
|
||||
lipc.set_int_prop("com.lab126.wifid", "enable", if value { 1 } else { 0 })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cm_state(lipc: &Lipc) -> Result<String> {
|
||||
lipc.get_str_prop("com.lab126.wifid", "cmState")
|
||||
}
|
||||
|
||||
fn get_connected(lipc: &Lipc) -> Result<bool> {
|
||||
Ok(get_cm_state(lipc)? == "CONNECTED")
|
||||
}
|
||||
|
||||
fn get_signal_strength(lipc: &Lipc) -> Result<String> {
|
||||
lipc.get_str_prop("com.lab126.wifid", "signalStrength")
|
||||
}
|
||||
|
||||
fn reconnect(net: &str) -> Result<()> {
|
||||
log::info!("Reconnecting WiFi");
|
||||
utils::exec_command_discard("/usr/bin/wpa_cli", &["-i", net, "reconnect"])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn log_connection(lipc: &Lipc, net: &str) -> Result<()> {
|
||||
let ifconfig = utils::exec_command("ifconfig", &[net])?;
|
||||
log::info!("ifconfig: {}", ifconfig);
|
||||
|
||||
let cm_state = get_cm_state(lipc)?;
|
||||
log::info!("cmState: {}", cm_state);
|
||||
|
||||
let signal_strength = get_signal_strength(lipc)?;
|
||||
log::info!("signalStrength: {}", signal_strength);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_connection(lipc: &Lipc, net: &str) -> Result<bool> {
|
||||
let mut wlan_connected = true;
|
||||
let mut wlan_counter: u64 = 0;
|
||||
|
||||
while !get_connected(lipc)? {
|
||||
if wlan_counter > 5 {
|
||||
log::info!("Trying WiFi reconnect");
|
||||
reconnect(net)?;
|
||||
} else if wlan_counter > 30 {
|
||||
log::info!("No known WiFi found");
|
||||
log_connection(lipc, net)?;
|
||||
wlan_connected = false;
|
||||
// TODO: eips wifi error
|
||||
break;
|
||||
}
|
||||
|
||||
wlan_counter += 1;
|
||||
log::info!("Waiting for WiFi: {}", wlan_counter);
|
||||
thread::sleep(time::Duration::from_secs(wlan_counter));
|
||||
}
|
||||
|
||||
Ok(wlan_connected)
|
||||
}
|
||||
|
||||
pub fn get_gateway(net: &str) -> Result<Option<net::IpAddr>> {
|
||||
let routes = utils::exec_command("ip", &["route"])?;
|
||||
for line in routes.lines() {
|
||||
if line.contains("default") && line.contains(net) {
|
||||
if let Some(gateway) = line.split_whitespace().nth(2) {
|
||||
return Ok(Some(gateway.parse()?));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn route_add_default_gateway_router_ip(router_ip: net::IpAddr) -> Result<()> {
|
||||
utils::exec_command_discard(
|
||||
"route",
|
||||
&["add", "default", "gw", router_ip.to_string().as_str()],
|
||||
)
|
||||
}
|
1
kdash_protocol/.gitignore
vendored
Normal file
1
kdash_protocol/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
12
kdash_protocol/Cargo.toml
Normal file
12
kdash_protocol/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "kdash_protocol"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
chrono-tz = { version = "0.10.0", features = ["serde"] }
|
||||
cron = { version = "0.13.0", features = ["serde"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
|
||||
validated_newtype = "0.1.1"
|
68
kdash_protocol/src/lib.rs
Normal file
68
kdash_protocol/src/lib.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use chrono::Duration;
|
||||
use chrono_tz::Tz;
|
||||
use cron::Schedule;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validated_newtype::validated_newtype;
|
||||
|
||||
validated_newtype! {
|
||||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Clone, Copy)]
|
||||
u8 => pub Percent
|
||||
if |n: &u8| *n <= 100;
|
||||
error "percent must in range 0-100"
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
pub name: String,
|
||||
pub battery: BatteryConfig,
|
||||
pub time: TimeConfig,
|
||||
pub device: DeviceConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct BatteryConfig {
|
||||
pub alert: Percent,
|
||||
pub low: Percent,
|
||||
pub restart_powerd_threshold: Percent,
|
||||
}
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct TimeConfig {
|
||||
pub timezone: Tz,
|
||||
|
||||
pub update_schedule: Schedule,
|
||||
|
||||
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
|
||||
pub delay_on_error: Duration,
|
||||
|
||||
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
|
||||
pub delay_before_suspend: Duration,
|
||||
|
||||
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
|
||||
pub delay_low_battery: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct DeviceConfig {
|
||||
pub use_rtc: bool,
|
||||
pub full_refresh: bool,
|
||||
pub orientation: Orientation,
|
||||
pub status_box_location: Corner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum Orientation {
|
||||
PortraitUp,
|
||||
PortraitDown,
|
||||
LandscapeLeft,
|
||||
LandscapeRight,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
1
kdash_server/.gitignore
vendored
Normal file
1
kdash_server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
21
kdash_server/Cargo.toml
Normal file
21
kdash_server/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "kdash_server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.9.0"
|
||||
anyhow = { version = "1.0.93", features = ["backtrace"] }
|
||||
chrono = "0.4.38"
|
||||
chrono-tz = { version = "0.10.0", features = ["serde"] }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
cron = { version = "0.13.0", features = ["serde"] }
|
||||
env_logger = "0.11.5"
|
||||
jsonwebtoken = "9.3.0"
|
||||
kdash_protocol = { path = "../kdash_protocol" }
|
||||
reqwest = { version = "0.12.9", features = ["stream"] }
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
|
||||
url = { version = "2.5.3", features = ["serde"] }
|
||||
uuid = { version = "1.11.0", features = ["serde"] }
|
97
kdash_server/src/device.rs
Normal file
97
kdash_server/src/device.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use chrono::Duration;
|
||||
use chrono_tz::Tz;
|
||||
use cron::Schedule;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use kdash_protocol::Percent;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
pub name: String,
|
||||
pub image: ImageConfiguration,
|
||||
pub battery: BatteryConfig,
|
||||
pub time: TimeConfig,
|
||||
pub device: DeviceConfig,
|
||||
}
|
||||
|
||||
impl From<Config> for kdash_protocol::Config {
|
||||
fn from(value: Config) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
battery: value.battery.into(),
|
||||
time: value.time.into(),
|
||||
device: value.device.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ImageConfiguration {
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct BatteryConfig {
|
||||
pub alert: Percent,
|
||||
pub low: Percent,
|
||||
pub restart_powerd_threshold: Percent,
|
||||
}
|
||||
|
||||
impl From<BatteryConfig> for kdash_protocol::BatteryConfig {
|
||||
fn from(value: BatteryConfig) -> Self {
|
||||
Self {
|
||||
alert: value.alert,
|
||||
low: value.low,
|
||||
restart_powerd_threshold: value.restart_powerd_threshold,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct TimeConfig {
|
||||
pub timezone: Tz,
|
||||
|
||||
pub update_schedule: Schedule,
|
||||
|
||||
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
|
||||
pub delay_on_error: Duration,
|
||||
|
||||
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
|
||||
pub delay_before_suspend: Duration,
|
||||
|
||||
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
|
||||
pub delay_low_battery: Duration,
|
||||
}
|
||||
|
||||
impl From<TimeConfig> for kdash_protocol::TimeConfig {
|
||||
fn from(value: TimeConfig) -> Self {
|
||||
Self {
|
||||
timezone: value.timezone,
|
||||
update_schedule: value.update_schedule,
|
||||
delay_on_error: value.delay_on_error,
|
||||
delay_before_suspend: value.delay_before_suspend,
|
||||
delay_low_battery: value.delay_low_battery,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct DeviceConfig {
|
||||
pub use_rtc: bool,
|
||||
pub full_refresh: bool,
|
||||
pub orientation: kdash_protocol::Orientation,
|
||||
pub status_box_location: kdash_protocol::Corner,
|
||||
}
|
||||
|
||||
impl From<DeviceConfig> for kdash_protocol::DeviceConfig {
|
||||
fn from(value: DeviceConfig) -> Self {
|
||||
Self {
|
||||
use_rtc: value.use_rtc,
|
||||
full_refresh: value.full_refresh,
|
||||
orientation: value.orientation,
|
||||
status_box_location: value.status_box_location,
|
||||
}
|
||||
}
|
||||
}
|
97
kdash_server/src/handlers.rs
Normal file
97
kdash_server/src/handlers.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use actix_web::{dev::PeerAddr, get, http, web, Error, HttpRequest, HttpResponse};
|
||||
|
||||
use crate::{AppState, Claims};
|
||||
|
||||
#[get("/config")]
|
||||
pub async fn get_config(req: HttpRequest, data: web::Data<AppState>) -> HttpResponse {
|
||||
let token = match req
|
||||
.headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.filter(|s| s.starts_with("Bearer "))
|
||||
.map(|s| &s[7..])
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return HttpResponse::Unauthorized().finish(),
|
||||
};
|
||||
|
||||
let decoded =
|
||||
match jsonwebtoken::decode::<Claims>(token, &data.jwt_decoding_key, &data.jwt_validation) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
|
||||
if !data.device_ids.contains(&decoded.claims.sub) {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
|
||||
let buf = match data.devices_api_json.get(&decoded.claims.sub) {
|
||||
Some(x) => x,
|
||||
None => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(buf.clone())
|
||||
}
|
||||
|
||||
#[get("/image")]
|
||||
pub async fn get_image(
|
||||
req: HttpRequest,
|
||||
peer_addr: Option<PeerAddr>,
|
||||
data: web::Data<AppState>,
|
||||
client: web::Data<reqwest::Client>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let token = match req
|
||||
.headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.filter(|s| s.starts_with("Bearer "))
|
||||
.map(|s| &s[7..])
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return Ok(HttpResponse::Unauthorized().finish()),
|
||||
};
|
||||
|
||||
let decoded =
|
||||
match jsonwebtoken::decode::<Claims>(token, &data.jwt_decoding_key, &data.jwt_validation) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Ok(HttpResponse::BadRequest().finish()),
|
||||
};
|
||||
|
||||
if !data.device_ids.contains(&decoded.claims.sub) {
|
||||
return Ok(HttpResponse::Unauthorized().finish());
|
||||
}
|
||||
|
||||
let config = match data.devices.get(&decoded.claims.sub) {
|
||||
Some(x) => x,
|
||||
None => return Ok(HttpResponse::InternalServerError().finish()),
|
||||
};
|
||||
|
||||
let mut forwarded_req = client.get(config.image.url.to_owned());
|
||||
forwarded_req = match peer_addr {
|
||||
Some(PeerAddr(addr)) => forwarded_req.header("x-forwarded-for", addr.ip().to_string()),
|
||||
None => forwarded_req,
|
||||
};
|
||||
|
||||
let resp = forwarded_req
|
||||
.send()
|
||||
.await
|
||||
.map_err(actix_web::error::ErrorInternalServerError)?;
|
||||
|
||||
let mut client_resp = HttpResponse::build(
|
||||
actix_web::http::StatusCode::from_u16(resp.status().as_u16())
|
||||
.map_err(actix_web::error::ErrorInternalServerError)?,
|
||||
);
|
||||
|
||||
// Remove `Connection` as per
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection#Directives
|
||||
for (header_name, header_value) in resp.headers().iter().filter(|(h, _)| *h != "connection") {
|
||||
client_resp.insert_header((
|
||||
actix_web::http::header::HeaderName::from_bytes(header_name.as_ref()).unwrap(),
|
||||
actix_web::http::header::HeaderValue::from_bytes(header_value.as_ref()).unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(client_resp.streaming(resp.bytes_stream()))
|
||||
}
|
37
kdash_server/src/lib.rs
Normal file
37
kdash_server/src/lib.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use anyhow::Result;
|
||||
use jsonwebtoken::{DecodingKey, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod device;
|
||||
pub mod handlers;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub devices: HashMap<Uuid, device::Config>,
|
||||
}
|
||||
|
||||
pub fn read_config(path: &Path) -> Result<Config> {
|
||||
let buf = fs::read(path)?;
|
||||
let config = serde_json::from_slice(&buf)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub devices: HashMap<Uuid, device::Config>,
|
||||
pub devices_api: HashMap<Uuid, kdash_protocol::Config>,
|
||||
pub devices_api_json: HashMap<Uuid, Vec<u8>>,
|
||||
pub device_ids: HashSet<Uuid>,
|
||||
pub jwt_decoding_key: DecodingKey,
|
||||
pub jwt_validation: Validation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub exp: i64,
|
||||
pub sub: Uuid,
|
||||
}
|
102
kdash_server/src/main.rs
Normal file
102
kdash_server/src/main.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use actix_web::{middleware::Logger, web, App, HttpServer};
|
||||
use anyhow::Result;
|
||||
use chrono::prelude::*;
|
||||
use clap::{Parser, Subcommand};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
path::PathBuf,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[arg(long, value_name = "FILE")]
|
||||
config: PathBuf,
|
||||
|
||||
#[arg(long, value_name = "FILE")]
|
||||
private_key: PathBuf,
|
||||
|
||||
#[arg(long, value_name = "FILE")]
|
||||
public_key: PathBuf,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
GenJwt { id: Uuid },
|
||||
Start,
|
||||
}
|
||||
|
||||
const ALGORITHM: Algorithm = Algorithm::RS512;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let config = kdash_server::read_config(&cli.config)?;
|
||||
|
||||
let sk = fs::read(cli.private_key)?;
|
||||
let encoding_key = EncodingKey::from_rsa_pem(&sk)?;
|
||||
|
||||
let pk = fs::read(cli.public_key)?;
|
||||
let decoding_key = DecodingKey::from_rsa_pem(&pk)?;
|
||||
|
||||
match cli.command {
|
||||
Commands::GenJwt { id: sub } => {
|
||||
let exp = Utc::now() + chrono::Duration::days(10 * 365);
|
||||
let exp_ts = exp.timestamp();
|
||||
let token = jsonwebtoken::encode(
|
||||
&Header::new(ALGORITHM),
|
||||
&kdash_server::Claims { exp: exp_ts, sub },
|
||||
&encoding_key,
|
||||
)?;
|
||||
|
||||
println!("{}", token);
|
||||
}
|
||||
Commands::Start => {
|
||||
HttpServer::new(move || {
|
||||
let devices = config.devices.to_owned();
|
||||
let device_ids: HashSet<Uuid> = config.devices.keys().cloned().collect();
|
||||
|
||||
let devices_api: HashMap<Uuid, kdash_protocol::Config> = devices
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_owned(), kdash_protocol::Config::from(v.to_owned())))
|
||||
.collect();
|
||||
|
||||
let devices_api_json: HashMap<Uuid, Vec<u8>> = devices_api
|
||||
.iter()
|
||||
.map(|(k, v)| Ok((k.to_owned(), serde_json::to_vec(v)?)))
|
||||
.collect::<Result<_>>()
|
||||
.unwrap();
|
||||
|
||||
let reqwest_client = reqwest::Client::default();
|
||||
|
||||
App::new()
|
||||
.app_data(web::Data::new(kdash_server::AppState {
|
||||
devices,
|
||||
devices_api,
|
||||
devices_api_json,
|
||||
device_ids,
|
||||
jwt_decoding_key: decoding_key.to_owned(),
|
||||
jwt_validation: Validation::new(ALGORITHM),
|
||||
}))
|
||||
.app_data(web::Data::new(reqwest_client))
|
||||
.wrap(Logger::default())
|
||||
.service(kdash_server::handlers::get_config)
|
||||
.service(kdash_server::handlers::get_image)
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
.run()
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in a new issue