PJLink is a protocol developed by JBMIA for controlling projectors (and presentation displays for example from NEC) via LAN connections
It use get and set commands. You can get or set several thinks, Power on/off or get the status. Input for video and audio source. Volume and if it's projector you can work also with lens and apertur. More documentation can be find here: http://pjlink.jbmia.or.jp/english/
Here we will concentrate in Authentication and Security.
There are two possibilities
there is no authentication and PJLink device start the communication with: PJLINK 0
there is authentication and PJLink device start communication with PJLINK 1 4bytes random number
The authentication process is as follows(schematics below):
PJLink device send PJLINK 1 4bytes random number
PJLink clinet will concatenate this 4bytes random number and password and do MD5 hash
PJLink client will send MD5 Hash and command to PJLink device
PJLink device do same MD5 Hash generation and compare the result with client response to verify password and either respond with command result or error message
To obtain access to PJLink device with authentication there two ways.
as per point 4 above PJLink device is doing MD5(random number + password) => which means it have a password plain text (or symetricaly encrypted) somewhere localy, hence if you have other access to device you might be able to get it.
we can simply do brute force attack and guess password (here are scripts to do word list based brute force attack) the problem is that every login using random salt, which means you need to find really correct password (collision is no help)
PJLink is a protocol developed by JBMIA for controlling projectors (and presentation displays for example from NEC) via LAN connections
The protocol works over TCP/IP, by default it use port 4352. Protocol supports authentification, however as I didn't set password for PJLink on my NEC, it works without authentification. (Not sure if that would be same on other venodrs as well).
It use get and set commands. You can get or set several thinks, Power on/off or get the status. Input for video and audio source. Volume and if it's projector you can work also with lens and apertur.
The structure is prety easy header followed by command and parametr:
Below examples and test was done on my NEC MultiSync V423, PJLink listen on port 4352 as standard suggest the other ports are 80 http for other settings and 7142 which is NEC prorietary remote control protocol, more can be found here.
root@raspberrypi:/home/pi# nmap -sT 10.0.0.40 -p 1-65535
Starting Nmap 6.00 ( http://nmap.org ) at 2017-03-24 21:00 CET
Nmap scan report for 10.0.0.40
Host is up (0.018s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE
80/tcp open http
4352/tcp open unknown
7142/tcp open unknown
MAC Address: 58:C2:32:97:3A:48 (Unknown)
Below are example use telnet to switch on the device, switch it off and get status:
Connection closed by foreign host.
root@raspberrypi:/home/pi# telnet 10.0.0.40 4352
Trying 10.0.0.40...
Connected to 10.0.0.40.
Escape character is '^]'.
PJLINK 0
%1POWR 1
%1POWR=OK
%1POWR 0
%1POWR=OK
%1POWR ?
%1POWR=0
^]
telnet> quit
Connection closed.
Here is Python example for Power On/Off
#! /usr/bin/python
#takes two arguments IP and Power ON|OFF
#example: ./PJLinkPower.py 10.0.0.40 OFF
import sys
import cgi
import socket
if len(sys.argv) == 1:
form = cgi.FieldStorage()
monitor_ip = form.getvalue('monitor_ip')
else:
monitor_ip = sys.argv[1]
power = sys.argv[2]
#define some constanst to make life easier
port = 4352
buffer_size = 1024
if (power == 'ON'):
command = '%1POWR 1\r'
elif (power == 'OFF'):
command = '%1POWR 0\r'
else:
command = '%1POWR ?\r'
print "Invalid command."
print "Use: IP ON|OFF"
print "getting current status of device:"
new = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new.connect((monitor_ip, port))
recv_data = new.recv(buffer_size)
print recv_data
print "sending: ", command
new.send(command)
recv_data = new.recv(buffer_size)
print recv_data
new.close()
Here is example of use:
root@raspberrypi:/var/www/cgi-bin# ./PJLinkPower.py 10.0.0.40 ON
PJLINK 0
sending: %1POWR 1
%1POWR=OK
root@raspberrypi:/var/www/cgi-bin# ./PJLinkPower.py 10.0.0.40 OFF
PJLINK 0
sending: %1POWR 0
%1POWR=OK
root@raspberrypi:/var/www/cgi-bin#
root@raspberrypi:/var/www/cgi-bin# ./PJLinkPower.py 10.0.0.40 TEST
Invalid command.
Use: IP ON|OFF
getting current status of device:
PJLINK 0
sending: %1POWR ?
%1POWR=0
I have few devices which are creating my home entertainment system:
NEC monitor (sometimes good monitor is better then TV)
Yamaha HTR-4065
PC Zotac
Evolveo DaulCoder DVB-T
XBOX 360
All those are connected to network via ethernet and there is some way to monitor or even to remotely control, lets start one by one:
DVB-T and PC
As you might know one of the easiest way to check if device is up or down would be to ping it, however in case of audio/video systems it's bit hard because if they are in Standby mode they will answer to the ping.
In some cases I don't even give the device permanent IP(DVB-T and PC). However there is the way to discover them via UPnP.
There is linux utility gssdp-discovery which use ssdp to discover UPnP capable device and as far as each device have unique identificator uuid we can check if device is running by checking if device is responding to ssdp discovery.
Below is small script which check based on uuid if device is running, which IP it was assigned and on which port UPnP is listening and record this to sqlite database.
#! /bin/bash
#test if device can be discovered using SSDP
#IMPORTANT return true if device is down
testUPnP()
{
return `gssdp-discover -n 10 -t uuid:$1 | grep "resource available" | wc -l`
}
HFILE='/var/www/monitoring/device_upnp.list'
DBFILE='/var/www/judo.db'
if [[ -z $HFILE ]]
then
echo "$HFILE doesn't exist"
exit 1
fi
while read line
do
uuid=$(echo "$line" | cut -d, -f5)
mac=$(echo "$line" | cut -d, -f4)
new_polls=1
prev_status_polls=$(sqlite3 $DBFILE "SELECT cur_state,polls_in_state FROM dev_status WHERE mac = '$mac';")
prev_status=$(echo "$prev_status_polls" | cut -d\| -f1)
prev_polls=$(echo "$prev_status_polls" | cut -d\| -f2)
#remeber testUPnP returns true if device is down
if `testUPnP $uuid`
then
new_status="Down"
if [ "$prev_status" == "Up" ]
then
sqlite3 $DBFILE "UPDATE devices SET ip = NULL, upnp_port = NULL WHERE mac = '$mac';"
fi
else
new_status="Up"
if [ "$prev_status" == "Down" ]
then
location=`gssdp-discover -n 10 -t uuid:$uuid | grep "Location:" | sed 's/ *Location: http:\/\///g'| cut -d/ -f1 | sed 's/\///g'`
echo "$location"
ip=$(echo "$location" | cut -d: -f1)
upnp_port=$(echo "$location" | cut -d: -f2)
echo "$ip:$upnp_port"
sqlite3 $DBFILE "UPDATE devices SET ip = '$ip', upnp_port = '$upnp_port' WHERE mac = '$mac';"
fi
fi
if [ "$prev_status" == "$new_status" ]
then
new_polls=$((prev_polls + 1))
fi
sqlite3 $DBFILE "UPDATE dev_status SET cur_state = '$new_status', polls_in_state = '$new_polls' WHERE mac = '$mac';"
echo "$uuid,$mac,$new_status,$prev_status,$new_polls"
done < ${HFILE}
Yamaha HTR-4065
Even though Yamaha have UPnP we will rather use YNC to get the status, because we also want to know which input is actually active.
Below is python script which finds the status and input and store that in sqlite database.
NEC responds to ping in Standby and of course don't have UPnP, however the NEC remote control over ethernet let us find out the actual status.
Below python script check the status and store it in sqlite database.
#! /usr/bin/python
import sys
import socket
import sqlite3
monitor_ip = '10.0.0.35'
port = 7142
buffer_size = 2048
data_on = '\x01\x30\x41\x30\x41\x30\x36\x02\x30\x31\x44\x36\x03\x74\x0d'
new = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new.connect((monitor_ip, port))
new.send(data_on)
data = ''
#message is split over several packets so need to get all content, message we expect have 25bytes
packet = new.recv(buffer_size)
data += packet
while len(data) < 25 or not packet:
packet = new.recv(buffer_size)
data += packet
new.close()
#Reponse OFF
#00AB120200D60000040004q
#Response ON
#00AB120200D60000040001t
off_response = b'\x30\x30\x41\x42\x31\x32\x30\x32\x30\x30\x44\x36\x30\x30\x30\x30\x30\x34\x30\x30\x30\x34'
on_response = bytes("\x30\x30\x41\x42\x31\x32\x30\x32\x30\x30\x44\x36\x30\x30\x30\x30\x30\x34\x30\x30\x30\x31")
#there seems to be some not printable chars because of packet fragmentation
#using index is dirty hack but don't care
if data[23] == off_response[21]:
power_status = "Off"
elif data[23] == on_response[21]:
power_status = "On"
else:
power_status = "Unknown"
#Now we need to update sqlite with status
select_query = "SELECT cur_state,polls_in_state FROM dev_status WHERE mac = '00:25:5c:2e:36:39';"
#print update_query
new_polls=1
judodb = sqlite3.connect('/var/www/judo.db')
cur = judodb.cursor()
rows = cur.execute(select_query)
for row in rows:
prev_state = row[0]
prev_polls = row[1]
if prev_state == power_status:
new_polls = prev_polls + 1
print prev_state + "\t\t" + str(new_polls)
update_query = "UPDATE dev_status SET cur_state = \'" + power_status + "\', polls_in_state = " + str(new_polls) + " WHERE mac = '00:25:5c:2e:36:39'"
cur.execute(update_query)
judodb.commit()
judodb.close()
XBOX 360
For monitoring of XBOX 360 I've have to use arping and UPnP, because of limitation of XBOX 360 OS more can be find here including the script.
Automatic Switch Off
Now as we have ways of getting status we just use crontab to run the scripts each 10 minutes and to get the status and store it in to sqlite database. We are storing also how many time we see it consecutively in current status. (polls_in_state column in table below)
Last thing we have to do is set crontab to run script, which check status based on the status of devices finds what is running and if no device coresponding to AV receiver input is running, (means that AV receiver is running for nothing) it will switch it off. Same for NEC.
Two years back I bought NEC MultiSync V321 one of the reasons was it have ethernet port and you can control it over the network.
The other reason is that there is documentation for it provided by NEC which can help you with commands, if you are intelligent and good enough to understand it. Which is probably not my case.
However NEC also offers NEC PD comms tool, which is application which let you control your NEC.
As I have Yamaha AV reciever (for which Yamaha provides control app) my intention recently was to have small app just to switch on/off the monitor and so limit number of remote controlers.
I also recently bought raspberry Pi and find good use for it. I'm running web server there and using cgi I can switch on/off my NEC and also Yamaha (will write about it in future as I will implement more features).
So what I did, I used NEC PD comms tool and wireshark to catch the tcp data payload for on and off, I wrote few lines in python to send those data and add few more to work nicely as cgi.
Here are the source codes:
NECOn.py
#! /usr/bin/python
import socket
monitor_ip = '10.0.0.35'
port = 7142
buffer_size = 1024
data_on = '\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x30\x33\x44\x36\x30\x30\x30\x31\x03\x73\x0d'
new = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new.connect((monitor_ip, port))
new.send(data_on)
recv_data = new.recv(buffer_size)
new.close()
print """
<html>
<head>
<meta http-equiv="refresh" content="0;url=http://10.0.0.37" />
<title>You are going to be redirected</title>
</head>
<body>
Redirecting...
</body>
</html>
"""
For monitoring purpose I needed to find the howto check status of my NEC and recorded in the Database(I use sqlite3), even the response is just 25 bytes NEC dived it into 2 packets, so we are recording all packets and then check if the On or Off.
NECStatus.py
#! /usr/bin/python
import sys
import socket
import sqlite3
monitor_ip = '10.0.0.35'
port = 7142
buffer_size = 2048
data_on = '\x01\x30\x41\x30\x41\x30\x36\x02\x30\x31\x44\x36\x03\x74\x0d'
new = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new.connect((monitor_ip, port))
new.send(data_on)
data = ''
#message is split over several packets so need to get all content, message we expect have 25bytes
packet = new.recv(buffer_size)
data += packet
while len(data) < 25 or not packet:
packet = new.recv(buffer_size)
data += packet
new.close()
#Reponse OFF
#00AB120200D60000040004q
#Response ON
#00AB120200D60000040001t
off_response = b'\x30\x30\x41\x42\x31\x32\x30\x32\x30\x30\x44\x36\x30\x30\x30\x30\x30\x34\x30\x30\x30\x34'
on_response = bytes("\x30\x30\x41\x42\x31\x32\x30\x32\x30\x30\x44\x36\x30\x30\x30\x30\x30\x34\x30\x30\x30\x31")
#there seems to be some not printable chars because of packet fragmentation
#using index is dirty hack but don't care
if data[23] == off_response[21]:
power_status = "Off"
elif data[23] == on_response[21]:
power_status = "On"
else:
power_status = "Unknown"
#Now we need to update sqlite with status
select_query = "SELECT cur_state,polls_in_state FROM dev_status WHERE mac = '00:25:5c:2e:36:39';"
#print update_query
new_polls=1
judodb = sqlite3.connect('/var/www/judo.db')
cur = judodb.cursor()
rows = cur.execute(select_query)
for row in rows:
prev_state = row[0]
prev_polls = row[1]
if prev_state == power_status:
new_polls = prev_polls + 1
print prev_state + "\t\t" + str(new_polls)
update_query = "UPDATE dev_status SET cur_state = \'" + power_status + "\', polls_in_state = " + str(new_polls) + " WHERE mac = '00:25:5c:2e:36:39'"
cur.execute(update_query)
judodb.commit()
judodb.close()
If you have any questions you can always contact me.