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}