[refactor](trx-client): remove Qt/QML frontend support
Remove the Linux-only Qt/QML frontend (trx-frontend-qt) crate and all references to it from the workspace, trx-client binary, configuration, and documentation. This prepares for replacement with a native macOS AppKit frontend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
Generated
+16
-132
@@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -179,7 +179,7 @@ dependencies = [
|
||||
"actix-router",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -323,12 +323,6 @@ dependencies = [
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
@@ -399,7 +393,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -447,56 +441,6 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpp"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f36bcac3d8234c1fb813358e83d1bb6b0290a3d2b3b5efc6b88bfeaf9d8eec17"
|
||||
dependencies = [
|
||||
"cpp_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpp_build"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27f8638c97fbd79cc6fc80b616e0e74b49bac21014faed590bbc89b7e2676c90"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cpp_common",
|
||||
"lazy_static",
|
||||
"proc-macro2",
|
||||
"regex",
|
||||
"syn 2.0.111",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpp_common"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25fcfea2ee05889597d35e986c2ad0169694320ae5cc8f6d2640a4bb8a884560"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"proc-macro2",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpp_macros"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d156158fe86e274820f5a53bc9edb0885a6e7113909497aa8d883b69dd171871"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"byteorder",
|
||||
"cpp_common",
|
||||
"lazy_static",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -544,7 +488,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -564,7 +508,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@@ -607,7 +551,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -718,7 +662,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1250,43 +1194,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qmetaobject"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "426a57e85d36f055a0c82cb0a8a261d49ba051ab2a2ef5471835f69d477816cd"
|
||||
dependencies = [
|
||||
"cpp",
|
||||
"cpp_build",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"qmetaobject_impl",
|
||||
"qttypes",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qmetaobject_impl"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc24897c707dcd6963e359e7f2b123857c508f129bed8ac4d3bd575c1a47627"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qttypes"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7edf5b38c97ad8900ad2a8418ee44b4adceaa866a4a3405e2f1c909871d7ebd"
|
||||
dependencies = [
|
||||
"cpp",
|
||||
"cpp_build",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
@@ -1440,7 +1347,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1580,17 +1487,6 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
@@ -1610,7 +1506,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1630,7 +1526,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1708,7 +1604,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1811,7 +1707,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1889,7 +1785,6 @@ dependencies = [
|
||||
"trx-frontend",
|
||||
"trx-frontend-http",
|
||||
"trx-frontend-http-json",
|
||||
"trx-frontend-qt",
|
||||
"trx-frontend-rigctl",
|
||||
]
|
||||
|
||||
@@ -1938,17 +1833,6 @@ dependencies = [
|
||||
"trx-frontend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trx-frontend-qt"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"qmetaobject",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"trx-core",
|
||||
"trx-frontend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trx-frontend-rigctl"
|
||||
version = "0.1.0"
|
||||
@@ -2278,7 +2162,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -2299,7 +2183,7 @@ checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2319,7 +2203,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -2353,7 +2237,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -11,7 +11,6 @@ members = [
|
||||
"src/trx-client/trx-frontend",
|
||||
"src/trx-client/trx-frontend/trx-frontend-http",
|
||||
"src/trx-client/trx-frontend/trx-frontend-http-json",
|
||||
"src/trx-client/trx-frontend/trx-frontend-qt",
|
||||
"src/trx-client/trx-frontend/trx-frontend-rigctl",
|
||||
"src/trx-core",
|
||||
]
|
||||
|
||||
+1
-13
@@ -20,7 +20,6 @@
|
||||
| TCP CAT transport | Partial (config wiring only) |
|
||||
| JSON TCP control (line-delimited) | Implemented (configurable frontend) |
|
||||
| Plugin registry loading | Implemented (shared libraries) |
|
||||
| Qt/QML GUI frontend | In progress (Linux only, optional) |
|
||||
| Configuration file (TOML) | Implemented |
|
||||
| Rig state machine | Implemented |
|
||||
| Command handlers | Implemented |
|
||||
@@ -81,10 +80,9 @@
|
||||
| `trx-frontend` | Frontend trait (`FrontendSpawner`) |
|
||||
| `trx-frontend-http` | Web UI with REST API and SSE |
|
||||
| `trx-frontend-http-json` | JSON-over-TCP control frontend |
|
||||
| `trx-frontend-qt` | Qt/QML GUI frontend (Linux only, optional) |
|
||||
| `trx-frontend-rigctl` | Hamlib rigctl-compatible TCP interface |
|
||||
| `trx-server` | Server binary — connects to rig backend, exposes JSON TCP control |
|
||||
| `trx-client` | Client binary — connects to server, runs frontends (HTTP, rigctl, Qt) |
|
||||
| `trx-client` | Client binary — connects to server, runs frontends (HTTP, rigctl) |
|
||||
|
||||
---
|
||||
|
||||
@@ -103,10 +101,6 @@ Plugin discovery:
|
||||
- Uses shared libraries with a `trx_register` entrypoint.
|
||||
- Searches `./plugins`, `~/.config/trx-rs/plugins`, and any paths in `TRX_PLUGIN_DIRS`.
|
||||
|
||||
Qt remote client:
|
||||
- Uses JSON TCP (`frontends.http_json`) with optional bearer tokens.
|
||||
- Configure the client with `frontends.qt.remote.enabled/url/auth.token`.
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```toml
|
||||
@@ -139,12 +133,6 @@ listen = "127.0.0.1"
|
||||
port = 9000
|
||||
auth.tokens = ["demo-token"]
|
||||
|
||||
[frontends.qt]
|
||||
enabled = false
|
||||
remote.enabled = true
|
||||
remote.url = "127.0.0.1:9000"
|
||||
remote.auth.token = "demo-token"
|
||||
|
||||
[behavior]
|
||||
poll_interval_ms = 500
|
||||
poll_interval_tx_ms = 100
|
||||
|
||||
@@ -17,7 +17,6 @@ The rig task is now driven by the controller components (state machine, handlers
|
||||
|
||||
- HTTP status/control frontend (`trx-frontend-http`)
|
||||
- JSON TCP control frontend (`trx-frontend-http-json`)
|
||||
- Qt/QML GUI frontend (`trx-frontend-qt`, Linux only, optional via `qt-frontend` feature)
|
||||
- rigctl-compatible TCP frontend (`trx-frontend-rigctl`, listens on 127.0.0.1:4532)
|
||||
|
||||
## Plugin discovery
|
||||
@@ -31,13 +30,6 @@ via a `trx_register` entrypoint. Search paths:
|
||||
|
||||
Example plugin: `examples/trx-plugin-example`
|
||||
|
||||
## Qt remote client
|
||||
|
||||
The Qt frontend can run as a remote client over the JSON TCP interface.
|
||||
Configure the server with `frontends.http_json.auth.tokens` and the client with
|
||||
`frontends.qt.remote.enabled`, `frontends.qt.remote.url`, and
|
||||
`frontends.qt.remote.auth.token`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the BSD-2-Clause license. See `LICENSES/` for bundled third-party license files.
|
||||
|
||||
@@ -22,8 +22,3 @@ trx-frontend = { path = "trx-frontend" }
|
||||
trx-frontend-http = { path = "trx-frontend/trx-frontend-http" }
|
||||
trx-frontend-http-json = { path = "trx-frontend/trx-frontend-http-json" }
|
||||
trx-frontend-rigctl = { path = "trx-frontend/trx-frontend-rigctl" }
|
||||
trx-frontend-qt = { path = "trx-frontend/trx-frontend-qt", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
qt-frontend = ["trx-frontend-qt/qt"]
|
||||
|
||||
@@ -67,7 +67,7 @@ pub struct RemoteAuthConfig {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
/// Frontend configurations (client — includes Qt).
|
||||
/// Frontend configurations.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct FrontendsConfig {
|
||||
@@ -77,8 +77,6 @@ pub struct FrontendsConfig {
|
||||
pub rigctl: RigctlFrontendConfig,
|
||||
/// JSON TCP frontend settings
|
||||
pub http_json: HttpJsonFrontendConfig,
|
||||
/// Qt/QML frontend settings
|
||||
pub qt: QtFrontendConfig,
|
||||
}
|
||||
|
||||
/// HTTP frontend configuration.
|
||||
@@ -158,14 +156,6 @@ pub struct HttpJsonAuthConfig {
|
||||
pub tokens: Vec<String>,
|
||||
}
|
||||
|
||||
/// Qt/QML frontend configuration.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct QtFrontendConfig {
|
||||
/// Whether Qt frontend is enabled
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl ClientConfig {
|
||||
/// Load configuration from a specific file path.
|
||||
pub fn load_from_file(path: &Path) -> Result<Self, ConfigError> {
|
||||
@@ -235,7 +225,6 @@ impl ClientConfig {
|
||||
port: 4532,
|
||||
},
|
||||
http_json: HttpJsonFrontendConfig::default(),
|
||||
qt: QtFrontendConfig { enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -290,7 +279,6 @@ mod tests {
|
||||
assert_eq!(config.frontends.rigctl.port, 4532);
|
||||
assert!(config.frontends.http_json.enabled);
|
||||
assert_eq!(config.frontends.http_json.port, 0);
|
||||
assert!(!config.frontends.qt.enabled);
|
||||
assert!(config.remote.url.is_none());
|
||||
assert_eq!(config.remote.poll_interval_ms, 750);
|
||||
}
|
||||
@@ -311,8 +299,6 @@ enabled = true
|
||||
listen = "127.0.0.1"
|
||||
port = 8080
|
||||
|
||||
[frontends.qt]
|
||||
enabled = true
|
||||
"#;
|
||||
|
||||
let config: ClientConfig = toml::from_str(toml_str).unwrap();
|
||||
@@ -321,7 +307,6 @@ enabled = true
|
||||
assert_eq!(config.remote.auth.token, Some("my-token".to_string()));
|
||||
assert_eq!(config.remote.poll_interval_ms, 500);
|
||||
assert!(config.frontends.http.enabled);
|
||||
assert!(config.frontends.qt.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -25,15 +25,11 @@ use trx_frontend_http::register_frontend as register_http_frontend;
|
||||
use trx_frontend_http_json::{register_frontend as register_http_json_frontend, set_auth_tokens};
|
||||
use trx_frontend_rigctl::register_frontend as register_rigctl_frontend;
|
||||
|
||||
#[cfg(feature = "qt-frontend")]
|
||||
use trx_frontend_qt::register_frontend as register_qt_frontend;
|
||||
|
||||
use config::ClientConfig;
|
||||
use remote_client::{parse_remote_url, RemoteClientConfig};
|
||||
|
||||
const PKG_DESCRIPTION: &str = concat!(env!("CARGO_PKG_NAME"), " - remote rig client");
|
||||
const RIG_TASK_CHANNEL_BUFFER: usize = 32;
|
||||
const QT_FRONTEND_LISTEN_ADDR: ([u8; 4], u16) = ([127, 0, 0, 1], 0);
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
@@ -57,7 +53,7 @@ struct Cli {
|
||||
/// Poll interval in milliseconds
|
||||
#[arg(long = "poll-interval")]
|
||||
poll_interval_ms: Option<u64>,
|
||||
/// Frontend(s) to expose locally (e.g. http,rigctl,qt)
|
||||
/// Frontend(s) to expose locally (e.g. http,rigctl)
|
||||
#[arg(short = 'f', long = "frontend", value_delimiter = ',', num_args = 1..)]
|
||||
frontends: Option<Vec<String>>,
|
||||
/// HTTP frontend listen address
|
||||
@@ -98,8 +94,6 @@ async fn main() -> DynResult<()> {
|
||||
register_http_frontend();
|
||||
register_http_json_frontend();
|
||||
register_rigctl_frontend();
|
||||
#[cfg(feature = "qt-frontend")]
|
||||
register_qt_frontend();
|
||||
let _plugin_libs = plugins::load_plugins();
|
||||
|
||||
let cli = Cli::parse();
|
||||
@@ -155,9 +149,6 @@ async fn main() -> DynResult<()> {
|
||||
if cfg.frontends.http_json.enabled {
|
||||
fes.push("httpjson".to_string());
|
||||
}
|
||||
if cfg.frontends.qt.enabled {
|
||||
fes.push("qt".to_string());
|
||||
}
|
||||
if fes.is_empty() {
|
||||
fes.push("http".to_string());
|
||||
}
|
||||
@@ -239,7 +230,6 @@ async fn main() -> DynResult<()> {
|
||||
"http" => SocketAddr::from((http_listen, http_port)),
|
||||
"rigctl" => SocketAddr::from((rigctl_listen, rigctl_port)),
|
||||
"httpjson" => SocketAddr::from((http_json_listen, http_json_port)),
|
||||
"qt" => SocketAddr::from(QT_FRONTEND_LISTEN_ADDR),
|
||||
other => {
|
||||
return Err(format!("Frontend missing listen configuration: {}", other).into());
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
[package]
|
||||
name = "trx-frontend-qt"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
qt = ["dep:qmetaobject"]
|
||||
|
||||
[dependencies]
|
||||
trx-core = { path = "../../../trx-core" }
|
||||
trx-frontend = { path = ".." }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
qmetaobject = { version = "0.2", optional = true }
|
||||
@@ -1,36 +0,0 @@
|
||||
# Qt QML Frontend Requirements
|
||||
|
||||
## Scope
|
||||
- Provide a Qt Quick (QML) GUI frontend for trx-rs.
|
||||
- Linux-only support for the initial implementation.
|
||||
- Use system-wide Qt6 (no vendored Qt).
|
||||
- Frontend must be optional and feature-gated; default build should not require Qt.
|
||||
- Feature name in `trx-client`: `qt-frontend`.
|
||||
|
||||
## Functional Requirements
|
||||
- Show rig status: frequency, mode, PTT state, VFO info, lock state, power state.
|
||||
- Show basic meters when available: RX signal, TX power/limit/SWR/ALC (as provided by state).
|
||||
- Allow commands: set frequency, set mode, toggle PTT, power on/off, toggle VFO, lock/unlock, set TX limit (if supported).
|
||||
- Reflect live updates pushed from the rig task (watch updates).
|
||||
|
||||
## Non-Functional Requirements
|
||||
- Linux-only for now.
|
||||
- Build relies on Qt6 libraries/headers installed on the system.
|
||||
- GUI must be responsive and not block the rig task or frontend thread.
|
||||
- Minimal but clear UI; no advanced theming or custom widgets required yet.
|
||||
|
||||
## Configuration & Integration
|
||||
- Expose as a new frontend crate: `trx-frontend-qt`.
|
||||
- Register via frontend registry under name: `qt`.
|
||||
- Optional via feature flag (e.g., `qt`) and not part of default workspace features.
|
||||
- Provide config toggles under `[frontends.qt]` for enable/listen if needed.
|
||||
- Remote client mode uses JSON TCP with bearer token via `frontends.qt.remote.*`.
|
||||
|
||||
## Packaging/Build
|
||||
- Document required packages (Qt6 base + QML modules + qmetaobject-rs build prereqs).
|
||||
- Provide build/run instructions in README/OVERVIEW updates.
|
||||
|
||||
## Out of Scope (for v1)
|
||||
- Windows/macOS support.
|
||||
- Offline themes or custom QML assets.
|
||||
- Advanced settings editor or multi-rig management.
|
||||
@@ -1,102 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
visible: true
|
||||
width: 900
|
||||
height: 540
|
||||
title: "trx-rs"
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
text: "trx-rs Qt frontend (stub)"
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
Label { text: "Frequency: " + rig.freq_text + " (" + rig.freq_hz + " Hz)" }
|
||||
Label { text: "Mode: " + rig.mode + " Band: " + rig.band }
|
||||
Label { text: "PTT: " + (rig.tx_enabled ? "TX" : "RX") + " Power: " + (rig.powered ? "On" : "Off") }
|
||||
Label { text: "Lock: " + (rig.locked ? "Locked" : "Unlocked") }
|
||||
Label { text: "RX Sig: " + rig.rx_sig + " dB" }
|
||||
Label { text: "TX Pwr: " + rig.tx_power + " Limit: " + rig.tx_limit + " SWR: " + rig.tx_swr + " ALC: " + rig.tx_alc }
|
||||
|
||||
Row {
|
||||
spacing: 6
|
||||
|
||||
TextField {
|
||||
id: freqInput
|
||||
width: 140
|
||||
placeholderText: "Freq (Hz)"
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Set Freq"
|
||||
onClicked: rig.set_freq_hz(parseInt(freqInput.text))
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: modeInput
|
||||
width: 80
|
||||
placeholderText: "Mode"
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Set Mode"
|
||||
onClicked: rig.set_mode(modeInput.text)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 6
|
||||
|
||||
Button {
|
||||
text: rig.tx_enabled ? "PTT Off" : "PTT On"
|
||||
onClicked: rig.toggle_ptt()
|
||||
}
|
||||
Button {
|
||||
text: rig.powered ? "Power Off" : "Power On"
|
||||
onClicked: rig.toggle_power()
|
||||
}
|
||||
Button {
|
||||
text: "VFO"
|
||||
onClicked: rig.toggle_vfo()
|
||||
}
|
||||
Button {
|
||||
text: rig.locked ? "Unlock" : "Lock"
|
||||
onClicked: rig.locked ? rig.unlock_panel() : rig.lock_panel()
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 6
|
||||
TextField {
|
||||
id: txLimitInput
|
||||
width: 120
|
||||
placeholderText: "TX Limit"
|
||||
}
|
||||
Button {
|
||||
text: "Set Limit"
|
||||
onClicked: rig.set_tx_limit(parseInt(txLimitInput.text))
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 540
|
||||
height: 120
|
||||
color: "#20252b"
|
||||
radius: 6
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
color: "#d0d6de"
|
||||
text: rig.vfo
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "qt"))]
|
||||
pub mod server;
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "qt"))]
|
||||
pub fn register_frontend() {
|
||||
use trx_frontend::FrontendSpawner;
|
||||
trx_frontend::register_frontend("qt", server::QtFrontend::spawn_frontend);
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_os = "linux", feature = "qt")))]
|
||||
pub fn register_frontend() {
|
||||
// No-op on non-Linux platforms.
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
|
||||
use qmetaobject::{
|
||||
qt_base_class, qt_method, qt_property, qt_signal, queued_callback, QObject, QObjectPinned,
|
||||
QString, QmlEngine,
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot, watch};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use trx_core::rig::command::RigCommand;
|
||||
use trx_core::rig::state::RigMode;
|
||||
use trx_core::{RigRequest, RigState};
|
||||
use trx_frontend::FrontendSpawner;
|
||||
|
||||
/// Qt/QML frontend (Linux-only).
|
||||
pub struct QtFrontend;
|
||||
|
||||
impl FrontendSpawner for QtFrontend {
|
||||
fn spawn_frontend(
|
||||
state_rx: watch::Receiver<RigState>,
|
||||
rig_tx: mpsc::Sender<RigRequest>,
|
||||
_callsign: Option<String>,
|
||||
listen_addr: SocketAddr,
|
||||
) -> JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let (update_tx, update_rx) = oneshot::channel::<Box<dyn Fn(RigState) + Send + Sync>>();
|
||||
|
||||
spawn_qt_thread(update_tx, listen_addr, rig_tx);
|
||||
spawn_state_watcher(state_rx, update_rx).await;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_qt_thread(
|
||||
update_tx: oneshot::Sender<Box<dyn Fn(RigState) + Send + Sync>>,
|
||||
listen_addr: SocketAddr,
|
||||
rig_tx: mpsc::Sender<RigRequest>,
|
||||
) {
|
||||
thread::spawn(move || {
|
||||
let model_cell = Box::leak(Box::new(RefCell::new(RigStateModel::default())));
|
||||
let model_ptr = model_cell.as_ptr();
|
||||
model_cell.borrow_mut().rig_tx = Some(rig_tx);
|
||||
|
||||
let update = queued_callback(move |state: RigState| unsafe {
|
||||
// Safe as queued_callback executes on the Qt thread where the model lives.
|
||||
let model_cell = &mut *model_ptr;
|
||||
update_model(model_cell, &state);
|
||||
});
|
||||
|
||||
if update_tx.send(Box::new(update)).is_err() {
|
||||
warn!("Qt frontend update channel dropped before init");
|
||||
}
|
||||
|
||||
let mut engine = QmlEngine::new();
|
||||
engine.set_object_property("rig".into(), unsafe { QObjectPinned::new(model_cell) });
|
||||
|
||||
let qml_path = qml_main_path();
|
||||
info!("Qt frontend loading QML from {}", qml_path.display());
|
||||
engine.load_file(QString::from(qml_path.to_string_lossy().to_string()));
|
||||
info!("Qt frontend running (addr hint: {})", listen_addr);
|
||||
engine.exec();
|
||||
});
|
||||
}
|
||||
|
||||
async fn spawn_state_watcher(
|
||||
mut state_rx: watch::Receiver<RigState>,
|
||||
update_rx: oneshot::Receiver<Box<dyn Fn(RigState) + Send + Sync>>,
|
||||
) {
|
||||
let Ok(update) = update_rx.await else {
|
||||
warn!("Qt frontend update channel closed");
|
||||
return;
|
||||
};
|
||||
|
||||
update(state_rx.borrow().clone());
|
||||
while state_rx.changed().await.is_ok() {
|
||||
update(state_rx.borrow().clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn qml_main_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("qml")
|
||||
.join("Main.qml")
|
||||
}
|
||||
|
||||
#[derive(QObject, Default)]
|
||||
struct RigStateModel {
|
||||
base: qt_base_class!(trait QObject),
|
||||
rig_tx: Option<mpsc::Sender<RigRequest>>,
|
||||
freq_hz: qt_property!(u64; NOTIFY freq_hz_changed),
|
||||
freq_hz_changed: qt_signal!(),
|
||||
freq_text: qt_property!(QString; NOTIFY freq_text_changed),
|
||||
freq_text_changed: qt_signal!(),
|
||||
mode: qt_property!(QString; NOTIFY mode_changed),
|
||||
mode_changed: qt_signal!(),
|
||||
band: qt_property!(QString; NOTIFY band_changed),
|
||||
band_changed: qt_signal!(),
|
||||
tx_enabled: qt_property!(bool; NOTIFY tx_enabled_changed),
|
||||
tx_enabled_changed: qt_signal!(),
|
||||
locked: qt_property!(bool; NOTIFY locked_changed),
|
||||
locked_changed: qt_signal!(),
|
||||
powered: qt_property!(bool; NOTIFY powered_changed),
|
||||
powered_changed: qt_signal!(),
|
||||
rx_sig: qt_property!(i32; NOTIFY rx_sig_changed),
|
||||
rx_sig_changed: qt_signal!(),
|
||||
tx_power: qt_property!(i32; NOTIFY tx_power_changed),
|
||||
tx_power_changed: qt_signal!(),
|
||||
tx_limit: qt_property!(i32; NOTIFY tx_limit_changed),
|
||||
tx_limit_changed: qt_signal!(),
|
||||
tx_swr: qt_property!(f64; NOTIFY tx_swr_changed),
|
||||
tx_swr_changed: qt_signal!(),
|
||||
tx_alc: qt_property!(i32; NOTIFY tx_alc_changed),
|
||||
tx_alc_changed: qt_signal!(),
|
||||
vfo: qt_property!(QString; NOTIFY vfo_changed),
|
||||
vfo_changed: qt_signal!(),
|
||||
set_freq_hz: qt_method!(
|
||||
fn set_freq_hz(&self, hz: i64) {
|
||||
if hz <= 0 {
|
||||
return;
|
||||
}
|
||||
self.send_command(RigCommand::SetFreq(trx_core::radio::freq::Freq {
|
||||
hz: hz as u64,
|
||||
}));
|
||||
}
|
||||
),
|
||||
set_mode: qt_method!(
|
||||
fn set_mode(&self, mode: QString) {
|
||||
let mode = parse_mode(&mode.to_string());
|
||||
self.send_command(RigCommand::SetMode(mode));
|
||||
}
|
||||
),
|
||||
toggle_ptt: qt_method!(
|
||||
fn toggle_ptt(&self) {
|
||||
self.send_command(RigCommand::SetPtt(!self.tx_enabled));
|
||||
}
|
||||
),
|
||||
toggle_power: qt_method!(
|
||||
fn toggle_power(&self) {
|
||||
if self.powered {
|
||||
self.send_command(RigCommand::PowerOff);
|
||||
} else {
|
||||
self.send_command(RigCommand::PowerOn);
|
||||
}
|
||||
}
|
||||
),
|
||||
toggle_vfo: qt_method!(
|
||||
fn toggle_vfo(&self) {
|
||||
self.send_command(RigCommand::ToggleVfo);
|
||||
}
|
||||
),
|
||||
lock_panel: qt_method!(
|
||||
fn lock_panel(&self) {
|
||||
self.send_command(RigCommand::Lock);
|
||||
}
|
||||
),
|
||||
unlock_panel: qt_method!(
|
||||
fn unlock_panel(&self) {
|
||||
self.send_command(RigCommand::Unlock);
|
||||
}
|
||||
),
|
||||
set_tx_limit: qt_method!(
|
||||
fn set_tx_limit(&self, limit: i32) {
|
||||
if limit < 0 {
|
||||
return;
|
||||
}
|
||||
self.send_command(RigCommand::SetTxLimit(limit as u8));
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
impl RigStateModel {
|
||||
fn send_command(&self, cmd: RigCommand) {
|
||||
let Some(tx) = self.rig_tx.as_ref() else {
|
||||
warn!("Qt frontend: rig command dropped (channel not set)");
|
||||
return;
|
||||
};
|
||||
|
||||
let (resp_tx, _resp_rx) = oneshot::channel();
|
||||
if tx
|
||||
.blocking_send(RigRequest {
|
||||
cmd,
|
||||
respond_to: resp_tx,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
warn!("Qt frontend: rig command send failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_model(model: &mut RigStateModel, state: &RigState) {
|
||||
let freq_hz = state.status.freq.hz;
|
||||
if model.freq_hz != freq_hz {
|
||||
model.freq_hz = freq_hz;
|
||||
model.freq_hz_changed();
|
||||
}
|
||||
|
||||
let freq_text = QString::from(format_freq(freq_hz));
|
||||
if model.freq_text != freq_text {
|
||||
model.freq_text = freq_text;
|
||||
model.freq_text_changed();
|
||||
}
|
||||
|
||||
let mode = QString::from(mode_label(&state.status.mode));
|
||||
if model.mode != mode {
|
||||
model.mode = mode;
|
||||
model.mode_changed();
|
||||
}
|
||||
|
||||
let band = QString::from(state.band_name().unwrap_or_else(|| "--".to_string()));
|
||||
if model.band != band {
|
||||
model.band = band;
|
||||
model.band_changed();
|
||||
}
|
||||
|
||||
if model.tx_enabled != state.status.tx_en {
|
||||
model.tx_enabled = state.status.tx_en;
|
||||
model.tx_enabled_changed();
|
||||
}
|
||||
|
||||
let locked = state.status.lock.unwrap_or(false);
|
||||
if model.locked != locked {
|
||||
model.locked = locked;
|
||||
model.locked_changed();
|
||||
}
|
||||
|
||||
let powered = state.control.enabled.unwrap_or(false);
|
||||
if model.powered != powered {
|
||||
model.powered = powered;
|
||||
model.powered_changed();
|
||||
}
|
||||
|
||||
let rx_sig = state.status.rx.as_ref().and_then(|rx| rx.sig).unwrap_or(0);
|
||||
if model.rx_sig != rx_sig {
|
||||
model.rx_sig = rx_sig;
|
||||
model.rx_sig_changed();
|
||||
}
|
||||
|
||||
let tx_power = state
|
||||
.status
|
||||
.tx
|
||||
.as_ref()
|
||||
.and_then(|tx| tx.power)
|
||||
.map(i32::from)
|
||||
.unwrap_or(0);
|
||||
if model.tx_power != tx_power {
|
||||
model.tx_power = tx_power;
|
||||
model.tx_power_changed();
|
||||
}
|
||||
|
||||
let tx_limit = state
|
||||
.status
|
||||
.tx
|
||||
.as_ref()
|
||||
.and_then(|tx| tx.limit)
|
||||
.map(i32::from)
|
||||
.unwrap_or(0);
|
||||
if model.tx_limit != tx_limit {
|
||||
model.tx_limit = tx_limit;
|
||||
model.tx_limit_changed();
|
||||
}
|
||||
|
||||
let tx_swr = state
|
||||
.status
|
||||
.tx
|
||||
.as_ref()
|
||||
.and_then(|tx| tx.swr)
|
||||
.unwrap_or(0.0) as f64;
|
||||
if (model.tx_swr - tx_swr).abs() > f64::EPSILON {
|
||||
model.tx_swr = tx_swr;
|
||||
model.tx_swr_changed();
|
||||
}
|
||||
|
||||
let tx_alc = state
|
||||
.status
|
||||
.tx
|
||||
.as_ref()
|
||||
.and_then(|tx| tx.alc)
|
||||
.map(i32::from)
|
||||
.unwrap_or(0);
|
||||
if model.tx_alc != tx_alc {
|
||||
model.tx_alc = tx_alc;
|
||||
model.tx_alc_changed();
|
||||
}
|
||||
|
||||
let vfo = QString::from(vfo_label(state));
|
||||
if model.vfo != vfo {
|
||||
model.vfo = vfo;
|
||||
model.vfo_changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn format_freq(hz: u64) -> String {
|
||||
if hz >= 1_000_000_000 {
|
||||
format!("{:.3} GHz", hz as f64 / 1_000_000_000.0)
|
||||
} else if hz >= 10_000_000 {
|
||||
format!("{:.3} MHz", hz as f64 / 1_000_000.0)
|
||||
} else if hz >= 1_000 {
|
||||
format!("{:.1} kHz", hz as f64 / 1_000.0)
|
||||
} else {
|
||||
format!("{hz} Hz")
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_label(mode: &RigMode) -> String {
|
||||
match mode {
|
||||
RigMode::LSB => "LSB".to_string(),
|
||||
RigMode::USB => "USB".to_string(),
|
||||
RigMode::CW => "CW".to_string(),
|
||||
RigMode::CWR => "CWR".to_string(),
|
||||
RigMode::AM => "AM".to_string(),
|
||||
RigMode::WFM => "WFM".to_string(),
|
||||
RigMode::FM => "FM".to_string(),
|
||||
RigMode::DIG => "DIG".to_string(),
|
||||
RigMode::PKT => "PKT".to_string(),
|
||||
RigMode::Other(val) => val.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mode(value: &str) -> RigMode {
|
||||
match value.trim().to_uppercase().as_str() {
|
||||
"LSB" => RigMode::LSB,
|
||||
"USB" => RigMode::USB,
|
||||
"CW" => RigMode::CW,
|
||||
"CWR" => RigMode::CWR,
|
||||
"AM" => RigMode::AM,
|
||||
"FM" => RigMode::FM,
|
||||
"WFM" => RigMode::WFM,
|
||||
"DIG" | "DIGI" => RigMode::DIG,
|
||||
"PKT" | "PACKET" => RigMode::PKT,
|
||||
other => RigMode::Other(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn vfo_label(state: &RigState) -> String {
|
||||
let Some(vfo) = state.status.vfo.as_ref() else {
|
||||
return "--".to_string();
|
||||
};
|
||||
|
||||
let mut lines = Vec::new();
|
||||
for (idx, entry) in vfo.entries.iter().enumerate() {
|
||||
let marker = if vfo.active == Some(idx) { "*" } else { " " };
|
||||
let freq = format_freq(entry.freq.hz);
|
||||
let mode = entry
|
||||
.mode
|
||||
.as_ref()
|
||||
.map(mode_label)
|
||||
.unwrap_or_else(|| "--".to_string());
|
||||
lines.push(format!("{marker} {}: {} {}", entry.name, freq, mode));
|
||||
}
|
||||
lines.join("\\n")
|
||||
}
|
||||
@@ -47,7 +47,3 @@ listen = "127.0.0.1"
|
||||
port = 0
|
||||
# List of accepted bearer tokens (empty = no auth)
|
||||
# auth.tokens = ["example-token"]
|
||||
|
||||
[frontends.qt]
|
||||
# Enable Qt/QML GUI frontend (Linux only, requires system Qt6)
|
||||
enabled = false
|
||||
|
||||
Reference in New Issue
Block a user