Merge branch 'release/V0.2.0_Alpha' into stable

This commit is contained in:
Jordon Brooks 2023-06-26 21:54:05 +01:00
commit 110c6aad08
30 changed files with 2229 additions and 62 deletions

View file

@ -1,4 +0,0 @@
[target.x86_64-unknown-linux-gnu]
rustflags = [
"-C", "link-arg=-Wl,-rpath,$ORIGIN",
]

27
.gitignore vendored
View file

@ -1,16 +1,13 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
*
!*/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
!Resources/*
!.gitignore
!src/**
!res/HarmonyLinkLogo.ico
!Resources/**
!.github/**
!Build.rs
!Cargo.toml
!Cargo.lock
!LICENSE
!README.md

View file

@ -1,6 +0,0 @@
{
"rust-analyzer.linkedProjects": [
".\\Cargo.toml",
".\\Cargo.toml"
]
}

1589
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,11 @@
[package]
name = "harmony_link_server"
version = "1.0.0"
version = "0.2.0"
edition = "2021"
authors = ["Jordon jordon@jordongamedev.co.uk"]
homepage = "https://jordongamedev.co.uk"
repository = "https://github.com/Jordonbc/HarmonyLinkServer"
license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -12,9 +16,27 @@ lto = true # Enables link to optimizations
opt-level = "z" # Optimize for binary size
strip = true # Remove debug symbols
[package.metadata.winres]
LegalCopyright = "Copyright © 2023 Jordon Brooks"
ProductName = "HarmonyLink: Server"
FileDescription = "Optimized games for your handheld!"
[[bin]]
name = "harmony_link_server"
path = "src/main.rs"
[build-dependencies]
vergen = { version = "8.2.1", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] }
winres = "0.1.12"
[dependencies]
libloading = "0.8.0"
actix-web = "4.3.1"
env_logger = "0.10.0"
log = "0.4.18"
serde = {version = "1.0.163", features = ["derive"]}
serde_json = "1.0.96"
sysinfo = "0.29.0"
os_info = "3.0"
battery = "0.7.8"
lazy_static = "1.4.0"
rusb = "0.9.2"

View file

@ -0,0 +1,11 @@
[
{
"brand": "JSAUX",
"model": "HB0603",
"usb_ids": [
[ 4826, 21521 ],
[ 4826, 1041 ],
[ 4826, 33139 ]
]
}
]

Binary file not shown.

Binary file not shown.

View file

@ -1,14 +1,21 @@
use std::env;
use std::error::Error;
use std::fs;
use std::path::Path;
fn main() {
use vergen::EmitBuilder;
fn copy_resources() -> Result<(), Box<dyn Error>> {
// The directory of the Cargo manifest of the package that is currently being built.
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not defined");
// The directory where the final binaries will be placed.
let profile = env::var("PROFILE").expect("PROFILE is not defined");
let out_dir = Path::new(&manifest_dir).join("target").join(profile);
let profile = env::var("PROFILE")?;
let out_dir = Path::new(&manifest_dir).join("target").join(profile).join("Resources");
if !out_dir.exists() {
fs::create_dir(&out_dir)?;
}
// The Resources directory.
let resources_dir = Path::new(&manifest_dir).join("Resources");
@ -19,17 +26,47 @@ fn main() {
}
// Iterate over each entry in the Resources directory.
for entry in fs::read_dir(resources_dir).expect("read_dir call failed") {
let entry = entry.expect("entry is invalid");
for entry in fs::read_dir(resources_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
// The destination path is the output directory plus the file name.
let dest_path = out_dir.join(path.file_name().expect("file has no name"));
// Copy the file.
fs::copy(&path, &dest_path).expect("copy failed");
fs::copy(&path, &dest_path)?;
}
}
println!("cargo:rustc-env=RUSTFLAGS=-C link-arg=-Wl,-rpath,$ORIGIN");
println!("cargo:rerun-if-changed=src/main.rs");
Ok(())
}
#[cfg(windows)]
fn windows_resource() -> Result<(), Box<dyn Error>> {
let mut res = winres::WindowsResource::new();
res.set_icon("res/HarmonyLinkLogo.ico");
res.compile()?;
Ok(())
}
#[cfg(unix)]
fn windows_resource() -> Result<(), Box<dyn Error>> {
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
// Emit the instructions
EmitBuilder::builder()
.all_build()
.all_cargo()
.all_git()
.all_rustc()
.all_sysinfo()
.emit()?;
// Copy the Resources folder
copy_resources()?;
windows_resource()?;
Ok(())
}

BIN
res/HarmonyLinkLogo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

14
src/api/api.rs Normal file
View file

@ -0,0 +1,14 @@
use actix_web::{HttpResponse, get, web};
use crate::version::info::Version;
#[get("/supported_versions")]
pub async fn versions() -> HttpResponse {
let version = Version::get();
HttpResponse::Ok().json(&version.supported_api_versions)
}
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(versions);
// Register other version 1 handlers here...
}

99
src/api/endpoints_v1.rs Normal file
View file

@ -0,0 +1,99 @@
use actix_web::web;
use actix_web::{HttpResponse, get};
use crate::v1::{docking, os, all_info, battery};
use crate::version;
#[get("/are_you_there")]
pub async fn heartbeat() -> HttpResponse {
HttpResponse::Ok().body("yes")
}
#[get("/all_info")]
pub async fn get_all_info() -> HttpResponse {
match all_info::stats::get_all_info() {
Ok(info) => {
#[cfg(debug_assertions)]
{
println!("Successfully got all info: {}", &info.clone().to_string());
}
HttpResponse::Ok().json(&info)
},
Err(err) => {
eprintln!("Failed to get all info: {}", err);
HttpResponse::InternalServerError().body(format!("Failed to get device info: {}", err))
}
}
}
#[get("/dock_info")]
pub async fn get_dock_info() -> HttpResponse {
match docking::stats::get_dock() {
Ok(info) => {
#[cfg(debug_assertions)]
{
println!("Successfully got dock info: {}", &info.clone().to_string());
}
HttpResponse::Ok().json(&info)
},
Err(err) => {
eprintln!("Failed to get dock info: {}", err);
HttpResponse::InternalServerError().body(format!("Failed to get dock info: {}", err))
}
}
}
#[get("/os_info")]
pub async fn get_os_info() -> HttpResponse {
match os::stats::get_os() {
Ok(info) => {
#[cfg(debug_assertions)]
{
println!("Successfully got os info: {}", &info.clone().to_string());
}
HttpResponse::Ok().json(&info)
},
Err(err) => {
eprintln!("Failed to get os info: {}", err);
HttpResponse::InternalServerError().body(format!("Failed to get OS info: {}", err))
}
}
}
#[get("/battery_info")]
pub async fn get_battery_info() -> HttpResponse {
match battery::stats::get_battery_info() {
Ok(info) => {
#[cfg(debug_assertions)]
{
println!("Successfully got battery info: {}", &info.clone().to_string());
}
HttpResponse::Ok().json(&info)
},
Err(err) => {
eprintln!("Failed to get battery info: {}", err);
HttpResponse::InternalServerError().body(format!("Failed to get battery info: {}", err))
}
}
}
#[get("/version_info")]
pub async fn get_version_info() -> HttpResponse {
#[cfg(debug_assertions)]
{
println!("Successfully got version info: {}", version::info::Version::get().to_string());
}
HttpResponse::Ok().json(&version::info::Version::get())
}
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(heartbeat);
cfg.service(get_all_info);
cfg.service(get_dock_info);
cfg.service(get_os_info);
cfg.service(get_battery_info);
cfg.service(get_version_info);
// Register other version 1 handlers here...
}

3
src/api/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod server;
mod endpoints_v1;
mod api;

28
src/api/server.rs Normal file
View file

@ -0,0 +1,28 @@
use actix_web::{HttpServer, web};
use crate::api::endpoints_v1;
use crate::api::api;
#[allow(dead_code)]
pub async fn stop_actix_web(server: actix_web::dev::Server) -> std::io::Result<()> {
println!("Stopping server.");
server.handle().stop(true).await;
Ok(())
}
pub fn start_actix_web(port: u16) -> std::io::Result<actix_web::dev::Server> {
println!("Starting webserver on 127.0.0.1:{}", port);
let server = HttpServer::new(move || {
let logger = actix_web::middleware::Logger::default();
actix_web::App::new()
.wrap(logger)
.service(web::scope("/api").configure(api::configure))
.service(web::scope("/v1").configure(endpoints_v1::configure))
})
.bind(("127.0.0.1", port))?
.run();
Ok(server)
}

View file

@ -1,34 +1,37 @@
extern crate libloading;
mod v1;
mod version;
use version::info::Version;
mod api;
static PORT: u16 = 9000;
static USE_FALLBACK_DOCK_DETECTION: bool = false;
fn main() {
// Use `cfg!` macro to detect OS
let lib_path = if cfg!(target_os = "windows") {
"harmony_link_core.dll"
} else if cfg!(target_os = "linux") {
"libharmony_link_core.so"
} else {
eprintln!("Unsupported OS");
return;
};
let lib = unsafe { match libloading::Library::new(lib_path) {
Ok(lib) => lib,
Err(err) => {
eprintln!("Error loading dynamic library: {}", err);
return;
},
}
};
//#[cfg(debug_assertions)]
{
let version_info = Version::get();
println!("Version: {}", version_info.version);
println!("Build Timestamp: {}", version_info.build_timestamp);
println!("Git Branch: {}", version_info.git_branch);
println!("Git Describe: {}", version_info.git_describe);
println!("Git Commit Timestamp: {}", version_info.git_commit_timestamp);
println!("Debug Build: {}", version_info.debug);
println!("API versions: {}", version_info.supported_api_versions_to_string());
unsafe {
let func: libloading::Symbol<unsafe extern "C" fn()> = match lib.get(b"start") {
Ok(func) => func,
Err(err) => {
eprintln!("Error finding function in dynamic library: {}", err);
return;
println!("\n\n");
}
};
func();
}
println!("HarmonyLink ©️ Jordon Brooks 2023");
let sys = actix_web::rt::System::new();
sys.block_on(async {
let result = api::server::start_actix_web(PORT).expect("err");
let _ = result.await;
});
}

2
src/v1/all_info/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod stats;
pub mod structs;

18
src/v1/all_info/stats.rs Normal file
View file

@ -0,0 +1,18 @@
use crate::v1::docking;
use crate::v1::battery;
use crate::v1::os;
use crate::version;
use super::structs::Allinfo;
/* This will query all the modules and return all the combined data */
pub fn get_all_info() -> Result<Allinfo, Box<dyn std::error::Error>> {
let mut all_info = Allinfo::new();
all_info.dock = docking::stats::get_dock_info()?;
all_info.battery = battery::stats::get_battery_info()?;
all_info.os = os::stats::get_os()?;
all_info.version = version::info::Version::get();
Ok(all_info)
}

