672 lines
21 KiB
Rust
672 lines
21 KiB
Rust
use std::collections::HashMap;
|
||
use std::io::{Read, Write};
|
||
use std::net::{Shutdown, TcpStream, ToSocketAddrs};
|
||
use std::sync::{Mutex, OnceLock};
|
||
use std::thread;
|
||
use std::time::Duration;
|
||
|
||
use anyhow::{Context, Result};
|
||
use tracing::{debug, warn};
|
||
|
||
use crate::DeviceIdentity;
|
||
use crate::capabilities::{
|
||
PlaybackPosition, PlaybackPositionInfo, PlaybackState, PlaybackStatus, TransportControl,
|
||
VolumeControl,
|
||
};
|
||
use crate::errors::ControlPointError;
|
||
use crate::linkplay_renderer::{extract_linkplay_host, parse_flat_json};
|
||
use crate::model::RendererInfo;
|
||
use std::time::Instant;
|
||
|
||
// Garde global pour respecter le délai de 200ms entre commandes
|
||
static LAST_COMMAND_TIME: OnceLock<Mutex<Instant>> = OnceLock::new();
|
||
|
||
fn last_command_time() -> &'static Mutex<Instant> {
|
||
LAST_COMMAND_TIME.get_or_init(|| Mutex::new(Instant::now()))
|
||
}
|
||
|
||
const ARYLIC_TCP_PORT: u16 = 8899;
|
||
const PACKET_HEADER: [u8; 4] = [0x18, 0x96, 0x18, 0x20];
|
||
const RESERVED_BYTES: [u8; 8] = [0; 8];
|
||
const MAX_RESPONSE_ATTEMPTS: usize = 8;
|
||
const DEFAULT_TIMEOUT_SECS: u64 = 3;
|
||
|
||
static DETECTION_CACHE: OnceLock<Mutex<HashMap<String, bool>>> = OnceLock::new();
|
||
|
||
/// Mode d’attente de réponse pour une commande TCP Arylic.
|
||
enum ResponseMode<'a> {
|
||
/// On n’attend aucune réponse (fire-and-forget).
|
||
None,
|
||
/// On attend une réponse, mais si la lecture échoue immédiatement, on traite comme succès.
|
||
Optional(&'a [&'a str]),
|
||
/// On attend une réponse, et l’absence de réponse est une erreur.
|
||
Required(&'a [&'a str]),
|
||
}
|
||
|
||
fn detection_cache() -> &'static Mutex<HashMap<String, bool>> {
|
||
DETECTION_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
||
}
|
||
|
||
/// Probe whether the renderer at the given location exposes the Arylic TCP API.
|
||
pub(crate) fn detect_arylic_tcp(location: &str, timeout: Duration) -> bool {
|
||
let Some(host) = extract_linkplay_host(location) else {
|
||
return false;
|
||
};
|
||
|
||
if let Ok(cache) = detection_cache().lock() {
|
||
if let Some(result) = cache.get(&host) {
|
||
return *result;
|
||
}
|
||
}
|
||
|
||
let detected = match try_detect_tcp(&host, timeout) {
|
||
Ok(_) => true,
|
||
Err(err) => {
|
||
debug!(
|
||
"Arylic TCP detection failed for {} (host={}): {}",
|
||
location, host, err
|
||
);
|
||
false
|
||
}
|
||
};
|
||
|
||
if let Ok(mut cache) = detection_cache().lock() {
|
||
cache.insert(host, detected);
|
||
}
|
||
|
||
detected
|
||
}
|
||
|
||
fn try_detect_tcp(host: &str, timeout: Duration) -> Result<(), ControlPointError> {
|
||
let payload = send_command_required(
|
||
host,
|
||
ARYLIC_TCP_PORT,
|
||
timeout,
|
||
"MCU+INF+GET",
|
||
&["AXX+INF+", "AXX+DEV+"],
|
||
)?;
|
||
|
||
if payload.starts_with("AXX+INF+") || payload.starts_with("AXX+DEV+") {
|
||
Ok(())
|
||
} else {
|
||
Err(ControlPointError::ArilycTcpError(format!(
|
||
"Unexpected INF response from {}: {}",
|
||
host,
|
||
payload
|
||
)))
|
||
}
|
||
}
|
||
|
||
/// Backend speaking the Arylic TCP control protocol (port 8899).
|
||
#[derive(Clone, Debug)]
|
||
pub struct ArylicTcpRenderer {
|
||
host: String,
|
||
port: u16,
|
||
timeout: Duration,
|
||
}
|
||
|
||
impl ArylicTcpRenderer {
|
||
pub fn from_renderer_info(info: RendererInfo) -> Result<Self, ControlPointError> {
|
||
let host = extract_linkplay_host(info.location())
|
||
.ok_or_else(|| ControlPointError::ArilycTcpError(format!("Renderer {} has no valid LOCATION host", info.udn())))?;
|
||
|
||
Ok(Self {
|
||
host,
|
||
port: ARYLIC_TCP_PORT,
|
||
timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||
})
|
||
}
|
||
|
||
fn send_required(&self, cmd: &str, expected: &[&str]) -> Result<String, ControlPointError> {
|
||
send_command_required(&self.host, self.port, self.timeout, cmd, expected)
|
||
}
|
||
|
||
fn send_optional(&self, cmd: &str, expected: &[&str]) -> Result<Option<String>, ControlPointError> {
|
||
send_command_optional(&self.host, self.port, self.timeout, cmd, expected)
|
||
}
|
||
|
||
fn send_no_response(&self, cmd: &str) -> Result<(), ControlPointError> {
|
||
send_command_no_response(&self.host, self.port, self.timeout, cmd)
|
||
}
|
||
|
||
fn fetch_playback_info(&self) -> Result<ArylicPlaybackInfo, ControlPointError> {
|
||
let payload = self.send_required("MCU+PINFGET", &["AXX+PLY+INF"])?;
|
||
match parse_playback_info(&payload) {
|
||
Ok(info) => Ok(info),
|
||
Err(err) => {
|
||
debug!(
|
||
"Failed to parse Arylic playback info for {}: {}",
|
||
self.host, err
|
||
);
|
||
Err(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn format_volume_command(value: u16) -> String {
|
||
format!("MCU+VOL+{:03}", value.min(100))
|
||
}
|
||
|
||
fn parse_volume_payload(payload: &str) -> Result<u16, ControlPointError> {
|
||
let data = payload
|
||
.strip_prefix("AXX+VOL+")
|
||
.ok_or_else(|| ControlPointError::ArilycTcpError(format!("Unexpected volume response: {}", payload)))?;
|
||
let value: u16 = data
|
||
.trim()
|
||
.parse()
|
||
.map_err(|_| ControlPointError::ArilycTcpError(format!("Invalid volume value: {}", data)))?;
|
||
Ok(value.min(100))
|
||
}
|
||
|
||
fn parse_mute_payload(payload: &str) -> Result<bool, ControlPointError> {
|
||
let data = payload
|
||
.strip_prefix("AXX+MUT+")
|
||
.ok_or_else(|| ControlPointError::ArilycTcpError(format!("Unexpected mute response: {}", payload)))?;
|
||
match data.trim() {
|
||
"000" | "0" => Ok(false),
|
||
"001" | "1" => Ok(true),
|
||
other => Err(ControlPointError::ArilycTcpError(format!("Invalid mute value: {}", other))),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl TransportControl for ArylicTcpRenderer {
|
||
fn play_uri(&self, _uri: &str, _meta: &str) -> Result<(), ControlPointError> {
|
||
Err(ControlPointError::upnp_operation_not_supported(
|
||
"Arylic TCP backend does not support direct URL loading.",
|
||
"ArylicTcpRenderer",
|
||
))
|
||
}
|
||
|
||
fn play(&self) -> Result<(), ControlPointError> {
|
||
self.send_no_response("MCU+PLY-PLA")
|
||
}
|
||
|
||
fn pause(&self) -> Result<(), ControlPointError> {
|
||
let _ = self.send_optional("MCU+PLY-PUS", &["AXX+PLY+"])?;
|
||
Ok(())
|
||
}
|
||
|
||
fn stop(&self) -> Result<(), ControlPointError> {
|
||
self.send_no_response("MCU+PLY-STP")
|
||
}
|
||
|
||
fn seek_rel_time(&self, hhmmss: &str) -> Result<(), ControlPointError> {
|
||
let _ = parse_hhmmss(hhmmss)?;
|
||
Err(ControlPointError::ArilycTcpError(format!(
|
||
"Arylic TCP seek_rel_time is not implemented yet for this device."
|
||
)))
|
||
}
|
||
}
|
||
|
||
impl VolumeControl for ArylicTcpRenderer {
|
||
fn volume(&self) -> Result<u16, ControlPointError> {
|
||
if let Ok(info) = self.fetch_playback_info() {
|
||
if let Some(vol) = info.volume {
|
||
return Ok(vol);
|
||
}
|
||
debug!(
|
||
"Arylic playback info for {} missing volume, falling back to VOL GET",
|
||
self.host
|
||
);
|
||
}
|
||
let payload = self.send_required("MCU+VOL+GET", &["AXX+VOL+"])?;
|
||
Self::parse_volume_payload(&payload)
|
||
}
|
||
|
||
fn set_volume(&self, v: u16) -> Result<(), ControlPointError> {
|
||
let command = Self::format_volume_command(v);
|
||
let _ = self.send_optional(&command, &["AXX+VOL+"])?;
|
||
Ok(())
|
||
}
|
||
|
||
fn mute(&self) -> Result<bool, ControlPointError> {
|
||
if let Ok(info) = self.fetch_playback_info() {
|
||
if let Some(mute) = info.mute {
|
||
return Ok(mute);
|
||
}
|
||
debug!(
|
||
"Arylic playback info for {} missing mute, falling back to MUT GET",
|
||
self.host
|
||
);
|
||
}
|
||
let payload = self.send_required("MCU+MUT+GET", &["AXX+MUT+"])?;
|
||
Self::parse_mute_payload(&payload)
|
||
}
|
||
|
||
fn set_mute(&self, m: bool) -> Result<(), ControlPointError> {
|
||
let command = if m { "MCU+MUT+001" } else { "MCU+MUT+000" };
|
||
let payload = self.send_required(command, &["AXX+MUT+"])?;
|
||
let _ = Self::parse_mute_payload(&payload)?;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl PlaybackStatus for ArylicTcpRenderer {
|
||
fn playback_state(&self) -> Result<PlaybackState, ControlPointError> {
|
||
let info = self.fetch_playback_info()?;
|
||
Ok(info.playback_state())
|
||
}
|
||
}
|
||
|
||
impl PlaybackPosition for ArylicTcpRenderer {
|
||
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
|
||
let info = self.fetch_playback_info()?;
|
||
Ok(info.position_info())
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
struct ArylicPlaybackInfo {
|
||
status_raw: String,
|
||
curpos_ms: u64,
|
||
totlen_ms: u64,
|
||
volume: Option<u16>,
|
||
mute: Option<bool>,
|
||
playlist_size: Option<u32>,
|
||
track_index: Option<u32>,
|
||
}
|
||
|
||
impl ArylicPlaybackInfo {
|
||
fn playback_state(&self) -> PlaybackState {
|
||
match self.status_raw.as_str() {
|
||
"play" => PlaybackState::Playing,
|
||
"pause" => PlaybackState::Paused,
|
||
"stop" => PlaybackState::Stopped,
|
||
other => PlaybackState::Unknown(other.to_string()),
|
||
}
|
||
}
|
||
|
||
fn position_info(&self) -> PlaybackPositionInfo {
|
||
let track = match (self.track_index, self.playlist_size) {
|
||
(Some(idx), Some(count)) if count > 0 => Some(idx.min(count)),
|
||
(Some(idx), _) => Some(idx),
|
||
_ => None,
|
||
};
|
||
|
||
PlaybackPositionInfo {
|
||
track,
|
||
rel_time: Some(format_hms(self.curpos_ms / 1000)),
|
||
abs_time: None,
|
||
track_duration: if self.totlen_ms > 0 {
|
||
Some(format_hms(self.totlen_ms / 1000))
|
||
} else {
|
||
None
|
||
},
|
||
track_metadata: None,
|
||
track_uri: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn parse_playback_info(payload: &str) -> Result<ArylicPlaybackInfo, ControlPointError> {
|
||
let json_blob = payload
|
||
.strip_prefix("AXX+PLY+INF")
|
||
.ok_or_else(|| ControlPointError::ArilycTcpError(format!("Unexpected playback info prefix: {}", payload)))?;
|
||
|
||
let json_blob = json_blob.trim_end_matches('&').trim();
|
||
let map = parse_flat_json(json_blob)?;
|
||
|
||
let status_raw = map
|
||
.get("status")
|
||
.cloned()
|
||
.ok_or_else(|| ControlPointError::ArilycTcpError(format!("Playback info missing `status` field")))?;
|
||
|
||
let curpos_ms = parse_u64_field(&map, "curpos")?;
|
||
let totlen_ms = parse_u64_field(&map, "totlen")?;
|
||
|
||
let volume = match map.get("vol") {
|
||
Some(raw) => match raw.parse::<u16>() {
|
||
Ok(value) => Some(value.min(100)),
|
||
Err(err) => {
|
||
debug!("Invalid Arylic `vol` value {}: {}", raw, err);
|
||
None
|
||
}
|
||
},
|
||
None => None,
|
||
};
|
||
|
||
let mute = match map.get("mute") {
|
||
Some(value) if value == "1" => Some(true),
|
||
Some(value) if value == "0" => Some(false),
|
||
Some(other) => {
|
||
debug!("Invalid Arylic `mute` value {}", other);
|
||
None
|
||
}
|
||
None => None,
|
||
};
|
||
|
||
let playlist_size = map
|
||
.get("plicount")
|
||
.and_then(|raw| match raw.parse::<u32>() {
|
||
Ok(count) if count > 0 => Some(count),
|
||
Ok(_) => None,
|
||
Err(err) => {
|
||
debug!("Invalid Arylic `plicount` value {}: {}", raw, err);
|
||
None
|
||
}
|
||
});
|
||
|
||
let track_index = map.get("plicurr").and_then(|raw| match raw.parse::<u32>() {
|
||
Ok(idx) if idx > 0 => Some(idx),
|
||
Ok(_) => None,
|
||
Err(err) => {
|
||
debug!("Invalid Arylic `plicurr` value {}: {}", raw, err);
|
||
None
|
||
}
|
||
});
|
||
|
||
Ok(ArylicPlaybackInfo {
|
||
status_raw,
|
||
curpos_ms,
|
||
totlen_ms,
|
||
volume,
|
||
mute,
|
||
playlist_size,
|
||
track_index,
|
||
})
|
||
}
|
||
|
||
fn parse_u64_field(map: &HashMap<String, String>, key: &str) -> Result<u64, ControlPointError> {
|
||
let raw = map
|
||
.get(key)
|
||
.ok_or_else(|| ControlPointError::ArilycTcpError(format!("Playback info missing `{}` field", key)))?;
|
||
raw.parse::<u64>()
|
||
.map_err(|_| ControlPointError::ArilycTcpError(format!("Invalid `{}` value: {}", key, raw)))
|
||
}
|
||
|
||
fn connect(host: &str, port: u16, timeout: Duration) -> Result<TcpStream, ControlPointError> {
|
||
if let Ok(mut last_time) = last_command_time().lock() {
|
||
let elapsed = last_time.elapsed();
|
||
if elapsed < Duration::from_millis(200) {
|
||
let wait = Duration::from_millis(200) - elapsed;
|
||
debug!(
|
||
"Waiting {:?} before sending command to respect 200ms interval",
|
||
wait
|
||
);
|
||
thread::sleep(wait);
|
||
}
|
||
*last_time = Instant::now();
|
||
}
|
||
|
||
let address = if host.contains(':') {
|
||
format!("[{}]:{}", host, port)
|
||
} else {
|
||
format!("{host}:{port}")
|
||
};
|
||
|
||
let mut last_err = None;
|
||
for addr in address
|
||
.to_socket_addrs()
|
||
.with_context(|| format!("Failed to resolve {}:{}", host, port))
|
||
.map_err(|_| {
|
||
ControlPointError::ArilycTcpError(format!("Failed to resolve {}:{}", host, port))
|
||
})?
|
||
{
|
||
match TcpStream::connect_timeout(&addr, timeout) {
|
||
Ok(stream) => {
|
||
stream
|
||
.set_read_timeout(Some(timeout))
|
||
.and_then(|_| stream.set_write_timeout(Some(timeout)))
|
||
.with_context(|| format!("Failed to set socket timeouts for {}", address))
|
||
.map_err(|err| {
|
||
ControlPointError::ArilycTcpError(format!(
|
||
"Failed to set socket timeouts for {}",
|
||
address
|
||
))
|
||
})?;
|
||
|
||
return Ok(stream);
|
||
}
|
||
Err(err) => {
|
||
last_err = Some((addr, err));
|
||
}
|
||
}
|
||
}
|
||
|
||
match last_err {
|
||
Some((addr, err)) => Err(ControlPointError::ArilycTcpError(format!(
|
||
"Failed to connect to {} via {}: {}",
|
||
host, addr, err
|
||
))),
|
||
None => Err(ControlPointError::ArilycTcpError(format!(
|
||
"No socket addresses resolved for {}",
|
||
address
|
||
))),
|
||
}
|
||
}
|
||
|
||
fn encode_packet(payload: &str) -> Vec<u8> {
|
||
let bytes = payload.as_bytes();
|
||
let len = bytes.len() as u32;
|
||
let checksum = bytes.iter().fold(0u32, |acc, b| acc + (*b as u32));
|
||
|
||
let mut out = Vec::with_capacity(4 + 4 + 4 + 8 + bytes.len());
|
||
out.extend_from_slice(&PACKET_HEADER);
|
||
out.extend_from_slice(&len.to_le_bytes());
|
||
out.extend_from_slice(&checksum.to_le_bytes());
|
||
out.extend_from_slice(&RESERVED_BYTES);
|
||
out.extend_from_slice(bytes);
|
||
out
|
||
}
|
||
|
||
fn read_packet(stream: &mut TcpStream) -> Result<String, ControlPointError> {
|
||
let mut header = [0u8; 4];
|
||
stream.read_exact(&mut header)
|
||
.map_err(|e| ControlPointError::ArilycTcpError(format!("{}",e)))?;
|
||
if header != PACKET_HEADER {
|
||
return Err(ControlPointError::ArilycTcpError(format!("Invalid Arylic packet header: {:x?}", header)));
|
||
}
|
||
|
||
let mut len_buf = [0u8; 4];
|
||
stream.read_exact(&mut len_buf)
|
||
.map_err(|e| ControlPointError::ArilycTcpError(format!("{}",e)))?;
|
||
let len = u32::from_le_bytes(len_buf) as usize;
|
||
|
||
let mut checksum_buf = [0u8; 4];
|
||
stream.read_exact(&mut checksum_buf)
|
||
.map_err(|e| ControlPointError::ArilycTcpError(format!("{}",e)))?;
|
||
let expected_checksum = u32::from_le_bytes(checksum_buf);
|
||
|
||
let mut reserved = [0u8; 8];
|
||
stream.read_exact(&mut reserved)
|
||
.map_err(|e| ControlPointError::ArilycTcpError(format!("{}",e)))?;
|
||
|
||
let mut payload = vec![0u8; len];
|
||
stream.read_exact(&mut payload)
|
||
.map_err(|e| ControlPointError::ArilycTcpError(format!("{}",e)))?;
|
||
|
||
let actual_checksum = payload.iter().fold(0u32, |acc, b| acc + (*b as u32));
|
||
if actual_checksum != expected_checksum {
|
||
warn!(
|
||
"Arylic payload checksum mismatch: expected={} actual={}",
|
||
expected_checksum, actual_checksum
|
||
);
|
||
}
|
||
|
||
Ok(String::from_utf8(payload)
|
||
.map_err(|e| ControlPointError::ArilycTcpError(format!("{}",e)))?)
|
||
}
|
||
|
||
fn format_hms(secs: u64) -> String {
|
||
let h = secs / 3600;
|
||
let m = (secs % 3600) / 60;
|
||
let s = secs % 60;
|
||
format!("{:02}:{:02}:{:02}", h, m, s)
|
||
}
|
||
|
||
fn parse_hhmmss(value: &str) -> Result<u64, ControlPointError> {
|
||
let parts: Vec<_> = value.split(':').collect();
|
||
if parts.len() != 3 {
|
||
return Err(ControlPointError::ArilycTcpError(format!(
|
||
"Invalid time format `{}`. Expected HH:MM:SS.",
|
||
value
|
||
)));
|
||
}
|
||
|
||
let hours: u64 = parts[0]
|
||
.parse()
|
||
.map_err(|_| ControlPointError::ArilycTcpError(format!("Invalid hour component in {}", value)))?;
|
||
let minutes: u64 = parts[1]
|
||
.parse()
|
||
.map_err(|_| ControlPointError::ArilycTcpError(format!("Invalid minute component in {}", value)))?;
|
||
let seconds: u64 = parts[2]
|
||
.parse()
|
||
.map_err(|_| ControlPointError::ArilycTcpError(format!("Invalid second component in {}", value)))?;
|
||
|
||
if minutes > 59 || seconds > 59 {
|
||
return Err(ControlPointError::ArilycTcpError(format!(
|
||
"Invalid HH:MM:SS value `{}`. Minutes and seconds must be < 60.",
|
||
value
|
||
)));
|
||
}
|
||
|
||
Ok(hours * 3600 + minutes * 60 + seconds)
|
||
}
|
||
|
||
fn send_command_with_mode(
|
||
host: &str,
|
||
port: u16,
|
||
timeout: Duration,
|
||
payload: &str,
|
||
mode: ResponseMode<'_>,
|
||
) -> Result<Option<String>, ControlPointError> {
|
||
let mut stream = connect(host, port, timeout)?;
|
||
let packet = encode_packet(payload);
|
||
|
||
stream.write_all(&packet).map_err(|_| {
|
||
ControlPointError::ArilycTcpError(
|
||
format!(
|
||
"Failed to write Arylic TCP packet for {}: {}",
|
||
host, payload
|
||
))
|
||
})?;
|
||
|
||
stream.flush().map_err(|_| {
|
||
ControlPointError::ArilycTcpError(
|
||
format!(
|
||
"Failed to flush Arylic TCP stream for {} (command {})",
|
||
host, payload
|
||
))
|
||
})?;
|
||
|
||
match mode {
|
||
ResponseMode::None => {
|
||
debug!(
|
||
"Arylic TCP fire-and-forget command sent to {}: {}",
|
||
host, payload
|
||
);
|
||
let _ = stream.shutdown(Shutdown::Write);
|
||
Ok(None)
|
||
}
|
||
ResponseMode::Required(expected) => {
|
||
read_expected_response(&mut stream, host, payload, expected).map(Some)
|
||
}
|
||
ResponseMode::Optional(expected) => {
|
||
for _ in 0..MAX_RESPONSE_ATTEMPTS {
|
||
match read_packet(&mut stream) {
|
||
Ok(response) => {
|
||
if expected.iter().any(|p| response.starts_with(p)) {
|
||
return Ok(Some(response));
|
||
}
|
||
debug!(
|
||
"Ignoring unsolicited Arylic payload from {}: {}",
|
||
host, response
|
||
);
|
||
}
|
||
Err(err) => {
|
||
debug!(
|
||
"No full response for Arylic TCP command {} on {}: {}. Treating as success and relying on PINFGET.",
|
||
payload, host, err
|
||
);
|
||
return Ok(None);
|
||
}
|
||
}
|
||
}
|
||
|
||
Err(ControlPointError::ArilycTcpError(format!(
|
||
"No expected response for optional command {} on {}",
|
||
payload,
|
||
host
|
||
)))
|
||
}
|
||
}
|
||
}
|
||
|
||
fn read_expected_response(
|
||
stream: &mut TcpStream,
|
||
host: &str,
|
||
payload: &str,
|
||
expected: &[&str],
|
||
) -> Result<String, ControlPointError> {
|
||
for _ in 0..MAX_RESPONSE_ATTEMPTS {
|
||
let response = match read_packet(stream) {
|
||
Ok(resp) => resp,
|
||
Err(err) => {
|
||
return Err(ControlPointError::ArilycTcpError(format!(
|
||
"Failed to read Arylic TCP response for {} (command {}): {}",
|
||
host,
|
||
payload,
|
||
err
|
||
)));
|
||
}
|
||
};
|
||
if expected.iter().any(|prefix| response.starts_with(prefix)) {
|
||
return Ok(response);
|
||
}
|
||
|
||
debug!(
|
||
"Ignoring unsolicited Arylic payload from {}: {}",
|
||
host, response
|
||
);
|
||
}
|
||
|
||
Err(ControlPointError::ArilycTcpError(format!(
|
||
"No expected response for command {} on {}",
|
||
payload,
|
||
host
|
||
)))
|
||
}
|
||
|
||
fn send_command_required(
|
||
host: &str,
|
||
port: u16,
|
||
timeout: Duration,
|
||
payload: &str,
|
||
expected: &[&str],
|
||
) -> Result<String, ControlPointError> {
|
||
match send_command_with_mode(
|
||
host,
|
||
port,
|
||
timeout,
|
||
payload,
|
||
ResponseMode::Required(expected),
|
||
)? {
|
||
Some(s) => Ok(s),
|
||
None => Err(ControlPointError::ArilycTcpError(format!(
|
||
"Arylic TCP: no response payload for required command {}",
|
||
payload
|
||
))),
|
||
}
|
||
}
|
||
|
||
fn send_command_optional(
|
||
host: &str,
|
||
port: u16,
|
||
timeout: Duration,
|
||
payload: &str,
|
||
expected: &[&str],
|
||
) -> Result<Option<String>, ControlPointError> {
|
||
send_command_with_mode(
|
||
host,
|
||
port,
|
||
timeout,
|
||
payload,
|
||
ResponseMode::Optional(expected),
|
||
)
|
||
}
|
||
|
||
fn send_command_no_response(host: &str, port: u16, timeout: Duration, payload: &str) -> Result<(), ControlPointError> {
|
||
send_command_with_mode(host, port, timeout, payload, ResponseMode::None).map(|_| ())
|
||
}
|