use thiserror::Error;
pub type CortexResult<T> = std::result::Result<T, CortexError>;
#[derive(Error, Debug)]
pub enum CortexError {
#[error("Failed to connect to Cortex at {url}: {reason}. Is the EMOTIV Launcher running?")]
ConnectionFailed { url: String, reason: String },
#[error("Connection to Cortex lost: {reason}")]
ConnectionLost { reason: String },
#[error("Not connected to Cortex")]
NotConnected,
#[error(
"Authentication failed: {reason}. \
Check your client_id and client_secret from the Emotiv Developer Portal."
)]
AuthenticationFailed { reason: String },
#[error("Cortex token expired — re-authentication required")]
TokenExpired,
#[error("Access denied: {reason}. Approve the application in the EMOTIV Launcher.")]
AccessDenied { reason: String },
#[error("User not logged in to EmotivID. Open the EMOTIV Launcher and sign in.")]
UserNotLoggedIn,
#[error(
"Application not approved. \
Open the EMOTIV Launcher and approve access for your app."
)]
NotApproved,
#[error("Emotiv license error: {reason}")]
LicenseError { reason: String },
#[error("No headset found. Ensure the headset is powered on and within range.")]
NoHeadsetFound,
#[error("Headset is in use by another session")]
HeadsetInUse,
#[error("Headset connection error: {reason}")]
HeadsetError { reason: String },
#[error("Session error: {reason}")]
SessionError { reason: String },
#[error("Stream error: {reason}")]
StreamError { reason: String },
#[error("Cortex API error {code}: {message}")]
ApiError { code: i32, message: String },
#[error("Cortex service is starting up — retry in a few seconds")]
CortexStarting,
#[error("API method not found: {method}")]
MethodNotFound { method: String },
#[error("Operation timed out after {seconds}s")]
Timeout { seconds: u64 },
#[error("Operation failed after {attempts} attempts: {last_error}")]
RetriesExhausted {
attempts: u32,
last_error: Box<CortexError>,
},
#[error("Protocol error: {reason}")]
ProtocolError { reason: String },
#[error("Configuration error: {reason}")]
ConfigError { reason: String },
#[error("WebSocket error: {0}")]
WebSocket(String),
#[error("TLS error: {0}")]
Tls(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
}
impl CortexError {
pub fn from_api_error(code: i32, message: impl Into<String>) -> Self {
let message = message.into();
match code {
-32601 => CortexError::MethodNotFound {
method: message.clone(),
},
-32001 | -32004 => CortexError::NoHeadsetFound,
-32002 | -32024 => CortexError::LicenseError { reason: message },
-32005 | -32012 => CortexError::SessionError { reason: message },
-32014 | -32021 => CortexError::AuthenticationFailed { reason: message },
-32015 => CortexError::TokenExpired,
-32016 => CortexError::StreamError { reason: message },
-32033 => CortexError::UserNotLoggedIn,
-32142 => CortexError::NotApproved,
-32152 => CortexError::HeadsetError { reason: message },
-32102 => CortexError::NotApproved,
-32122 => CortexError::CortexStarting,
_ => CortexError::ApiError { code, message },
}
}
#[must_use]
pub fn is_retryable(&self) -> bool {
matches!(
self,
CortexError::ConnectionLost { .. }
| CortexError::Timeout { .. }
| CortexError::CortexStarting
| CortexError::WebSocket(_)
)
}
#[must_use]
pub fn is_connection_error(&self) -> bool {
matches!(
self,
CortexError::ConnectionFailed { .. }
| CortexError::ConnectionLost { .. }
| CortexError::NotConnected
| CortexError::WebSocket(_)
)
}
}
impl From<tokio_tungstenite::tungstenite::Error> for CortexError {
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
CortexError::WebSocket(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_api_error_known_codes() {
assert!(matches!(
CortexError::from_api_error(-32001, "no headset"),
CortexError::NoHeadsetFound
));
assert!(matches!(
CortexError::from_api_error(-32002, "invalid license"),
CortexError::LicenseError { .. }
));
assert!(matches!(
CortexError::from_api_error(-32004, "headset unavailable"),
CortexError::NoHeadsetFound
));
assert!(matches!(
CortexError::from_api_error(-32005, "session already exists"),
CortexError::SessionError { .. }
));
assert!(matches!(
CortexError::from_api_error(-32014, "invalid token"),
CortexError::AuthenticationFailed { .. }
));
assert!(matches!(
CortexError::from_api_error(-32015, "expired token"),
CortexError::TokenExpired
));
assert!(matches!(
CortexError::from_api_error(-32016, "invalid stream"),
CortexError::StreamError { .. }
));
assert!(matches!(
CortexError::from_api_error(-32033, "not logged in"),
CortexError::UserNotLoggedIn
));
assert!(matches!(
CortexError::from_api_error(-32142, "not approved"),
CortexError::NotApproved
));
}
#[test]
fn test_from_api_error_unknown_code() {
let err = CortexError::from_api_error(-99999, "something else");
assert!(matches!(err, CortexError::ApiError { code: -99999, .. }));
}
#[test]
fn test_is_retryable() {
assert!(CortexError::Timeout { seconds: 10 }.is_retryable());
assert!(CortexError::CortexStarting.is_retryable());
assert!(CortexError::ConnectionLost { reason: "test".into() }.is_retryable());
assert!(!CortexError::NoHeadsetFound.is_retryable());
assert!(!CortexError::TokenExpired.is_retryable());
assert!(!CortexError::NotApproved.is_retryable());
}
#[test]
fn test_is_connection_error() {
assert!(CortexError::NotConnected.is_connection_error());
assert!(CortexError::ConnectionLost { reason: "test".into() }.is_connection_error());
assert!(!CortexError::TokenExpired.is_connection_error());
assert!(!CortexError::NoHeadsetFound.is_connection_error());
}
#[test]
fn test_error_display_messages() {
let err = CortexError::from_api_error(-32015, "token expired");
assert!(err.to_string().contains("re-authentication"));
let err = CortexError::from_api_error(-32033, "not logged in");
assert!(err.to_string().contains("EmotivID"));
let err = CortexError::from_api_error(-32142, "not approved");
assert!(err.to_string().contains("EMOTIV Launcher"));
}
}