parent
eb4fac99ba
commit
ec1b1aca51
@ -0,0 +1,103 @@
|
|||||||
|
#[cfg(feature = "async-mio")]
|
||||||
|
use mio::{unix::SourceFd, Events, Interest, Poll, Token};
|
||||||
|
#[cfg(feature = "async-mio")]
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::{self, Write},
|
||||||
|
os::unix::io::AsRawFd,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "async-mio")]
|
||||||
|
use ssip_client::{fifo, AsyncClient, ClientError, ClientName, ClientResult, Request, Response};
|
||||||
|
|
||||||
|
#[cfg(feature = "async-mio")]
|
||||||
|
fn main() -> ClientResult<()> {
|
||||||
|
let stdin = io::stdin();
|
||||||
|
|
||||||
|
// Poll instance
|
||||||
|
let mut poll = Poll::new()?;
|
||||||
|
|
||||||
|
// Register stdin
|
||||||
|
let stdin_fd = stdin.as_raw_fd();
|
||||||
|
let mut source_fd = SourceFd(&stdin_fd);
|
||||||
|
let stdin_token = Token(0);
|
||||||
|
poll.registry()
|
||||||
|
.register(&mut source_fd, stdin_token, Interest::READABLE)?;
|
||||||
|
|
||||||
|
// Register the SSIP client
|
||||||
|
let mut ssip_client = AsyncClient::new(fifo::Builder::new().build()?);
|
||||||
|
let speech_input_token = Token(1);
|
||||||
|
let speech_output_token = Token(2);
|
||||||
|
ssip_client.register(&poll, speech_input_token, speech_output_token)?;
|
||||||
|
|
||||||
|
// Loop for events
|
||||||
|
let mut events = Events::with_capacity(16);
|
||||||
|
let mut speech_writable = false;
|
||||||
|
let mut send_requests = VecDeque::with_capacity(4);
|
||||||
|
ssip_client.push(Request::SetName(ClientName::new("joe", "async")));
|
||||||
|
|
||||||
|
fn prompt() -> io::Result<()> {
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
write!(stdout, "> ")?;
|
||||||
|
stdout.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Enter an empty line to quit.");
|
||||||
|
prompt()?;
|
||||||
|
loop {
|
||||||
|
if !speech_writable || !ssip_client.has_next() {
|
||||||
|
poll.poll(&mut events, None)?;
|
||||||
|
}
|
||||||
|
for event in &events {
|
||||||
|
let token = event.token();
|
||||||
|
if token == stdin_token {
|
||||||
|
let mut text = String::new();
|
||||||
|
stdin.read_line(&mut text)?;
|
||||||
|
text = text.trim_end().to_string();
|
||||||
|
match text.len() {
|
||||||
|
0 => return Ok(()),
|
||||||
|
1 => {
|
||||||
|
if let Some(ch) = text.chars().next() {
|
||||||
|
println!("sending char: {}", ch);
|
||||||
|
ssip_client.push(Request::SendChar(ch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("sending line: {}", text);
|
||||||
|
send_requests.push_back(Request::SendLine(text.to_owned()));
|
||||||
|
ssip_client.push(Request::Speak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prompt()?;
|
||||||
|
} else if token == speech_input_token {
|
||||||
|
match ssip_client.receive_next() {
|
||||||
|
Err(ClientError::Io(err)) => return Err(ClientError::from(err)),
|
||||||
|
Err(ClientError::Ssip(err)) => eprintln!("SSIP error: {:?}", err),
|
||||||
|
Err(_) => panic!("internal error"),
|
||||||
|
Ok(result) => match result {
|
||||||
|
Response::MessageQueued | Response::ClientNameSet => (),
|
||||||
|
Response::ReceivingData => {
|
||||||
|
ssip_client.push(send_requests.pop_front().unwrap())
|
||||||
|
}
|
||||||
|
_ => panic!("Unexpected response: {:?}", result),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if token == speech_output_token {
|
||||||
|
speech_writable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if speech_writable {
|
||||||
|
match ssip_client.send_next() {
|
||||||
|
Err(ClientError::NotReady) => speech_writable = false,
|
||||||
|
Err(ClientError::Io(err)) => return Err(ClientError::from(err)),
|
||||||
|
Err(_) => panic!("internal error"),
|
||||||
|
Ok(()) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "async-mio"))]
|
||||||
|
fn main() {
|
||||||
|
println!("see hello for an example of synchronous client.");
|
||||||
|
}
|
@ -0,0 +1,253 @@
|
|||||||
|
// ssip-client -- Speech Dispatcher client in Rust
|
||||||
|
// Copyright (c) 2022 Laurent Pelecq
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
|
||||||
|
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||||
|
// option. All files in the project carrying such notice may not be copied,
|
||||||
|
// modified, or distributed except according to those terms.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
client::{Client, ClientError, ClientName, ClientResult},
|
||||||
|
constants::*,
|
||||||
|
types::{EventId, Source, SynthesisVoice},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// Request for SSIP server.
|
||||||
|
pub enum Request {
|
||||||
|
SetName(ClientName),
|
||||||
|
Speak,
|
||||||
|
SendLine(String),
|
||||||
|
SendLines(Vec<String>),
|
||||||
|
SendChar(char),
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// Response from SSIP server.
|
||||||
|
pub enum Response {
|
||||||
|
LanguageSet, // 201
|
||||||
|
PrioritySet, // 202
|
||||||
|
RateSet, // 203
|
||||||
|
PitchSet, // 204
|
||||||
|
PunctuationSet, // 205
|
||||||
|
CapLetRecognSet, // 206
|
||||||
|
SpellingSet, // 207
|
||||||
|
ClientNameSet, // 208
|
||||||
|
VoiceSet, // 209
|
||||||
|
Stopped, // 210
|
||||||
|
Paused, // 211
|
||||||
|
Resumed, // 212
|
||||||
|
Canceled, // 213
|
||||||
|
TableSet, // 215
|
||||||
|
OutputModuleSet, // 216
|
||||||
|
PauseContextSet, // 217
|
||||||
|
VolumeSet, // 218
|
||||||
|
SsmlModeSet, // 219
|
||||||
|
NotificationSet, // 220
|
||||||
|
PitchRangeSet, // 263
|
||||||
|
DebugSet, // 262
|
||||||
|
HistoryCurSetFirst, // 220
|
||||||
|
HistoryCurSetLast, // 221
|
||||||
|
HistoryCurSetPos, // 222
|
||||||
|
HistoryCurMoveFor, // 223
|
||||||
|
HistoryCurMoveBack, // 224
|
||||||
|
MessageQueued, // 225,
|
||||||
|
SoundIconQueued, // 226
|
||||||
|
MessageCanceled, // 227
|
||||||
|
ReceivingData, // 230
|
||||||
|
Bye, // 231
|
||||||
|
HistoryClientListSent(Vec<String>), // 240
|
||||||
|
HistoryMsgsListSent(Vec<String>), // 241
|
||||||
|
HistoryLastMsg(String), // 242
|
||||||
|
HistoryCurPosRet(String), // 243
|
||||||
|
TableListSent(Vec<String>), // 244
|
||||||
|
HistoryClientIdSent(String), // 245
|
||||||
|
MessageTextSent, // 246
|
||||||
|
HelpSent(Vec<String>), // 248
|
||||||
|
VoicesListSent(Vec<SynthesisVoice>), // 249
|
||||||
|
OutputModulesListSent(Vec<String>), // 250
|
||||||
|
GetString(String), // 251
|
||||||
|
GetInteger(u8), // 251
|
||||||
|
InsideBlock, // 260
|
||||||
|
OutsideBlock, // 261
|
||||||
|
NotImplemented, // 299
|
||||||
|
EventIndexMark(EventId, String), // 700
|
||||||
|
EventBegin(EventId), // 701
|
||||||
|
EventEnd(EventId), // 702
|
||||||
|
EventCanceled(EventId), // 703
|
||||||
|
EventPaused(EventId), // 704
|
||||||
|
EventResumed(EventId), // 705
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_REQUEST_QUEUE_CAPACITY: usize = 4;
|
||||||
|
|
||||||
|
/// Asynchronous client based on `mio`.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
pub struct AsyncClient<S: Read + Write + Source> {
|
||||||
|
client: Client<S>,
|
||||||
|
requests: VecDeque<Request>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Read + Write + Source> AsyncClient<S> {
|
||||||
|
/// New asynchronous client build on top of a synchronous client.
|
||||||
|
pub fn new(client: Client<S>) -> Self {
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
requests: VecDeque::with_capacity(INITIAL_REQUEST_QUEUE_CAPACITY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert two lines of the response in an event id
|
||||||
|
fn parse_event_id(lines: &[String]) -> ClientResult<EventId> {
|
||||||
|
match lines.len() {
|
||||||
|
0 | 1 => Err(ClientError::TooFewLines),
|
||||||
|
2 => Ok(EventId::new(&lines[0], &lines[1])),
|
||||||
|
_ => Err(ClientError::TooManyLines),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register client
|
||||||
|
pub fn register(
|
||||||
|
&mut self,
|
||||||
|
poll: &mio::Poll,
|
||||||
|
input_token: mio::Token,
|
||||||
|
output_token: mio::Token,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
self.client.register(poll, input_token, output_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a new request in the queue.
|
||||||
|
pub fn push(&mut self, request: Request) {
|
||||||
|
self.requests.push_back(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true if there is a pending request.
|
||||||
|
pub fn has_next(&self) -> bool {
|
||||||
|
!self.requests.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write one pending request if any.
|
||||||
|
///
|
||||||
|
/// Instance of `mio::Poll` generates a writable event only once until the socket returns `WouldBlock`.
|
||||||
|
/// This error is mapped to `ClientError::NotReady`.
|
||||||
|
pub fn send_next(&mut self) -> ClientResult<()> {
|
||||||
|
match self.requests.pop_front() {
|
||||||
|
Some(request) => match request {
|
||||||
|
Request::SetName(client_name) => self.client.set_client_name(client_name),
|
||||||
|
Request::Speak => self.client.speak(),
|
||||||
|
Request::SendLine(line) => self.client.send_line(&line),
|
||||||
|
Request::SendLines(lines) => self.client.send_lines(
|
||||||
|
lines
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.as_slice(),
|
||||||
|
),
|
||||||
|
Request::SendChar(ch) => self.client.send_char(ch),
|
||||||
|
Request::Quit => self.client.quit(),
|
||||||
|
}
|
||||||
|
.map(|_| ()),
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive one response.
|
||||||
|
///
|
||||||
|
/// Must be called each time a readable event is returned by `mio::Poll`.
|
||||||
|
pub fn receive_next(&mut self) -> ClientResult<Response> {
|
||||||
|
const MSG_CURSOR_SET_FIRST: &str = "OK CURSOR SET FIRST";
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let status = self.client.receive(&mut lines)?;
|
||||||
|
match status.code {
|
||||||
|
OK_LANGUAGE_SET => Ok(Response::LanguageSet),
|
||||||
|
OK_PRIORITY_SET => Ok(Response::PrioritySet),
|
||||||
|
OK_RATE_SET => Ok(Response::RateSet),
|
||||||
|
OK_PITCH_SET => Ok(Response::PitchSet),
|
||||||
|
OK_PUNCTUATION_SET => Ok(Response::PunctuationSet),
|
||||||
|
OK_CAP_LET_RECOGN_SET => Ok(Response::CapLetRecognSet),
|
||||||
|
OK_SPELLING_SET => Ok(Response::SpellingSet),
|
||||||
|
OK_CLIENT_NAME_SET => Ok(Response::ClientNameSet),
|
||||||
|
OK_VOICE_SET => Ok(Response::VoiceSet),
|
||||||
|
OK_STOPPED => Ok(Response::Stopped),
|
||||||
|
OK_PAUSED => Ok(Response::Paused),
|
||||||
|
OK_RESUMED => Ok(Response::Resumed),
|
||||||
|
OK_CANCELED => Ok(Response::Canceled),
|
||||||
|
OK_TABLE_SET => Ok(Response::TableSet),
|
||||||
|
OK_OUTPUT_MODULE_SET => Ok(Response::OutputModuleSet),
|
||||||
|
OK_PAUSE_CONTEXT_SET => Ok(Response::PauseContextSet),
|
||||||
|
OK_VOLUME_SET => Ok(Response::VolumeSet),
|
||||||
|
OK_SSML_MODE_SET => Ok(Response::SsmlModeSet),
|
||||||
|
// Warning OK_CUR_SET_FIRST == OK_NOTIFICATION_SET == 220. Matching message to make the difference
|
||||||
|
OK_NOTIFICATION_SET => {
|
||||||
|
if status.message == MSG_CURSOR_SET_FIRST {
|
||||||
|
//OK_CUR_SET_FIRST => Ok(Response::HistoryCurSetFirst)
|
||||||
|
Ok(Response::HistoryCurSetFirst)
|
||||||
|
} else {
|
||||||
|
Ok(Response::NotificationSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OK_CUR_SET_LAST => Ok(Response::HistoryCurSetLast),
|
||||||
|
OK_CUR_SET_POS => Ok(Response::HistoryCurSetPos),
|
||||||
|
OK_PITCH_RANGE_SET => Ok(Response::PitchRangeSet),
|
||||||
|
OK_DEBUG_SET => Ok(Response::DebugSet),
|
||||||
|
OK_CUR_MOV_FOR => Ok(Response::HistoryCurMoveFor),
|
||||||
|
OK_CUR_MOV_BACK => Ok(Response::HistoryCurMoveBack),
|
||||||
|
OK_MESSAGE_QUEUED => Ok(Response::MessageQueued),
|
||||||
|
OK_SND_ICON_QUEUED => Ok(Response::SoundIconQueued),
|
||||||
|
OK_MSG_CANCELED => Ok(Response::MessageCanceled),
|
||||||
|
OK_RECEIVING_DATA => Ok(Response::ReceivingData),
|
||||||
|
OK_BYE => Ok(Response::Bye),
|
||||||
|
OK_CLIENT_LIST_SENT => Ok(Response::HistoryClientListSent(lines)),
|
||||||
|
OK_MSGS_LIST_SENT => Ok(Response::HistoryMsgsListSent(lines)),
|
||||||
|
OK_LAST_MSG => Ok(Response::HistoryLastMsg(Client::<S>::parse_single_value(
|
||||||
|
&lines,
|
||||||
|
)?)),
|
||||||
|
OK_CUR_POS_RET => Ok(Response::HistoryCurPosRet(Client::<S>::parse_single_value(
|
||||||
|
&lines,
|
||||||
|
)?)),
|
||||||
|
OK_TABLE_LIST_SENT => Ok(Response::TableListSent(lines)),
|
||||||
|
OK_CLIENT_ID_SENT => Ok(Response::HistoryClientIdSent(
|
||||||
|
Client::<S>::parse_single_value(&lines)?,
|
||||||
|
)),
|
||||||
|
OK_MSG_TEXT_SENT => Ok(Response::MessageTextSent),
|
||||||
|
OK_HELP_SENT => Ok(Response::HelpSent(lines)),
|
||||||
|
OK_VOICES_LIST_SENT => Ok(Response::VoicesListSent(
|
||||||
|
Client::<S>::parse_synthesis_voices(&lines)?,
|
||||||
|
)),
|
||||||
|
OK_OUTPUT_MODULES_LIST_SENT => Ok(Response::OutputModulesListSent(lines)),
|
||||||
|
OK_GET => {
|
||||||
|
let sval = Client::<S>::parse_single_value(&lines)?;
|
||||||
|
Ok(match sval.parse::<u8>() {
|
||||||
|
Ok(uval) => Response::GetInteger(uval),
|
||||||
|
Err(_) => Response::GetString(sval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
OK_INSIDE_BLOCK => Ok(Response::InsideBlock),
|
||||||
|
OK_OUTSIDE_BLOCK => Ok(Response::OutsideBlock),
|
||||||
|
OK_NOT_IMPLEMENTED => Ok(Response::NotImplemented),
|
||||||
|
EVENT_INDEX_MARK => {
|
||||||
|
if lines.len() == 3 {
|
||||||
|
Ok(Response::EventIndexMark(
|
||||||
|
Self::parse_event_id(&lines)?,
|
||||||
|
lines[2].to_owned(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(ClientError::TooFewLines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EVENT_BEGIN => Ok(Response::EventBegin(Self::parse_event_id(&lines)?)),
|
||||||
|
EVENT_END => Ok(Response::EventEnd(Self::parse_event_id(&lines)?)),
|
||||||
|
EVENT_CANCELED => Ok(Response::EventCanceled(Self::parse_event_id(&lines)?)),
|
||||||
|
EVENT_PAUSED => Ok(Response::EventPaused(Self::parse_event_id(&lines)?)),
|
||||||
|
EVENT_RESUMED => Ok(Response::EventResumed(Self::parse_event_id(&lines)?)),
|
||||||
|
_ => panic!("error should have been caught earlier"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue