pysdcp-extended 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
pySDCP: Copyright (c) 2017 Guy Shapira
|
|
4
|
+
pySDCP-extended: Copyright (c) 2024 kennymc.c
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pysdcp-extended
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extended SDCP / PJ Talk library to control Sony projectors
|
|
5
|
+
Home-page: https://github.com/kennymc-c/pySDCP-extended
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: sdcp,pjtalk,sony,projector,ip-control,home-automation
|
|
8
|
+
Author: kennymc.c
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Home Automation
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Project-URL: Repository, https://github.com/kennymc-c/pySDCP-extended
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# pySDCP-extended
|
|
24
|
+
|
|
25
|
+
<!---[](https://pypi.org/project/pysdcp-extended)--->
|
|
26
|
+
|
|
27
|
+
Extended Sony SDCP / PJ Talk projector control.
|
|
28
|
+
|
|
29
|
+
Python **3** library to query and control Sony Projectors using SDCP (PJ Talk) protocol over IP.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
* Auto discover projector using SDAP (Simple Display Advertisement Protocol)
|
|
34
|
+
* Query and change power & input (HDMI 1 + 2)
|
|
35
|
+
* Set aspect ratio/zoom and calibration presets
|
|
36
|
+
|
|
37
|
+
### Extended Features
|
|
38
|
+
|
|
39
|
+
* Support for more commands (added to protocol.py)
|
|
40
|
+
* Query and set picture muting
|
|
41
|
+
* Query lamp hours
|
|
42
|
+
* Query model name and serial number
|
|
43
|
+
* Show response error message from the projector
|
|
44
|
+
* Set a custom PJ Talk community & UDP advertisement SDAP port and TCP SDCP port
|
|
45
|
+
|
|
46
|
+
## Protocol Documentation
|
|
47
|
+
|
|
48
|
+
* [Link](https://www.digis.ru/upload/iblock/f5a/VPL-VW320,%20VW520_ProtocolManual.pdf)
|
|
49
|
+
* [Link](https://docs.sony.com/release/VW100_protocol.pdf)
|
|
50
|
+
|
|
51
|
+
## Supported Projectors
|
|
52
|
+
|
|
53
|
+
Supported Sony projectors should include:
|
|
54
|
+
|
|
55
|
+
* VPL-HW65ES
|
|
56
|
+
* VPL-VW100
|
|
57
|
+
* VPL-VW260
|
|
58
|
+
* VPL-VW270
|
|
59
|
+
* VPL-VW285
|
|
60
|
+
* VPL-VW315
|
|
61
|
+
* VPL-VW320
|
|
62
|
+
* VPL-VW328
|
|
63
|
+
* VPL-VW365
|
|
64
|
+
* VPL-VW515
|
|
65
|
+
* VPL-VW520
|
|
66
|
+
* VPL-VW528
|
|
67
|
+
* VPL-VW665
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```pip install pysdcp-extended```
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
Sending any command will initiate auto discovery of the projector if none is known and will carry on the command. So just go for it and maybe you get lucky
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import pysdcp_extended
|
|
79
|
+
|
|
80
|
+
my_projector = pysdcp_extended.Projector()
|
|
81
|
+
|
|
82
|
+
my_projector.get_power()
|
|
83
|
+
my_projector.set_power(True)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Skip discovery to save time or if you know the IP of the projector
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
my_known_projector = pysdcp.Projector('10.1.2.3')
|
|
90
|
+
my_known_projector.set_HDMI_input(2)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
You can also set a custom PJ Talk community and tcp/udp port. By default "SONY" will be used as the community and 53862 as udp port for SDAP advertisement and 53484 as tcp port for SDCP
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
my_known_projector = pysdcp.Projector(ip='10.1.2.3', community="THEATER", udp_port=53860, tcp_port=53480)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Commands from protocol.py
|
|
100
|
+
|
|
101
|
+
While you can use the build in functions like get_power() or set_HDMI_input() you can also directly send any command from protocol.py like this
|
|
102
|
+
If you need to use more commands, just add to _protocol.py_, and send it like this:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from pysdcp_extended.protocol.py import *
|
|
106
|
+
|
|
107
|
+
my_projector._send_command(action=ACTIONS["SET"], command=COMMANDS_IR["CURSOR_UP"])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Please note that commands in `COMMANDS_IR` work as fire and forget and you only get a response if there is a timeout.
|
|
111
|
+
|
|
112
|
+
## Credits
|
|
113
|
+
|
|
114
|
+
This plugin is an extended fork of [pySDCP](https://github.com/Galala7/pySDCP) by [Galala7](https://github.com/Galala7) which is based on [sony-sdcp-com](https://github.com/vokkim/sony-sdcp-com) NodeJS library by [vokkim](https://github.com/vokkim).
|
|
115
|
+
|
|
116
|
+
## See also
|
|
117
|
+
|
|
118
|
+
* [homebridge-sony-sdcp](https://github.com/Galala7/homebridge-sony-sdcp) - Homebridge plugin to control Sony Projectors (based on Galala7/pySDCP)
|
|
119
|
+
* [ucr2-integration-sonySDCP](https://github.com/kennymc-c/ucr2-integration-sonySDCP) - SDCP integration for Unfolded Circle Remote devices
|
|
120
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# pySDCP-extended
|
|
2
|
+
|
|
3
|
+
<!---[](https://pypi.org/project/pysdcp-extended)--->
|
|
4
|
+
|
|
5
|
+
Extended Sony SDCP / PJ Talk projector control.
|
|
6
|
+
|
|
7
|
+
Python **3** library to query and control Sony Projectors using SDCP (PJ Talk) protocol over IP.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
* Auto discover projector using SDAP (Simple Display Advertisement Protocol)
|
|
12
|
+
* Query and change power & input (HDMI 1 + 2)
|
|
13
|
+
* Set aspect ratio/zoom and calibration presets
|
|
14
|
+
|
|
15
|
+
### Extended Features
|
|
16
|
+
|
|
17
|
+
* Support for more commands (added to protocol.py)
|
|
18
|
+
* Query and set picture muting
|
|
19
|
+
* Query lamp hours
|
|
20
|
+
* Query model name and serial number
|
|
21
|
+
* Show response error message from the projector
|
|
22
|
+
* Set a custom PJ Talk community & UDP advertisement SDAP port and TCP SDCP port
|
|
23
|
+
|
|
24
|
+
## Protocol Documentation
|
|
25
|
+
|
|
26
|
+
* [Link](https://www.digis.ru/upload/iblock/f5a/VPL-VW320,%20VW520_ProtocolManual.pdf)
|
|
27
|
+
* [Link](https://docs.sony.com/release/VW100_protocol.pdf)
|
|
28
|
+
|
|
29
|
+
## Supported Projectors
|
|
30
|
+
|
|
31
|
+
Supported Sony projectors should include:
|
|
32
|
+
|
|
33
|
+
* VPL-HW65ES
|
|
34
|
+
* VPL-VW100
|
|
35
|
+
* VPL-VW260
|
|
36
|
+
* VPL-VW270
|
|
37
|
+
* VPL-VW285
|
|
38
|
+
* VPL-VW315
|
|
39
|
+
* VPL-VW320
|
|
40
|
+
* VPL-VW328
|
|
41
|
+
* VPL-VW365
|
|
42
|
+
* VPL-VW515
|
|
43
|
+
* VPL-VW520
|
|
44
|
+
* VPL-VW528
|
|
45
|
+
* VPL-VW665
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```pip install pysdcp-extended```
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
Sending any command will initiate auto discovery of the projector if none is known and will carry on the command. So just go for it and maybe you get lucky
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import pysdcp_extended
|
|
57
|
+
|
|
58
|
+
my_projector = pysdcp_extended.Projector()
|
|
59
|
+
|
|
60
|
+
my_projector.get_power()
|
|
61
|
+
my_projector.set_power(True)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Skip discovery to save time or if you know the IP of the projector
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
my_known_projector = pysdcp.Projector('10.1.2.3')
|
|
68
|
+
my_known_projector.set_HDMI_input(2)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
You can also set a custom PJ Talk community and tcp/udp port. By default "SONY" will be used as the community and 53862 as udp port for SDAP advertisement and 53484 as tcp port for SDCP
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
my_known_projector = pysdcp.Projector(ip='10.1.2.3', community="THEATER", udp_port=53860, tcp_port=53480)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Commands from protocol.py
|
|
78
|
+
|
|
79
|
+
While you can use the build in functions like get_power() or set_HDMI_input() you can also directly send any command from protocol.py like this
|
|
80
|
+
If you need to use more commands, just add to _protocol.py_, and send it like this:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pysdcp_extended.protocol.py import *
|
|
84
|
+
|
|
85
|
+
my_projector._send_command(action=ACTIONS["SET"], command=COMMANDS_IR["CURSOR_UP"])
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Please note that commands in `COMMANDS_IR` work as fire and forget and you only get a response if there is a timeout.
|
|
89
|
+
|
|
90
|
+
## Credits
|
|
91
|
+
|
|
92
|
+
This plugin is an extended fork of [pySDCP](https://github.com/Galala7/pySDCP) by [Galala7](https://github.com/Galala7) which is based on [sony-sdcp-com](https://github.com/vokkim/sony-sdcp-com) NodeJS library by [vokkim](https://github.com/vokkim).
|
|
93
|
+
|
|
94
|
+
## See also
|
|
95
|
+
|
|
96
|
+
* [homebridge-sony-sdcp](https://github.com/Galala7/homebridge-sony-sdcp) - Homebridge plugin to control Sony Projectors (based on Galala7/pySDCP)
|
|
97
|
+
* [ucr2-integration-sonySDCP](https://github.com/kennymc-c/ucr2-integration-sonySDCP) - SDCP integration for Unfolded Circle Remote devices
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "pysdcp-extended"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Extended SDCP / PJ Talk library to control Sony projectors"
|
|
5
|
+
authors = ["kennymc.c"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/kennymc-c/pySDCP-extended"
|
|
9
|
+
keywords = ["sdcp", "pjtalk", "sony", "projector", "ip-control", "home-automation"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Topic :: Software Development :: Libraries",
|
|
15
|
+
"Topic :: Home Automation",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
]
|
|
18
|
+
# Need as the package name contains a hyphen
|
|
19
|
+
packages = [
|
|
20
|
+
{ include = "pysdcp_extended" },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.poetry.dependencies]
|
|
24
|
+
python = "^3.11"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["poetry-core"]
|
|
29
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#! py3
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
from struct import *
|
|
6
|
+
|
|
7
|
+
from pysdcp_extended.protocol import *
|
|
8
|
+
|
|
9
|
+
Header = namedtuple("Header", ['version', 'category', 'community'])
|
|
10
|
+
ProjInfo = namedtuple("ProjInfo", ['id', 'product_name', 'serial_number', 'power_state', 'location'])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_command_buffer(header: Header, action, command, data=None):
|
|
14
|
+
# create bytearray in the right size
|
|
15
|
+
if data is not None:
|
|
16
|
+
my_buf = bytearray(12)
|
|
17
|
+
else:
|
|
18
|
+
my_buf = bytearray(10)
|
|
19
|
+
# header
|
|
20
|
+
my_buf[0] = 2 # only works with version 2, don't know why
|
|
21
|
+
my_buf[1] = header.category
|
|
22
|
+
# community
|
|
23
|
+
my_buf[2] = ord(header.community[0])
|
|
24
|
+
my_buf[3] = ord(header.community[1])
|
|
25
|
+
my_buf[4] = ord(header.community[2])
|
|
26
|
+
my_buf[5] = ord(header.community[3])
|
|
27
|
+
# command
|
|
28
|
+
my_buf[6] = action
|
|
29
|
+
pack_into(">H", my_buf, 7, command)
|
|
30
|
+
if data is not None:
|
|
31
|
+
# add data len
|
|
32
|
+
my_buf[9] = 2 # Data is always 2 bytes
|
|
33
|
+
# add data
|
|
34
|
+
pack_into(">H", my_buf, 10, data)
|
|
35
|
+
else:
|
|
36
|
+
my_buf[9] = 0
|
|
37
|
+
return my_buf
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def process_command_response(msgBuf):
|
|
41
|
+
my_header = Header(
|
|
42
|
+
version=int(msgBuf[0]),
|
|
43
|
+
category=int(msgBuf[1]),
|
|
44
|
+
community=decode_text_field(msgBuf[2:6]))
|
|
45
|
+
is_success = bool(msgBuf[6])
|
|
46
|
+
command = unpack(">H", msgBuf[7:9])[0]
|
|
47
|
+
data_len = int(msgBuf[9])
|
|
48
|
+
if data_len != 0:
|
|
49
|
+
data = unpack(">H", msgBuf[10:10 + data_len])[0]
|
|
50
|
+
else:
|
|
51
|
+
data = None
|
|
52
|
+
return my_header, is_success, command, data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def process_SDAP(SDAP_buffer) -> (Header, ProjInfo):
|
|
56
|
+
try:
|
|
57
|
+
my_header = Header(
|
|
58
|
+
version=int(SDAP_buffer[2]),
|
|
59
|
+
category=int(SDAP_buffer[3]),
|
|
60
|
+
community=decode_text_field(SDAP_buffer[4:8]))
|
|
61
|
+
my_info = ProjInfo(
|
|
62
|
+
id=SDAP_buffer[0:2].decode(),
|
|
63
|
+
product_name=decode_text_field(SDAP_buffer[8:20]),
|
|
64
|
+
serial_number=unpack('>I', SDAP_buffer[20:24])[0],
|
|
65
|
+
power_state=unpack('>H', SDAP_buffer[24:26])[0],
|
|
66
|
+
location=decode_text_field(SDAP_buffer[26:]))
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print("Error parsing SDAP packet: {}".format(e))
|
|
69
|
+
raise
|
|
70
|
+
return my_header, my_info
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def decode_text_field(buf):
|
|
74
|
+
"""
|
|
75
|
+
Convert char[] string in buffer to python str object
|
|
76
|
+
:param buf: bytearray with array of chars
|
|
77
|
+
:return: string
|
|
78
|
+
"""
|
|
79
|
+
return buf.decode().strip(b'\x00'.decode())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Projector:
|
|
83
|
+
def __init__(self, ip: str = None, community: str = "SONY", udp_port: int = 53862, tcp_port: int = 53484):
|
|
84
|
+
"""
|
|
85
|
+
Base class for projector communication.
|
|
86
|
+
Enables communication with Projector, Sending commands and Querying Power State
|
|
87
|
+
|
|
88
|
+
:param ip: str, IP address for projector. If given, will create a projector with default values to communicate
|
|
89
|
+
with projector on the given ip. i.e. "10.0.0.5"
|
|
90
|
+
:param community: str, PJ Talk Community for the projector. If not given "SONY" will be used
|
|
91
|
+
:param udp_port: int, SDAP Advertisement UDP port. If not given 53862 will be used
|
|
92
|
+
:param tcp_port: int, PJ Talk/SDCP TCP port. If not given 53484 will be used
|
|
93
|
+
"""
|
|
94
|
+
self.info = ProjInfo(
|
|
95
|
+
product_name=None,
|
|
96
|
+
serial_number=None,
|
|
97
|
+
power_state=None,
|
|
98
|
+
location=None,
|
|
99
|
+
id=None)
|
|
100
|
+
if ip is None:
|
|
101
|
+
# Create empty Projector object
|
|
102
|
+
self.ip = None
|
|
103
|
+
self.header = Header(version=None, category=None, community=None)
|
|
104
|
+
self.is_init = False
|
|
105
|
+
else:
|
|
106
|
+
# Create projector from known ip
|
|
107
|
+
# Set default values to enable immediately communication with known project (ip)
|
|
108
|
+
self.ip = ip
|
|
109
|
+
self.header = Header(category=10, version=2, community=community)
|
|
110
|
+
self.is_init = True
|
|
111
|
+
|
|
112
|
+
# Default ports
|
|
113
|
+
self.UDP_IP = ""
|
|
114
|
+
self.UDP_PORT = udp_port
|
|
115
|
+
self.TCP_PORT = tcp_port
|
|
116
|
+
self.TCP_TIMEOUT = 2
|
|
117
|
+
self.UDP_TIMEOUT = 31
|
|
118
|
+
|
|
119
|
+
# Valid settings
|
|
120
|
+
self.SCREEN_SETTINGS = {
|
|
121
|
+
"ASPECT_RATIO": ASPECT_RATIOS,
|
|
122
|
+
"PICTURE_POSITION": PICTURE_POSITIONS,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
def __eq__(self, other):
|
|
126
|
+
return self.info.serial_number == other.info.serial_number
|
|
127
|
+
|
|
128
|
+
def _send_command(self, action, command, data=None, timeout=None):
|
|
129
|
+
timeout = timeout if timeout is not None else self.TCP_TIMEOUT
|
|
130
|
+
if not self.is_init:
|
|
131
|
+
self.find_projector()
|
|
132
|
+
if not self.is_init:
|
|
133
|
+
raise Exception("No projector found and / or specified")
|
|
134
|
+
|
|
135
|
+
my_buf = create_command_buffer(self.header, action, command, data)
|
|
136
|
+
|
|
137
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
138
|
+
sock.settimeout(timeout)
|
|
139
|
+
try:
|
|
140
|
+
sock.connect((self.ip, self.TCP_PORT))
|
|
141
|
+
sent = sock.send(my_buf)
|
|
142
|
+
except socket.timeout as e:
|
|
143
|
+
raise Exception("Timeout while trying to send command {}".format(command)) from e
|
|
144
|
+
|
|
145
|
+
if len(my_buf) != sent:
|
|
146
|
+
raise ConnectionError(
|
|
147
|
+
"Failed sending entire buffer to projector. Sent {} out of {} !".format(sent, len(my_buf)))
|
|
148
|
+
|
|
149
|
+
#Check if command is an simulated ir command without a response from the projector and always return true to avoid a timeout
|
|
150
|
+
if data is None and str(hex(command)).startswith(("0x17", "0x19", "0x1B")):
|
|
151
|
+
sock.close()
|
|
152
|
+
|
|
153
|
+
return True
|
|
154
|
+
else:
|
|
155
|
+
response_buf = sock.recv(1024)
|
|
156
|
+
|
|
157
|
+
sock.close()
|
|
158
|
+
|
|
159
|
+
_, is_success, _, data = process_command_response(response_buf)
|
|
160
|
+
|
|
161
|
+
if not is_success:
|
|
162
|
+
command = "{:x}".format(command)
|
|
163
|
+
try:
|
|
164
|
+
error_msg = RESPONSE_ERRORS[data]
|
|
165
|
+
except KeyError:
|
|
166
|
+
error_code = "{:x}".format(data)
|
|
167
|
+
error_msg = "Unknown error code: " + error_code
|
|
168
|
+
raise Exception("Received failed status from projector while sending command 0x" + command + ". " + error_msg)
|
|
169
|
+
|
|
170
|
+
return data
|
|
171
|
+
|
|
172
|
+
def find_projector(self, udp_ip: str = None, udp_port: int = None, timeout=None):
|
|
173
|
+
|
|
174
|
+
self.UDP_PORT = udp_port if udp_port is not None else self.UDP_PORT
|
|
175
|
+
self.UDP_IP = udp_ip if udp_ip is not None else self.UDP_IP
|
|
176
|
+
timeout = timeout if timeout is not None else self.UDP_TIMEOUT
|
|
177
|
+
|
|
178
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
179
|
+
|
|
180
|
+
sock.bind((self.UDP_IP, self.UDP_PORT))
|
|
181
|
+
|
|
182
|
+
sock.settimeout(timeout)
|
|
183
|
+
try:
|
|
184
|
+
SDAP_buffer, addr = sock.recvfrom(1028)
|
|
185
|
+
except socket.timeout:
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
self.header, self.info = process_SDAP(SDAP_buffer)
|
|
189
|
+
self.ip = addr[0]
|
|
190
|
+
self.is_init = True
|
|
191
|
+
|
|
192
|
+
def get_pjinfo(self, udp_ip: str = None, udp_port: int = None, timeout=None):
|
|
193
|
+
'''
|
|
194
|
+
Returns ip, serial and model name from projector via SDAP advertisement service as a dictionary. Can take up to 30 seconds.
|
|
195
|
+
'''
|
|
196
|
+
self.UDP_PORT = udp_port if udp_port is not None else self.UDP_PORT
|
|
197
|
+
self.UDP_IP = udp_ip if udp_ip is not None else self.UDP_IP
|
|
198
|
+
timeout = timeout if timeout is not None else self.UDP_TIMEOUT
|
|
199
|
+
|
|
200
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
201
|
+
|
|
202
|
+
sock.bind((self.UDP_IP, self.UDP_PORT))
|
|
203
|
+
|
|
204
|
+
sock.settimeout(timeout)
|
|
205
|
+
try:
|
|
206
|
+
SDAP_buffer, addr = sock.recvfrom(1028)
|
|
207
|
+
except socket.timeout as e:
|
|
208
|
+
raise Exception("Timeout while waiting for data from projector") from e
|
|
209
|
+
|
|
210
|
+
serial = unpack('>I', SDAP_buffer[20:24])[0]
|
|
211
|
+
model = decode_text_field(SDAP_buffer[8:20])
|
|
212
|
+
ip = addr[0]
|
|
213
|
+
|
|
214
|
+
result = {"model":model, "serial":serial, "ip":ip}
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def set_power(self, on=True):
|
|
219
|
+
self._send_command(action=ACTIONS["SET"], command=COMMANDS["SET_POWER"],
|
|
220
|
+
data=POWER_STATUS["START_UP"] if on else POWER_STATUS["STANDBY"])
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
def set_HDMI_input(self, hdmi_num: int):
|
|
224
|
+
self._send_command(action=ACTIONS["SET"], command=COMMANDS["INPUT"],
|
|
225
|
+
data=INPUTS["HDMI1"] if hdmi_num == 1 else INPUTS["HDMI2"])
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
def get_input(self):
|
|
229
|
+
data = self._send_command(action=ACTIONS["GET"], command=COMMANDS["INPUT"])
|
|
230
|
+
if data == INPUTS["HDMI1"]:
|
|
231
|
+
return "HDMI 1"
|
|
232
|
+
elif data == INPUTS["HDMI2"]:
|
|
233
|
+
return "HDMI 2"
|
|
234
|
+
|
|
235
|
+
def set_screen(self, command: str, value: str):
|
|
236
|
+
valid_values = self.SCREEN_SETTINGS.get(command)
|
|
237
|
+
if valid_values is None:
|
|
238
|
+
raise Exception("Invalid screen setting {}".format(command))
|
|
239
|
+
|
|
240
|
+
if value not in valid_values:
|
|
241
|
+
raise Exception("Invalid parameter: {}. Expected one of: {}".format(value, valid_values.keys()))
|
|
242
|
+
|
|
243
|
+
self._send_command(action=ACTIONS["SET"], command=COMMANDS[command],
|
|
244
|
+
data=valid_values[value])
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
def get_power(self):
|
|
248
|
+
data = self._send_command(action=ACTIONS["GET"], command=COMMANDS["GET_STATUS_POWER"])
|
|
249
|
+
if data == POWER_STATUS["STANDBY"] or data == POWER_STATUS["COOLING"] or data == POWER_STATUS["COOLING2"]:
|
|
250
|
+
return False
|
|
251
|
+
else:
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
def get_muting(self):
|
|
255
|
+
data = self._send_command(action=ACTIONS["GET"], command=COMMANDS["PICTURE_MUTING"])
|
|
256
|
+
if data == PICTURE_MUTING["OFF"]:
|
|
257
|
+
return False
|
|
258
|
+
else:
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
def set_muting(self, on=True):
|
|
262
|
+
self._send_command(action=ACTIONS["SET"], command=COMMANDS["PICTURE_MUTING"],
|
|
263
|
+
data=PICTURE_MUTING["ON"] if on else PICTURE_MUTING["OFF"])
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
def get_lamp_hours(self):
|
|
267
|
+
data = self._send_command(action=ACTIONS["GET"], command=COMMANDS["GET_STATUS_LAMP_TIMER"])
|
|
268
|
+
hours = "{:d}".format(data)
|
|
269
|
+
return hours
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == '__main__':
|
|
273
|
+
# b = Projector()
|
|
274
|
+
# b.find_projector(timeout=1)
|
|
275
|
+
# # print(b.get_power())
|
|
276
|
+
# # b = Projector("10.0.0.139")
|
|
277
|
+
# # #
|
|
278
|
+
# print(b.get_power())
|
|
279
|
+
# print(b.set_power(False))
|
|
280
|
+
# # import time
|
|
281
|
+
# # time.sleep(7)
|
|
282
|
+
# print (b.set_HDMI_input(1))
|
|
283
|
+
# # time.sleep(7)
|
|
284
|
+
# # print (b.set_HDMI_input(2))
|
|
285
|
+
pass
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Defines and protocol details from here: https://www.digis.ru/upload/iblock/f5a/VPL-VW320,%20VW520_ProtocolManual.pdf
|
|
2
|
+
|
|
3
|
+
ACTIONS = {
|
|
4
|
+
"GET": 0x01,
|
|
5
|
+
"SET": 0x00
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#Command
|
|
9
|
+
|
|
10
|
+
#Fire and forget, no response from the projector
|
|
11
|
+
COMMANDS_IR = {
|
|
12
|
+
#PROJECTOR=17, PROJECTOR-E=19, PROJECTOR-EE=1B
|
|
13
|
+
"MENU": 0x1729,
|
|
14
|
+
"CURSOR_RIGHT": 0x1733,
|
|
15
|
+
"CURSOR_LEFT": 0x1734,
|
|
16
|
+
"CURSOR_UP": 0x1735,
|
|
17
|
+
"CURSOR_DOWN": 0x1736,
|
|
18
|
+
"CURSOR_ENTER": 0x175A,
|
|
19
|
+
"LENS_SHIFT_UP": 0x1772,
|
|
20
|
+
"LENS_SHIFT_DOWN": 0x1773,
|
|
21
|
+
"LENS_SHIFT_LEFT": 0x1902,
|
|
22
|
+
"LENS_SHIFT_RIGHT": 0x1903,
|
|
23
|
+
"LENS_FOCUS_FAR": 0x1774,
|
|
24
|
+
"LENS_FOCUS_NEAR": 0x1775,
|
|
25
|
+
"LENS_ZOOM_LARGE": 0x1777,
|
|
26
|
+
"LENS_ZOOM_SMALL": 0x1778,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
COMMANDS = {
|
|
30
|
+
"SET_POWER": 0x0130,
|
|
31
|
+
"CALIBRATION_PRESET": 0x0002,
|
|
32
|
+
"LAMP_CONTROL": 0x001A,
|
|
33
|
+
"MOTIONFLOW": 0x0059,
|
|
34
|
+
"HDR": 0x007C,
|
|
35
|
+
"INPUT_LAG_REDUCTION": 0x0099,
|
|
36
|
+
"PICTURE_POSITION": 0x0066,
|
|
37
|
+
"ASPECT_RATIO": 0x0020,
|
|
38
|
+
"HDMI1_DYNAMIC_RANGE": 0x006E,
|
|
39
|
+
"HDMI2_DYNAMIC_RANGE": 0x006F,
|
|
40
|
+
"2D_3D_DISPLAY_SELECT": 0x0060,
|
|
41
|
+
"3D_FORMAT": 0x0061,
|
|
42
|
+
"INPUT": 0x0001,
|
|
43
|
+
"PICTURE_MUTING": 0x0030,
|
|
44
|
+
"MENU_POSITION": 0x00A6,
|
|
45
|
+
"GET_STATUS_ERROR": 0x0101,
|
|
46
|
+
"GET_STATUS_POWER": 0x0102,
|
|
47
|
+
"GET_STATUS_LAMP_TIMER": 0x0113,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#Data
|
|
51
|
+
|
|
52
|
+
CALIBRATION_PRESETS = {
|
|
53
|
+
"CINEMA_FILM_1": 0x0000,
|
|
54
|
+
"CINEMA_FILM_2": 0x0001,
|
|
55
|
+
"REF": 0x0002,
|
|
56
|
+
"TV": 0x0003,
|
|
57
|
+
"PHOTO": 0x0004,
|
|
58
|
+
"GAME": 0x0005,
|
|
59
|
+
"BRIGHT_CINEMA": 0x0006,
|
|
60
|
+
"BRIGHT_TV": 0x0007,
|
|
61
|
+
"USER": 0x0008,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
LAMP_CONTROL= {
|
|
65
|
+
"LOW": 0x0000,
|
|
66
|
+
"HIGH": 0x0001,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
MOTIONFLOW = {
|
|
70
|
+
"OFF": 0x0000,
|
|
71
|
+
"SMOTH_HIGH": 0x0001,
|
|
72
|
+
"SMOTH_LOW": 0x0002,
|
|
73
|
+
"IMPULSE": 0x0003,
|
|
74
|
+
"COMBINATION": 0x0004,
|
|
75
|
+
"TRUE_CINEMA": 0x0005
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
HDR = {
|
|
79
|
+
"OFF": 0x0000,
|
|
80
|
+
"ON": 0x0001,
|
|
81
|
+
"AUTO": 0x0002,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
INPUT_LAG_REDUCTION= {
|
|
85
|
+
"OFF": 0x0000,
|
|
86
|
+
"ON": 0x0001,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
PICTURE_POSITIONS = {
|
|
90
|
+
"1_85": 0x0000,
|
|
91
|
+
"2_35": 0x0001,
|
|
92
|
+
"CUSTOM_1": 0x0002,
|
|
93
|
+
"CUSTOM_2": 0x0003,
|
|
94
|
+
"CUSTOM_3": 0x0004
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
ASPECT_RATIOS = {
|
|
98
|
+
"NORMAL": 0x0001,
|
|
99
|
+
"V_STRETCH": 0x000B,
|
|
100
|
+
"ZOOM_1_85": 0x000C,
|
|
101
|
+
"ZOOM_2_35": 0x000D,
|
|
102
|
+
"STRETCH": 0x000E,
|
|
103
|
+
"SQUEEZE": 0x000F
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
DYNAMIC_RANGES = {
|
|
107
|
+
"AUTO": 0x0000,
|
|
108
|
+
"LIMITED": 0x0001,
|
|
109
|
+
"FULL": 0x0002
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
TWO_D_THREE_D_SELECT = {
|
|
113
|
+
"AUTO": 0x0000,
|
|
114
|
+
"3D": 0x0001,
|
|
115
|
+
"2D": 0x0002,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
THREE_D_FORMATS = {
|
|
119
|
+
"SIMULATED_3D": 0x0000,
|
|
120
|
+
"SIDE_BY_SIDE": 0x0001,
|
|
121
|
+
"OVER_UNDER": 0x0002,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
INPUTS = {
|
|
125
|
+
"HDMI1": 0x002,
|
|
126
|
+
"HDMI2": 0x003,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
PICTURE_MUTING = {
|
|
130
|
+
"OFF": 0x0000,
|
|
131
|
+
"ON": 0x0001,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
MENU_POSITIONS= {
|
|
135
|
+
"BOTTOM_LEFT": 0x0000,
|
|
136
|
+
"CENTER": 0x0001,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#Response data
|
|
140
|
+
|
|
141
|
+
ERROR_STATUS = {
|
|
142
|
+
"NO_ERROR": 0,
|
|
143
|
+
"LAMP_ERROR": 1,
|
|
144
|
+
"FAN_ERROR": 2,
|
|
145
|
+
"COVER_ERROR": 4,
|
|
146
|
+
"TEMP_ERROR": 8,
|
|
147
|
+
"D5V_ERROR": 10,
|
|
148
|
+
"POWER_ERROR": 20,
|
|
149
|
+
"TEMP_WARNING": 40,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
POWER_STATUS = {
|
|
153
|
+
"STANDBY": 0,
|
|
154
|
+
"START_UP": 1,
|
|
155
|
+
"START_UP_LAMP": 2,
|
|
156
|
+
"POWER_ON": 3,
|
|
157
|
+
"COOLING": 4,
|
|
158
|
+
"COOLING2": 5
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
RESPONSE_ERRORS = {
|
|
162
|
+
0x101: "Item Error: Invalid Item",
|
|
163
|
+
0x102: "Item Error: Invalid Item Request",
|
|
164
|
+
0x103: "Item Error: Invalid Length",
|
|
165
|
+
0x104: "Item Error: Invalid Data",
|
|
166
|
+
0x111: "Item Error: Short Data",
|
|
167
|
+
0x180: "Item Error: Not Applicable Item",
|
|
168
|
+
0x201: "Community Error: Different Community",
|
|
169
|
+
0x1001: "Request Error: Invalid Version",
|
|
170
|
+
0x1002: "Request Error: Invalid Category",
|
|
171
|
+
0x1003: "Request Error: Invalid Request",
|
|
172
|
+
0x1011: "Request Error: Short Header",
|
|
173
|
+
0x1012: "Request Error: Short Community",
|
|
174
|
+
0x1013: "Request Error: Short Command",
|
|
175
|
+
0xF001: "Comm Error: Timeout",
|
|
176
|
+
0xF010: "Comm Error: Check Sum Error",
|
|
177
|
+
0xF020: "Comm Error: Framing Error",
|
|
178
|
+
0xF030: "Comm Error: Parity Error",
|
|
179
|
+
0xF040: "Comm Error: Over Run Error",
|
|
180
|
+
0xF050: "Comm Error: Other Comm Error",
|
|
181
|
+
0xF0F0: "Comm Error: Unknown Response",
|
|
182
|
+
0xF110: "NVRAM Error: Read Error",
|
|
183
|
+
0xF120: "NVRAM Error: Write Error",
|
|
184
|
+
}
|