View file

@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
use crate::{v1::{os, battery, docking::{self, structs::DockInfo}}, version};
#[derive(Deserialize, Serialize, Clone)]
pub struct Allinfo {
pub os: os::structs::OSInfo,
pub battery: battery::structs::BatteryInfo,
pub dock: docking::structs::DockInfo,
pub version: version::info::Version
}
impl Allinfo {
pub fn new() -> Allinfo {
Allinfo { os: os::structs::OSInfo::new(),
battery: battery::structs::BatteryInfo::new(),
dock: DockInfo::new(),
version: version::info::Version::get()
}
}
pub fn to_string(self) -> String {
serde_json::to_string(&self).expect("Failed to parse into string")
}
}

2
src/v1/battery/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod stats;
pub mod structs;

38
src/v1/battery/stats.rs Normal file
View file

@ -0,0 +1,38 @@
use crate::v1::battery::structs::ChargingStatus;
use super::structs::BatteryInfo;
pub fn get_battery_info() -> Result<BatteryInfo, Box<dyn std::error::Error>> {
let mut battery_info = BatteryInfo { has_battery: false, battery_percent: 0, charging_status: ChargingStatus::UNKNOWN };
let manager = battery::Manager::new().unwrap();
battery_info.has_battery = manager.batteries().unwrap().count() > 0;
if !battery_info.has_battery {
return Ok(battery_info);
}
for (idx, battery) in manager.batteries()?.enumerate() {
let battery = battery?;
let state = battery.state();
let energy = battery.energy();
let full_energy = battery.energy_full();
let state_of_charge = (energy / full_energy).get::<battery::units::ratio::percent>();
println!("Battery #{}:", idx);
println!("Charging status: {:?}", state);
println!("Charge level: {:.2}%", state_of_charge);
battery_info.battery_percent = state_of_charge.round() as i8;
battery_info.charging_status = match state {
battery::State::Charging => ChargingStatus::Charging,
battery::State::Discharging => ChargingStatus::Battery,
battery::State::Empty => ChargingStatus::Battery,
battery::State::Full => ChargingStatus::Battery,
_ => ChargingStatus::UNKNOWN,
}
}
Ok(battery_info)
}

