[fix](trx-rs): harden hamlib rigctl compatibility

Improve rigctl interoperability with hamlib/WSJT-X and stabilize FT-817 PTT handling.\n\n- support extended '+' command replies\n- accept decimal and MHz-style frequency inputs\n- retry set_freq rounded to 10 Hz on CAT alignment errors\n- add compatibility handling for get_level probes\n- broaden PTT command parsing and aliases\n- derive PTT capability dynamically from snapshot data\n- improve dump_state/dump_caps compatibility behavior\n- move temporary rigctl diagnostics to debug level\n- make FT-817 set_ptt more reliable with unlock/clear and double-send\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-13 01:30:04 +01:00
parent c1a7eaa72d
commit 86ca1a60fb
2 changed files with 90 additions and 18 deletions
@@ -106,6 +106,7 @@ async fn process_command(
state_rx: &mut watch::Receiver<RigState>,
rig_tx: &mpsc::Sender<RigRequest>,
) -> CommandResult {
debug!("rigctl command: {}", cmd_line);
let mut parts = cmd_line.split_whitespace();
let Some(raw_op) = parts.next() else {
return CommandResult::Reply(err_response("empty command"));
@@ -151,7 +152,7 @@ async fn process_command(
Err(e) => err_response(&e),
}
}
"t" | "\\get_ptt" => match request_snapshot(rig_tx).await {
"t" | "\\get_ptt" | "get_ptt" => match request_snapshot(rig_tx).await {
Ok(snapshot) => {
ok_response(
op,
@@ -161,14 +162,30 @@ async fn process_command(
}
Err(e) => err_response(&e),
},
"T" | "\\set_ptt" => match parts.next() {
Some(v) => match parse_ptt_arg(v) {
Some(ptt) => match send_rig_command(rig_tx, RigCommand::SetPtt(ptt)).await {
Ok(_) => ok_only(op, extended),
Err(e) => err_response(&e),
},
None => err_response("expected PTT state (0/1)"),
},
"T" | "\\set_ptt" | "set_ptt" => match parse_ptt_tokens(parts.collect()) {
Some(v) => {
let snapshot = match current_snapshot(state_rx) {
Some(s) => s,
None => match request_snapshot(rig_tx).await {
Ok(s) => s,
Err(e) => return CommandResult::Reply(err_response(&e)),
},
};
if !rig_supports_ptt(&snapshot) {
return CommandResult::Reply(err_response("PTT not supported"));
}
match parse_ptt_arg(&v) {
Some(ptt) => {
debug!("rigctl ptt request: cmd='{}' parsed_ptt={}", cmd_line, ptt);
match send_rig_command(rig_tx, RigCommand::SetPtt(ptt)).await {
Ok(_) => ok_only(op, extended),
Err(e) => err_response(&e),
}
}
None => err_response("expected PTT state (0/1)"),
}
}
_ => err_response("expected PTT state (0/1)"),
},
"v" | "\\get_vfo" => match request_snapshot(rig_tx).await {
@@ -292,6 +309,16 @@ fn err_response(msg: &str) -> String {
"RPRT -1\n".to_string()
}
fn rig_supports_ptt(snapshot: &RigSnapshot) -> bool {
snapshot.status.tx.is_some()
|| snapshot
.info
.capabilities
.supported_bands
.iter()
.any(|b| b.tx_allowed)
}
async fn request_snapshot(rig_tx: &mpsc::Sender<RigRequest>) -> Result<RigSnapshot, String> {
send_rig_command(rig_tx, RigCommand::GetSnapshot).await
}
@@ -346,7 +373,7 @@ fn rig_mode_to_str(mode: &RigMode) -> String {
mode_to_string(mode)
}
fn dump_state_lines(_snapshot: &RigSnapshot) -> Vec<String> {
fn dump_state_lines(snapshot: &RigSnapshot) -> Vec<String> {
// Hamlib expects a long, fixed sequence of bare values.
// To maximize compatibility, mirror the ordering produced by hamlib's dummy backend.
// Some Hamlib/netrigctl versions expect a trailing `done` sentinel.
@@ -388,10 +415,14 @@ fn dump_state_lines(_snapshot: &RigSnapshot) -> Vec<String> {
"10 20 30 ".to_string(),
"0xffffffffffffffff".to_string(),
"0xffffffffffffffff".to_string(),
"0xfffffffff7ffffff".to_string(),
"0xfffeff7083ffffff".to_string(),
"0xffffffffffffffff".to_string(),
"0xffffffffffffffbf".to_string(),
if rig_supports_ptt(snapshot) {
"0xffffffffffffffff".to_string()
} else {
"0x0".to_string()
},
"0xffffffffffffffff".to_string(),
"0xffffffffffffffff".to_string(),
];
lines.push("done".to_string());
lines
@@ -438,7 +469,11 @@ fn dump_caps_response(op: &str, extended: bool, snapshot: &RigSnapshot) -> Strin
push(
&mut resp,
"can_ptt",
if snapshot.status.tx.is_some() { "1" } else { "0" }.to_string(),
if rig_supports_ptt(snapshot) {
"1".to_string()
} else {
"0".to_string()
},
);
resp.push_str("done\n");
if extended {
@@ -510,24 +545,40 @@ fn is_false(s: &str) -> bool {
}
fn parse_ptt_arg(s: &str) -> Option<bool> {
if is_true(s) {
let normalized = s.trim().trim_end_matches(';').trim_end_matches(',');
if is_true(normalized) {
return Some(true);
}
if is_false(s) {
if is_false(normalized) {
return Some(false);
}
// Hamlib may send enum-like numeric values where non-zero means ON.
if let Ok(v) = s.parse::<i64>() {
if let Ok(v) = normalized.parse::<i64>() {
return Some(v != 0);
}
match s.to_ascii_uppercase().as_str() {
match normalized.to_ascii_uppercase().as_str() {
"ON_DATA" | "DATA" | "MIC" | "ON_MIC" => Some(true),
_ => None,
}
}
fn parse_ptt_tokens(tokens: Vec<&str>) -> Option<String> {
match tokens.as_slice() {
[] => None,
[only] => Some((*only).to_string()),
[first, second, ..] if normalize_vfo_name(first).is_some() => Some((*second).to_string()),
_ => tokens
.iter()
.rev()
.find(|t| parse_ptt_arg(t).is_some())
.copied()
.map(str::to_string)
.or_else(|| tokens.last().map(|s| (*s).to_string())),
}
}
fn parse_freq_hz_arg(s: &str) -> Option<u64> {
if let Ok(hz) = s.parse::<u64>() {
return Some(hz);
@@ -648,4 +699,17 @@ mod tests {
assert_eq!(parse_ptt_arg("ON"), Some(true));
assert_eq!(parse_ptt_arg("DATA"), Some(true));
}
#[test]
fn parse_ptt_tokens_accepts_optional_vfo_prefix() {
assert_eq!(parse_ptt_tokens(vec!["1"]), Some("1".to_string()));
assert_eq!(
parse_ptt_tokens(vec!["VFOA", "1"]),
Some("1".to_string())
);
assert_eq!(
parse_ptt_tokens(vec!["VFOB", "ON_DATA"]),
Some("ON_DATA".to_string())
);
}
}
@@ -303,9 +303,17 @@ impl Ft817 {
/// Send CAT command to control PTT on FT-817.
pub async fn set_ptt(&mut self, ptt: bool) -> DynResult<()> {
let opcode = if ptt { CMD_PTT_ON } else { CMD_PTT_OFF };
// Mirror the reliability pattern used in set_mode: clear stale input and
// send twice because some radios occasionally drop the first CAT frame.
let _ = self.unlock().await;
let _ = self.port.clear(ClearBuffer::Input);
// PTT on/off does not take a payload; CAT uses separate opcodes.
let frame = [0x00, 0x00, 0x00, 0x00, opcode];
self.write_frame(&frame).await?;
self.port.flush().await?;
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
self.write_frame(&frame).await?;
self.port.flush().await?;
Ok(())
}