pjlink/
lib.rs

1// Copyright 2018 Rick Russell
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::io::prelude::*;
16use std::io::{Error, ErrorKind};
17use std::net::TcpStream;
18
19extern crate md5;
20
21const AUTH: char = '1';
22const NOAUTH: char = '0';
23static PORT: &'static str = "4352";
24
25// Return the correct error message based on the PJ Link specification
26fn pjlink_error(error_msg: &str) -> Error {
27    match &error_msg[0..4] {
28        "ERR1" => Error::new(ErrorKind::InvalidData, "Undefined command".to_string()),
29        "ERR2" => Error::new(ErrorKind::InvalidData, "Invalid parameter".to_string()),
30        "ERR3" => Error::new(
31            ErrorKind::InvalidData,
32            "Unavaiable at this time".to_string(),
33        ),
34        "ERR4" => Error::new(
35            ErrorKind::InvalidData,
36            "Projector/Display Failure".to_string(),
37        ),
38        "ERRA" => Error::new(
39            ErrorKind::PermissionDenied,
40            "Authorization Error".to_string(),
41        ),
42        _ => Error::new(
43            ErrorKind::InvalidData,
44            format!("Error reported from the projector {}", error_msg),
45        ),
46    }
47}
48
49// Parse the response from the device
50fn parse_response(response: &str) -> Result<PjlinkResponse, Error> {
51    let mut equals_sign: usize = 0;
52    let len = response.len();
53    //lets find the equals sign
54    for (i, c) in response.chars().enumerate() {
55        if c == '=' || c == ' ' {
56            equals_sign = i;
57            break;
58        }
59    }
60
61    let command = if &response[0..1] != "%" {
62        CommandType::PJLINK
63    } else {
64        match &response[2..equals_sign] {
65            "POWR" => CommandType::Power,
66            "INPT" => CommandType::Input,
67            "AVMT" => CommandType::AvMute,
68            "ERST" => CommandType::ErrorStatus,
69            "LAMP" => CommandType::Lamp,
70            "INST" => CommandType::InputList,
71            "NAME" => CommandType::Name,
72            "INF1" => CommandType::Manufacturer,
73            "INF2" => CommandType::ProductName,
74            "INFO" => CommandType::Information,
75            "CLSS" => CommandType::Class,
76            _ => {
77                return Err(Error::new(
78                    ErrorKind::InvalidInput,
79                    "Invalid command type returned.",
80                ));
81            }
82        }
83    };
84
85    let value = &response[equals_sign + 1..len];
86
87    // Did we get and error report and if so lets return it so the functions don't have check for errors.
88    if value.len() == 4 && &value[0..3] == "ERR" {
89        return Err(pjlink_error(value));
90    }
91
92    Ok(PjlinkResponse {
93        action: command,
94        value: value.to_string(),
95    })
96}
97
98// This is the list of standard command/response types from the PJLink spec.
99// At this point I would think that this would only be used internally.
100enum CommandType {
101    PJLINK,
102    Power,
103    Input,
104    AvMute,
105    ErrorStatus,
106    Lamp,
107    InputList,
108    Name,
109    Manufacturer,
110    ProductName,
111    Information,
112    Class,
113}
114
115/// Power status is based off of the PJLink specification and is used to be returned
116pub enum PowerStatus {
117    Off,
118    On,
119    Cooling,
120    Warmup,
121}
122
123pub enum InputType {
124    RGB(u8),
125    Video(u8),
126    Digital(u8),
127    Storage(u8),
128    Network(u8),
129}
130
131pub enum ErrorType {
132    NoError,
133    Warning,
134    Error,
135}
136
137pub struct AvMute {
138    pub audio: bool,
139    pub video: bool,
140}
141
142pub struct Lamp {
143    pub hours: u16,
144    pub on: bool,
145}
146
147pub struct ErrorStatus {
148    pub fan_error: ErrorType,
149    pub lamp_error: ErrorType,
150    pub temperature_error: ErrorType,
151    pub cover_open_error: ErrorType,
152    pub filter_error: ErrorType,
153    pub other_error: ErrorType,
154}
155
156struct PjlinkResponse {
157    action: CommandType,
158    value: String,
159}
160
161pub struct PjlinkDevice {
162    pub host: String,
163    password: String,
164    //managed: bool, // Currently not implemented but will add managed monitoring support with call backs with the status changes
165    //monitored: bool, // Currenly not implemented by will allow you to monitor a device with out mainting authority over it.
166}
167
168impl PjlinkDevice {
169    /// Constructs a new PjlinkDevice.
170    pub fn new(host: &str) -> Result<PjlinkDevice, Error> {
171        let pwd = String::from("");
172        PjlinkDevice::new_with_password(host, &pwd)
173    }
174
175    /// Contructs a new PjlinkDevice that has a password
176    pub fn new_with_password(host: &str, password: &str) -> Result<PjlinkDevice, Error> {
177        Ok(PjlinkDevice {
178            host: host.to_string(),
179            password: String::from(password),
180            //managed: false, // Hard coded for now until it is implemented
181            //monitored: false, // Hard coded for now until it is implemented
182        })
183    }
184
185    /// Send a command and a Result with the raw string or an error
186    pub fn send_command(&self, command: &str) -> Result<String, Error> {
187        let host_port = [&self.host, ":", PORT].concat();
188        let mut client_buffer = [0u8; 256];
189        let mut stream = try!(TcpStream::connect(host_port));
190
191        let _ = stream.read(&mut client_buffer); //Did we get the hello string?
192
193        let cmd: String = match client_buffer[7] as char {
194            // Does the connection require auth or not
195            AUTH => {
196                // Connection requires auth
197                let rnd_num = String::from_utf8_lossy(&client_buffer[9..17]).to_string();
198                if &self.password != "" {
199                    // We got a password
200                    let pwd_str = format!("{}{}", rnd_num, &self.password);
201                    let digest = md5::compute(pwd_str);
202                    format!("{:x}%1{}\r", digest, command)
203                } else {
204                    // No password was supplied so we are going to raise an error.
205                    return Err(Error::new(
206                        ErrorKind::InvalidInput,
207                        "This device requires a password and one was not supplied.",
208                    ));
209                }
210            }
211            NOAUTH => {
212                // Connection requires no auth
213                format!("%1{}\r", command)
214            }
215
216            _ => {
217                return Err(Error::new(
218                    ErrorKind::InvalidInput,
219                    "Invalid response or is not a PJLink device",
220                ));
221            }
222        };
223
224        let result = stream.write(cmd.as_bytes());
225        match result {
226            Ok(_) => (),
227            Err(e) => return Err(e),
228        };
229        let result = stream.read(&mut client_buffer);
230        let len = match result {
231            Ok(len) => len,
232            Err(e) => return Err(e),
233        };
234
235        let response = String::from_utf8_lossy(&client_buffer[0..len - 1]).to_string();
236        Ok(response)
237    }
238
239    // a wrapper around send_command that will parse the response
240    fn send(&self, cmd: &str) -> Result<PjlinkResponse, Error> {
241        match self.send_command(cmd) {
242            Ok(send_result) => match parse_response(&send_result) {
243                Ok(parse_result) => Ok(parse_result),
244                Err(e) => Err(e),
245            },
246            Err(e) => Err(e),
247        }
248    }
249
250    /// Check the power status of the device and returns an enum
251    pub fn get_power_status(&self) -> Result<PowerStatus, Error> {
252        match self.send("POWR ?") {
253            Ok(result) => {
254                match result.action {
255                    CommandType::Power => {
256                        match &result.value[0..1] {
257                            "0" => Ok(PowerStatus::Off),
258                            "1" => Ok(PowerStatus::On),
259                            "2" => Ok(PowerStatus::Cooling),
260                            "3" => Ok(PowerStatus::Warmup),
261                            _ => Err(Error::new(
262                                ErrorKind::InvalidInput,
263                                format!("Invalid Response: {}", result.value),
264                            )), // Invalid Response
265                        }
266                    }
267                    _ => Err(Error::new(
268                        ErrorKind::InvalidInput,
269                        format!("Got a response we didn't expect: {}", result.value),
270                    )),
271                }
272            }
273            Err(e) => Err(e),
274        }
275    }
276
277    /// Turn on the device and will return a Result enum with
278    /// Ok being a [pjlink::PowerStatus](enum.PowerStatus.html) or Err being a std::io::Error
279    ///
280    pub fn power_on(&self) -> Result<PowerStatus, Error> {
281        match self.send("POWR 1") {
282            Ok(result) => {
283                match result.action {
284                    CommandType::Power => {
285                        match &result.value[0..2] {
286                            "OK" => match self.get_power_status() {
287                                Ok(status) => Ok(status),
288                                Err(e) => Err(e),
289                            },
290                            _ => Err(Error::new(
291                                ErrorKind::InvalidInput,
292                                format!("Invalid Response: {}", result.value),
293                            )), // Invalid Response
294                        }
295                    }
296                    _ => Err(Error::new(
297                        ErrorKind::InvalidInput,
298                        format!("Got a response we didn't expect: {}", result.value),
299                    )),
300                }
301            }
302            Err(e) => Err(e),
303        }
304    }
305
306    /// Turn off the device and will return a Result enum with
307    /// Ok being a [pjlink::PowerStatus](enum.PowerStatus.html) or Err being a std::io::Error
308    ///
309    pub fn power_off(&self) -> Result<PowerStatus, Error> {
310        match self.send("POWR 0") {
311            Ok(result) => {
312                match result.action {
313                    CommandType::Power => {
314                        match &result.value[0..2] {
315                            "OK" => match self.get_power_status() {
316                                Ok(status) => Ok(status),
317                                Err(e) => Err(e),
318                            },
319                            _ => Err(Error::new(
320                                ErrorKind::InvalidInput,
321                                format!("Invalid Response: {}", result.value),
322                            )), // Invalid Response
323                        }
324                    }
325                    _ => Err(Error::new(
326                        ErrorKind::InvalidInput,
327                        format!("Got a response we didn't expect: {}", result.value),
328                    )),
329                }
330            }
331            Err(e) => Err(e),
332        }
333    }
334
335    /// Get the information (INFO ?) from theand returns a
336    /// string with the information or a std::io::Error
337    ///
338    pub fn get_info(&self) -> Result<String, Error> {
339        match self.send("INFO ?") {
340            Ok(result) => match result.action {
341                CommandType::Information => Ok(result.value),
342                _ => Err(Error::new(
343                    ErrorKind::InvalidInput,
344                    format!("Invalid Response:: {}", result.value),
345                )),
346            },
347            Err(e) => Err(e),
348        }
349    }
350
351    /// Get the manufacturer (INF1 ?) from the deviceand returns a
352    /// string with the information or a std::io::Error
353    ///
354    pub fn get_manufacturer(&self) -> Result<String, Error> {
355        match self.send("INF1 ?") {
356            Ok(result) => match result.action {
357                CommandType::Manufacturer => Ok(result.value),
358                _ => Err(Error::new(
359                    ErrorKind::InvalidInput,
360                    format!("Invalid Response:: {}", result.value),
361                )),
362            },
363            Err(e) => Err(e),
364        }
365    }
366
367    /// Get the product name (INF2 ?) from the deviceand returns a
368    /// string with the information or a std::io::Error
369    ///
370    pub fn get_product_name(&self) -> Result<String, Error> {
371        match self.send("INF2 ?") {
372            Ok(result) => match result.action {
373                CommandType::ProductName => Ok(result.value),
374                _ => Err(Error::new(
375                    ErrorKind::InvalidInput,
376                    format!("Invalid Response:: {}", result.value),
377                )),
378            },
379            Err(e) => Err(e),
380        }
381    }
382    /// Get the product class (CLSS ?) from the deviceand returns a
383    /// string with the information or a std::io::Error
384    ///
385    pub fn get_class(&self) -> Result<String, Error> {
386        match self.send("CLSS ?") {
387            Ok(result) => match result.action {
388                CommandType::Class => Ok(result.value),
389                _ => Err(Error::new(
390                    ErrorKind::InvalidInput,
391                    format!("Invalid Response:: {}", result.value),
392                )),
393            },
394            Err(e) => Err(e),
395        }
396    }
397
398    /// Get the device name (NAME ?) from the device and returns a
399    /// string with the information or a std::io::Error
400    ///
401    pub fn get_device_name(&self) -> Result<String, Error> {
402        match self.send("NAME ?") {
403            Ok(result) => match result.action {
404                CommandType::Name => Ok(result.value),
405                _ => Err(Error::new(
406                    ErrorKind::InvalidInput,
407                    format!("Invalid Response:: {}", result.value),
408                )),
409            },
410            Err(e) => Err(e),
411        }
412    }
413
414    /// Get the current input (INPT ?) from the device
415    /// Returns a Result enum with an Ok type of [pjlink::InputType](enum.InputType.html) example would be:
416    /// ```
417    /// pjlink::InputType::RGB(input_num) //with input_num being the number of the input with a type of u8
418    ///
419    /// ```
420    ///
421    pub fn get_input(&self) -> Result<InputType, Error> {
422        match self.send("INPT ?") {
423            Ok(result) => {
424                let input = result.value.parse::<u8>().unwrap();
425                match input {
426                    11...19 => Ok(InputType::RGB(input - 10)),
427                    21...29 => Ok(InputType::Video(input - 20)),
428                    31...39 => Ok(InputType::Digital(input - 30)),
429                    41...49 => Ok(InputType::Storage(input - 40)),
430                    51...59 => Ok(InputType::Network(input - 50)),
431                    _ => Err(Error::new(
432                        ErrorKind::InvalidInput,
433                        format!("Invalid input:: {}", input),
434                    )),
435                }
436            }
437            Err(e) => Err(e),
438        }
439    }
440
441    /// Change the current input (INPT 31 for) on the device
442    /// Returns a result enum with Ok type of [pjlink::InputType](enum.InputType.html) with a value associated
443    ///  of the input number or an std::io::Error
444    ///
445    /// ```
446    /// let result = pjlink::PjlinkDevice::set_input(&self, input: InputType).?
447    /// match device.get_input() {
448    ///    Ok(input) => {
449    ///        match input {
450    ///            InputType::RGB(input_number) => println!("Input: RGB {}", input_number),
451    ///            InputType::Video(input_number) => println!("Input: Video {}", input_number),
452    ///            InputType::Digital(input_number) => println!("Input: Digital {}", input_number),
453    ///            InputType::Storage(input_number) => println!("Input: Storage {}", input_number),
454    ///            InputType::Network(input_number) => println!("Input: Network {}", input_number),
455    ///        }
456    ///    },
457    ///    Err(err) => println!("An error occurred: {}", err),
458    /// }
459    /// ```
460    ///
461    pub fn set_input(&self, input: InputType) -> Result<InputType, Error> {
462        let input_number: u8 = match input {
463            InputType::RGB(i_num) => i_num + 10,
464            InputType::Video(i_num) => i_num + 20,
465            InputType::Digital(i_num) => i_num + 30,
466            InputType::Storage(i_num) => i_num + 40,
467            InputType::Network(i_num) => i_num + 50,
468        };
469
470        let command = format!("INPT {}", input_number);
471        match self.send(&command) {
472            Ok(result) => {
473                match result.action {
474                    CommandType::Input => {
475                        match &result.value[0..2] {
476                            "OK" => match self.get_input() {
477                                Ok(status) => Ok(status),
478                                Err(e) => Err(e),
479                            },
480                            _ => Err(Error::new(
481                                ErrorKind::InvalidInput,
482                                format!("Invalid Response: {}", result.value),
483                            )), // Invalid Response
484                        }
485                    }
486                    _ => Err(Error::new(
487                        ErrorKind::InvalidInput,
488                        format!("Got a response we didn't expect: {}", result.value),
489                    )),
490                }
491            }
492            Err(e) => Err(e),
493        }
494    }
495
496    /// Get the current Av Mute (AVMT ?) from the device
497    /// Returns a Result enum with an Ok type of [pjlink::AvMute](struct.AvMute.html) example would be:
498    /// ```
499    /// pjlink::AvMute::Audio or Video //with Audio and Video being a bool with the status.
500    ///
501    /// ```
502    ///
503    pub fn get_avmute(&self) -> Result<AvMute, Error> {
504        match self.send("AVMT ?") {
505            Ok(result) => {
506                let status = result.value.parse::<u8>().unwrap();
507                match status {
508                    11 => Ok(AvMute {
509                        audio: false,
510                        video: true,
511                    }),
512                    21 => Ok(AvMute {
513                        audio: true,
514                        video: false,
515                    }),
516                    31 => Ok(AvMute {
517                        audio: true,
518                        video: true,
519                    }),
520                    30 => Ok(AvMute {
521                        audio: false,
522                        video: false,
523                    }),
524                    _ => Err(Error::new(
525                        ErrorKind::InvalidInput,
526                        format!("Invalid result:: {}", status),
527                    )),
528                }
529            }
530            Err(e) => Err(e),
531        }
532    }
533
534    /// Set the AV Mute (AVMT 30) on the current device
535    /// Returns a Result enum with an Ok type of [pjlink::AvMute](struct.AvMute.html) example would be:
536    /// ```
537    /// let mutes = AvMute {
538    ///     video: true,
539    ///     audio: true,
540    /// }
541    ///
542    /// match device.set_avmute(mutes) {
543    ///     Ok(mutes) => println!(
544    ///         "{} Video Mute: {} Audio Mute: {}",
545    ///         host, mutes.video, mutes.audio
546    ///     ),
547    ///     Err(err) => println!("An error occurred: {}", err),
548    /// }
549    ///
550    /// ```
551    ///
552    pub fn set_avmute(&self, mute_status: AvMute) -> Result<AvMute, Error> {
553        let mutes: u8 = match mute_status {
554            AvMute {
555                video: true,
556                audio: false,
557            } => 11,
558            AvMute {
559                video: false,
560                audio: true,
561            } => 21,
562            AvMute {
563                video: true,
564                audio: true,
565            } => 31,
566            _ => 30,
567        };
568
569        let command = format!("AVMT {}", mutes);
570        match self.send(&command) {
571            Ok(result) => {
572                match result.action {
573                    CommandType::AvMute => {
574                        match &result.value[0..2] {
575                            "OK" => match self.get_avmute() {
576                                Ok(status) => Ok(status),
577                                Err(e) => Err(e),
578                            },
579                            _ => Err(Error::new(
580                                ErrorKind::InvalidInput,
581                                format!("Invalid Response: {}", result.value),
582                            )), // Invalid Response
583                        }
584                    }
585                    _ => Err(Error::new(
586                        ErrorKind::InvalidInput,
587                        format!("Got a response we didn't expect: {}", result.value),
588                    )),
589                }
590            }
591            Err(e) => Err(e),
592        }
593    }
594
595    /// Get the current lamp status (LAMP ?) from the device
596    /// Returns a Result enum with an Ok vector of [pjlink::Lamp](struct.Lamp.html) example would be:
597    /// ```
598    /// pjlink::Lamp::hours and on  //with hours being the total hours on that lamp
599    /// and "on" being a bool with the status of the lamp.
600    ///
601    /// ```
602    ///
603    pub fn get_lamp(&self) -> Result<Vec<Lamp>, Error> {
604        match self.send("LAMP ?") {
605            Ok(result) => {
606                let mut status = result.value.split_whitespace();
607                let mut lamps = Vec::new();
608                while let Some(l) = status.next() {
609                    let hours = l.parse::<u16>().unwrap();
610
611                    let on = match status.next() {
612                        Some(x) => x == "1",
613                        None => false,
614                    };
615                    lamps.push(Lamp {
616                        hours: hours,
617                        on: on,
618                    });
619                }
620                Ok(lamps)
621            }
622            Err(e) => Err(e),
623        }
624    }
625
626    /// Get the current error status of the device (ERST ?)
627    /// Returns a Result enum with an Ok being a [pjlink::ErrorStatus](struct.Lamp.html) example would be:
628    /// ```
629    /// match device.get_error_status() {
630    ///    Ok(error_status) => {
631    ///        match error_status.fan_error {
632    ///            ErrorType::Warning => println!("{} Error Status: Fan Warning", host),
633    ///            ErrorType::Error => println!("{} Error Status: Fan Error", host),
634    ///            _ => (),
635    ///        }
636    ///        match error_status.lamp_error {
637    ///            ErrorType::Warning => println!("{} Error Status: Lamp Warning", host),
638    ///            ErrorType::Error => println!("{} Error Status: Lamp Error", host),
639    ///            _ => (),
640    ///        }
641    ///        match error_status.temperature_error {
642    ///            ErrorType::Warning => println!("{} Error Status: Temperature Warning", host),
643    ///            ErrorType::Error => println!("{} Error Status: Temperature Error", host),
644    ///            _ => (),
645    ///        }
646    ///        match error_status.cover_open_error {
647    ///            ErrorType::Warning => println!("{} Error Status: Cover Open Warning", host),
648    ///            ErrorType::Error => println!("{} Error Status: Cover Open Error", host),
649    ///            _ => (),
650    ///        }
651    ///        match error_status.filter_error {
652    ///            ErrorType::Warning => println!("{} Error Status: Filter Warning", host),
653    ///            ErrorType::Error => println!("{} Error Status: Filter Error", host),
654    ///            _ => (),
655    ///        }
656    ///        match error_status.other_error {
657    ///            ErrorType::Warning => println!("{} Error Status: Other Warning", host),
658    ///            ErrorType::Error => println!("{} Error Status: Other Error", host),
659    ///            _ => (),
660    ///        }
661    ///    }
662    ///    Err(err) => println!("{} Error Status: error occurred: {}", host, err),
663    /// }
664    ///
665    /// ```
666    ///
667    pub fn get_error_status(&self) -> Result<ErrorStatus, Error> {
668        match self.send("ERST ?") {
669            Ok(result) => {
670                let mut status = result.value.chars();
671
672                Ok(ErrorStatus {
673                    fan_error: match status.next() {
674                        Some(e) => match e {
675                            '0' => ErrorType::NoError,
676                            '1' => ErrorType::Warning,
677                            '2' => ErrorType::Error,
678                            _ => ErrorType::NoError,
679                        },
680                        None => ErrorType::NoError,
681                    },
682                    lamp_error: match status.next() {
683                        Some(e) => match e {
684                            '0' => ErrorType::NoError,
685                            '1' => ErrorType::Warning,
686                            '2' => ErrorType::Error,
687                            _ => ErrorType::NoError,
688                        },
689                        None => ErrorType::NoError,
690                    },
691                    temperature_error: match status.next() {
692                        Some(e) => match e {
693                            '0' => ErrorType::NoError,
694                            '1' => ErrorType::Warning,
695                            '2' => ErrorType::Error,
696                            _ => ErrorType::NoError,
697                        },
698                        None => ErrorType::NoError,
699                    },
700                    cover_open_error: match status.next() {
701                        Some(e) => match e {
702                            '0' => ErrorType::NoError,
703                            '1' => ErrorType::Warning,
704                            '2' => ErrorType::Error,
705                            _ => ErrorType::NoError,
706                        },
707                        None => ErrorType::NoError,
708                    },
709                    filter_error: match status.next() {
710                        Some(e) => match e {
711                            '0' => ErrorType::NoError,
712                            '1' => ErrorType::Warning,
713                            '2' => ErrorType::Error,
714                            _ => ErrorType::NoError,
715                        },
716                        None => ErrorType::NoError,
717                    },
718                    other_error: match status.next() {
719                        Some(e) => match e {
720                            '0' => ErrorType::NoError,
721                            '1' => ErrorType::Warning,
722                            '2' => ErrorType::Error,
723                            _ => ErrorType::NoError,
724                        },
725                        None => ErrorType::NoError,
726                    },
727                })
728            }
729            Err(e) => Err(e),
730        }
731    }
732}