28
src/v1/battery/structs.rs Normal file
View file

@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, PartialEq, Clone)]
pub enum ChargingStatus {
Charging,
Battery,
UNKNOWN,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct BatteryInfo {
pub has_battery: bool,
pub battery_percent: i8,
pub charging_status: ChargingStatus,
}
impl BatteryInfo {
pub fn new() -> BatteryInfo {
BatteryInfo {
has_battery: false,
battery_percent: 0,
charging_status: ChargingStatus::UNKNOWN
}
}
pub fn to_string(self) -> String {
serde_json::to_string(&self).expect("Failed to parse into string")
}
}

2
src/v1/docking/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod stats;
pub mod structs;

90
src/v1/docking/stats.rs Normal file
View file

@ -0,0 +1,90 @@
use std::{io::BufReader, fs::File};
#[allow(unused_imports)]
use std::collections::HashSet;
use crate::{v1::{battery::{stats::get_battery_info, structs::ChargingStatus}}, USE_FALLBACK_DOCK_DETECTION};
use super::structs::{Dock, DockInfo};
/* This will get the current dock info. */
pub fn get_dock_info() -> Result<DockInfo, Box<dyn std::error::Error>> {
let mut dock = DockInfo::new();
dock.dock_info = get_dock()?;
dock.is_docked = dock.dock_info.brand == String::new() && dock.dock_info.model == String::new();
/*
This code will manually detect a dock if it wasn't automatically picked up by get_dock().
The code currently doesnt work. To manually detect a dock we will detect the presence of
a battery, charging of the handheld, and eventually when I find a cross-platform create,
it will also detect the presence of an external monitor (will most likely only work on a
Steam Deck for now)
*/
if USE_FALLBACK_DOCK_DETECTION {
if !dock.is_docked {
dock.fallback_detection = true;
let battery_info = get_battery_info()?;
if battery_info.has_battery && battery_info.charging_status == ChargingStatus::Charging {
}
}
}
Ok(dock)
}
/* Reads the dock_models.json file and returns a vector of structs with the data */
#[allow(dead_code)]
pub fn read_dock_models_from_file() -> Result<Vec<Dock>, Box<dyn std::error::Error>> {
let file = File::open("Resources/dock_models.json")?;
let reader = BufReader::new(file);
let dock_models: Vec<Dock> = serde_json::from_reader(reader)?;
Ok(dock_models)
}
#[cfg(target_os = "linux")]
/* This will detect the dock model and brand. */
pub fn get_dock() -> Result<Dock, Box<dyn std::error::Error>> {
let devices = rusb::devices()?;
let dock_models = read_dock_models_from_file()?;
for dock_model in dock_models {
let mut found_components = HashSet::new();
for device in devices.iter() {
let device_desc = device.device_descriptor()?;
let device_id = (device_desc.vendor_id(), device_desc.product_id());
// Check if the device is one of the components of the dock.
if dock_model.usb_ids.contains(&[device_desc.vendor_id(), device_desc.product_id()]) {
found_components.insert(device_id);
println!("(get_dock) Detected: {}", serde_json::to_string_pretty(&device_id)?);
}
}
if found_components.len() == dock_model.usb_ids.len() {
println!("(get_dock) All components detected for {}", serde_json::to_string_pretty(&dock_model)?);
return Ok(Dock {
model: dock_model.model.clone(),
brand: dock_model.brand.clone(),
usb_ids: dock_model.usb_ids.clone()
});
}
}
Ok(Dock::new())
}
#[cfg(target_os = "macos")]
/* This will detect the dock model and brand. */
pub fn get_dock() -> Result<Dock, Box<dyn std::error::Error>> {
Ok(Dock::new())
//Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Incorrect OS")))
}
/* This will detect the dock model and brand. */
#[cfg(target_os = "windows")]
pub fn get_dock() -> Result<Dock, Box<dyn std::error::Error>> {
Ok(Dock::new())
}

39
src/v1/docking/structs.rs Normal file
View file

@ -0,0 +1,39 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Clone)]
pub struct Dock {
pub brand: String, // ex: JSAUX
pub model: String, // ex: HB0603
pub usb_ids: Vec<[u16; 2]>,
}
impl Dock {
pub fn new() -> Dock {
Dock { brand: String::new(),
model: String::new(),
usb_ids: vec![],
}
}
pub fn to_string(self) -> String {
serde_json::to_string(&self).expect("Failed to parse into string")
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct DockInfo {
pub dock_info: Dock,
pub is_docked: bool,
pub fallback_detection: bool,
}
impl DockInfo {
pub fn new() -> DockInfo {
DockInfo { dock_info: Dock::new(),
is_docked: false,
fallback_detection: false
}
}
pub fn to_string(self) -> String {
serde_json::to_string(&self).expect("Failed to parse into string")
}
}

4
src/v1/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod battery;
pub mod docking;
pub mod os;
pub mod all_info;

2
src/v1/os/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod stats;
pub mod structs;

45
src/v1/os/stats.rs Normal file
View file

@ -0,0 +1,45 @@
use super::structs::{OSInfo, Platform, Architecture};
pub fn get_platform() -> Platform {
if cfg!(target_os = "windows") {
Platform::WINDOWS
} else if cfg!(target_os = "macos") {
Platform::MAC
} else if cfg!(target_os = "linux") {
Platform::LINUX
} else {
Platform::UNKNOWN
}
}
pub fn get_os() -> Result<OSInfo, Box<dyn std::error::Error>> {
let mut temp = OSInfo::new();
let info = os_info::get();
temp.platform = get_platform();
temp.name = match info.codename() {
Some(s) => s.to_string(),
_ => String::new(),
};
temp.version = info.version().to_string();
if temp.name == String::new() {
temp.name = match info.edition() {
Some(s) => s.to_string(),
_ => String::new(),
};
}
temp.bits = match info.bitness()
{
os_info::Bitness::X32 => Architecture::X86,
os_info::Bitness::X64 => Architecture::X86_64,
_ => Architecture::UNKNOWN,
};
Ok(temp)
}

38
src/v1/os/structs.rs Normal file
View file

@ -0,0 +1,38 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, PartialEq, Clone)]
pub enum Platform {
WINDOWS = 0,
LINUX = 1,
MAC = 2,
UNKNOWN = 255
}
#[derive(Deserialize, Serialize, Clone)]
pub enum Architecture {
X86 = 0,
X86_64 = 1,
UNKNOWN = 255,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct OSInfo {
pub platform: Platform, // Windows, Mac, Linux
pub name: String, // "Windows 11 2306", "Ubuntu 22.04 LTS"
pub version: String, // 2306, 22.04
pub bits: Architecture // 32, 64
}
impl OSInfo {
pub fn new() -> OSInfo {
OSInfo {
platform: Platform::UNKNOWN,
name: String::new(),
version: String::new(),
bits: Architecture::UNKNOWN
}
}
pub fn to_string(self) -> String {
serde_json::to_string(&self).expect("Failed to parse into string")
}
}

41
src/version/info.rs Normal file
View file

@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Clone)]
pub struct Version {
pub build_timestamp: String,
pub git_branch: String,
pub git_describe: String,
pub git_commit_timestamp: String,
pub debug: bool,
pub version: String,
pub version_major: i32,
pub version_minor: i32,
pub version_patch: i32,
pub version_pre: String,
pub supported_api_versions: Vec<String>
}
impl Version {
pub fn get() -> Version {
Version {
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
git_branch: env!("VERGEN_GIT_BRANCH").to_string(),
git_describe: env!("VERGEN_GIT_DESCRIBE").to_string(),
git_commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
debug: env!("VERGEN_CARGO_DEBUG").parse().unwrap(),
version: env!("CARGO_PKG_VERSION").to_string(),
version_major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
version_minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
version_patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
version_pre: "Alpha".to_string(),
supported_api_versions: vec!["v1".to_string()]
}
}
pub fn to_string(self) -> String {
serde_json::to_string(&self).expect("Failed to parse into string")
}
pub fn supported_api_versions_to_string(self) -> String {
self.supported_api_versions.join(", ")
}
}

1
src/version/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod info;