pszafer / epson_projector

MIT License
26 stars 19 forks source link

Add serial support #1

Closed mvdwetering closed 3 years ago

mvdwetering commented 6 years ago

Would it be possible to add support for the older Epson projectors that have a serial connection instead of network (e.g. EH-TW3200) ?

The protocol is the same otherwise (also ESC/VP21).

henryk commented 6 years ago

I would have the same request, but this doesn't look promising. Even though the home-assistant docs say "ESC/VP21", looking at the code there's no ESC/VP21 anywhere here. This library seems to implement some new-fangled JSON over HTTP stuff that newer projectors apparently speak natively, triggered from a local web-UI. The JSON stuff needs to be polled, it can't even push projector status updates (as the serial connection will do).

I have ESC/VP21 code that I once wrote for FHEM in perl (https://github.com/henryk/fhem-escvp21), and am migrating from FHEM to home-assistant, so I might find time to extend here. Until then: no luck.

pszafer commented 6 years ago

I will look into that next week. I don't think it is much problem to make a "switch" to choose between http with json, tcp or serial connection. I have USB only in projector, but as I know USB is on COM driver, so shouldn't be much differenct.

@henryk do you have some documentation for ESC/VP21.net ? I've tried to send commands over 3629 port several times before, but it wasn't working for me at all.

henryk commented 6 years ago

So, apparently Epson has taken down the epson322270eu.pdf file which I originally used. https://files.support.epson.com/pdf/pl600p/pl600pcm.pdf is still online though and has everything and more.

Connectivity: My projector only has RS-232. The way I understand it, the USB connection also is just a serial port (over USB) and the raw TCP/IP connection is the same protocol. (In my setup I have https://linux.die.net/man/8/ser2net running in order to expose the RS-232 as a TCP/IP port.)

dthulke commented 6 years ago

@henryk as a minor clarification, the protocol implemented in this library is ESC/VP21. The protocol just consist of the control / query commands and is independent of the network communication protocol (which can be JSON over HTTP, serial and so on).

mvdwetering commented 6 years ago

FYI the .PDF files seem to have been replaced by .XLSX files. At least for my projector the files are listed in the "Manuals" section of the product page on the Epson website. Note that this file contains commands for a lot of devices.

Filename: ESC/VP21 Command User’s Guide (XLSX) (D) https://www.epson.nl/products/projectors/home-cinema/epson-eh-tw3200#manuals

pszafer commented 6 years ago

So, little update for you. For about two weeks I don't direct access to my Epson :-1: , so hard to test anything... Anyway, checkout testing branch https://github.com/pszafer/epson_projector/tree/testing TCP connection to Epson should work. About serial, this is very initial version. I'll try to make some virtual device so I can imitate Epson. I'll try to keep you all up to date.

pszafer commented 6 years ago

Can somebody who have serial access to projector checkout testing branch? Maybe @mvdwetering ?, just run python3 test_serial.py it should power up your projector.

My projector won't respond over usb connection, it has only MiniUSB port, so my guess that it is not intended to be use for serial communication or I need some driver...

mvdwetering commented 6 years ago

Sorry for the delay, I just had time today to put the cables back in place and test.

I first got an error NameError: name 'TIMEOUT' is not defined which I got around by replacing it with 5 which I think is 5 seconds.

After this I got an error AttributeError: 'StreamWriter' object has no attribute 'is_closing' which I avoided by removing that part of the check in the send_request() function, not sure how bad that is.

Then I started to run into:

Timeout error
Traceback (most recent call last):
  File "test_serial.py", line 25, in <module>
    loop.run_until_complete(main_serial(loop))
  File "/usr/lib/python3.5/asyncio/base_events.py", line 466, in run_until_complete
    return future.result()
  File "/usr/lib/python3.5/asyncio/futures.py", line 293, in result
    raise self._exception
  File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
    result = coro.send(None)
  File "test_serial.py", line 11, in main_serial
    await run(loop)
  File "test_serial.py", line 18, in run
    data = await projector.get_property(POWER)
  File "/home/pi/epson_projector/epson_projector/main.py", line 56, in get_property
    self.__get_timeout(command))
  File "/home/pi/epson_projector/epson_projector/projector_serial.py", line 70, in get_property
    command=command+'?\r'
  File "/home/pi/epson_projector/epson_projector/projector_serial.py", line 97, in send_request
    if response[0].decode() != ":":
AttributeError: 'int' object has no attribute 'decode'

I think this one is because of decoding only the first byte/character in the response instead of the whole reponse, but I am not sure. The encoding/decoding part of Python always confuses me and with some local changes I could not get it working.

However I added some prints to command and response and noticed that the command seems fine, but the response does not seem to be complete. It only prints "PW" instead of "PWR=00\r:"

When trying the "PWR?" command manually with the miniterm that comes with pyserial I do get the complete response.

python3 -m serial.tools.miniterm /dev/ttyUSB1 9600 --eol CR --echo
--- Miniterm on /dev/ttyUSB1  9600,8,N,1 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
PWR?
PWR=00
:
pszafer commented 6 years ago

@mvdwetering thank you for testing. I repaired this TIMEOUT problem. About is_closing, can you test this module on Python 3.7, as is_closing is new in version 3.7, check it out here: https://docs.python.org/3/library/asyncio-stream.html (StreamWriter part).

About last error AttributeError: 'int' object has no attribute 'decode', let's check it out first with Python 3.7 and then we will see.

mvdwetering commented 6 years ago

I installed Python 3.7 on the Pi (which surprisingly requires manual compiling of Python)

Now I get:

pi@automation-pi3:~/epson_projector $  python3.7 test_serial.py
Timeout error
Traceback (most recent call last):
  File "test_serial.py", line 25, in <module>
    loop.run_until_complete(main_serial(loop))
  File "/usr/local/lib/python3.7/asyncio/base_events.py", line 568, in run_until_complete
    return future.result()
  File "test_serial.py", line 11, in main_serial
    await run(loop)
  File "test_serial.py", line 18, in run
    data = await projector.get_property(POWER)
  File "/home/pi/epson_projector/epson_projector/main.py", line 56, in get_property
    self.__get_timeout(command))
  File "/home/pi/epson_projector/epson_projector/projector_serial.py", line 79, in get_property
    return response.split('=')[1]
IndexError: list index out of range
pszafer commented 6 years ago

We've got timeout error. I extended timeout to 10 seconds for tests if it get's connected.

Try to run it like it, to get more verbose logging:

python3.7 test_serial.py --log=DEBUG

BTW any idea if I can connect to EH-TW5350 by serial USB? Do you connect to port named 'Service'?

mvdwetering commented 6 years ago

My projector does not have USB, only a serial port (marked RS-232-C on the device). Judging from the connections on EH-TW5350 the Service USB port is the only one that would make sense (the other USB is the wrong type for connecting to a computer).

Output from the latest version:

pi@automation-pi3:~/epson_projector $ python3.7 test_serial.py --log=DEBUG
Timeout error during connection
Bad response P
False
W
pszafer commented 6 years ago

Ok, I made test code from example pySerial-asyncio. Test it out and let me know: https://gist.github.com/pszafer/af7ce3c48890fdbd786f291330651f97

mvdwetering commented 6 years ago

This is the output

pi@automation-pi3:~ $ python3.7 epson_test.py  --log=DEBUG
port opened SerialTransport(<_UnixSelectorEventLoop running=True closed=False debug=False>, <__main__.Output object at 0x76586330>, Serial<id=0x76586350, open=True>(port='/dev/ttyUSB1', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=0, xonxoff=False, rtscts=False, dsrdtr=False))
data received b'P'
port closed
pszafer commented 6 years ago

I have no idea what's wrong... I'm testing it with printer without problems... Try this update https://gist.github.com/pszafer/af7ce3c48890fdbd786f291330651f97

Can you give me ssh access to you rpi somehow? Contact me by mail if it is possible pszafer@gmail.com . Next week I could look into that.

pszafer commented 6 years ago

@mvdwetering Run this command on rpi before executing any scripts:

stty -F /dev/ttyUSB1 raw

or if with first still no luck, try:

stty -F /dev/ttyUSB1 9600 line 0 min 1 time 0
mvdwetering commented 6 years ago

After some more experimentation it turns out that the serial stuff seems to work OK, but that the commands that are sent need a '\r' instead of a '\n' to close them (so transport.write(b'PWR?\r')). When I make this change in the v2 test I get this output:

pi@automation-pi3:~ $ python3.7 epson_test_v2.py
port opened SerialTransport(<_UnixSelectorEventLoop running=True closed=False debug=False>, <__main__.Output object at 0x76583310>, Serial<id=0x7693bcd0, open=True>(port='/dev/ttyUSB1', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=0, xonxoff=False, rtscts=False, dsrdtr=False))
data received b'P'
data received b'WR'
data received b'='
data received b'0'
data received b'0\r'
data received b':'

which indicates the projector is at standby.

pszafer commented 6 years ago

So in other words file epson_test_v1.py is working for you right now?

mvdwetering commented 6 years ago

Ah, I now see. I did not test that one before since I thought that was the same as you had in the first test request, but it was changed and renamed. I only tested the _v2 the second time.

Anyway epson_test_v1.py is working.

pi@automation-pi3:~ $ python3.7 epson_test_v1.py
port opened SerialTransport(<_UnixSelectorEventLoop running=True closed=False debug=False>, <__main__.Output object at 0x7657a610>, Serial<id=0x7657a310, open=True>(port='/dev/ttyUSB1', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=0, xonxoff=False, rtscts=False, dsrdtr=False))
data received b'PW'
data received b'R'
data received b'='
data received b'00'
data received b'\r'
port closed
pszafer commented 6 years ago

Can you try test_serial.py with setting before:

stty -F /dev/ttyUSB1 raw

For now I don't know reason why my module is not working...

mvdwetering commented 6 years ago

I did some more debugging and got it mostly working. I attached a patchfile with the exact changes, but these are the main points

So in the end with the attached patch the projector status is printed and the projector powers on.

pi@automation-pi3:~/epson_projector $ python3.7 test_serial.py
2018-09-24 20:48:48,153 - asyncio - DEBUG - Using selector: EpollSelector
2018-09-24 20:48:48,185 - epson_projector.main - DEBUG - Getting property PWR
2018-09-24 20:48:48,189 - epson_projector.projector_serial - INFO - Just say open, the projector does not return anything on connecting.
2018-09-24 20:48:48,220 - epson_projector.projector_serial - INFO - Response from Epson PWR=00
00
2018-09-24 20:48:48,221 - epson_projector.main - DEBUG - Sending command to projector PWR ON
2018-09-24 20:48:58,230 - epson_projector.projector_serial - ERROR - Timeout error during sending request
False

BTW the PWR ON results in a timeout (but the projector was turned on) when it sends the request, but according to the documentation the "PWR ON" command can take up to 40, 70 or 100 seconds. So it seems that poweron command needs a custom timeout.

serial.patch.txt

pszafer commented 6 years ago

@mvdwetering , great work! thanks. Update testing branch and check it out.

mvdwetering commented 6 years ago

The test_serial.py now mostly works. For some reason I still get a timeout, however the projector did power on. I did a separate timing of manually sending the PWR ON command and that took about 35 seconds, so not sure why the timeout kicked in when I ran it from test_serial (is it possible to get timing on the actual serial commands sent for debugging?). I will do a few more timings when I have to turn on the projector.

pi@automation-pi3:~/epson_projector $ python3.7 test_serial.py --log=DEBUG
00
Timeout error during sending request
False

Note that just sending :\r (or \r) to test the connection will return ERR\r:, not just \r:, not sure if that can cause issues.

I did try PWR OFF (twice until now) and that powered off the projector just fine. It did not display the confirmation dialog or require any additional input. However the command seemed to take a bit more than 10 seconds until it showed the : (I counted 15 in my head, but that is not very precise, need to test more on this)

pszafer commented 5 years ago

Is somebody willing to check and repair testing branch? If not I'm closing this issue. I cannot connect to my projector via serial so cannot implement it properly.

henryk commented 5 years ago

Yes, sorry. I'm in the process of moving, so the projector and all the hi-fi equipment is still in boxes. But since I'm setting up home automation from scratch in the new place (previously was FHEM with my own Epson code), I'll get ample opportunity to test and fix the code.

mvdwetering commented 5 years ago

Sorry for not coming back in 2018, I probably got distracted by something else and forgot to come back :( But I had a fresh look at it and found that the expected response in send_request() in projector_serial.py is incorrect for empty/success responses.

I added some additional logging and found that on a successful command the response is b':', in an error situation it is b'ERR\r:'. This is printed with the following logging added @ line 105 _LOGGER.info("Response from Epson before %s", response) (so just before the line with response = response[:-2].decode())

Output for PWR ON command 2019-04-15 22:03:48,982 - epson_projector.projector_serial - INFO - Response from Epson before b':' Output for BLAH command (manually coded just to get an error for comparison) 2019-04-15 22:06:21,219 - epson_projector.projector_serial - INFO - Response from Epson before b'ERR\r:' Output for PWR? (for a "GET") 2019-04-15 22:22:49,323 - epson_projector.projector_serial - INFO - Response from Epson before b'PWR=01\r:'

So for commands that give back data in the response waiting for CR_COLON works fine, but for commands that do not (like PWR ON) that does not work, so we end up in the timeout (beause no CR).

I changed the read_until() into: response = await self._reader.readuntil(COLON.encode()) and then the Timeout exception is gone. FYI time for projector to turn on is ~31 seconds so timeout of 40 seems fine.

However 'data2' (the result of send_command(PWR_ON)) still prints False. I am not sure what the expected output of send_command() should be in the success case. There is no actual message to return since the projector only responds with ':'. I changed the check if not response: in send_command() to if response is None: and that gets rid of the False, but I am not sure if that is the intended behaviour.

Sorry for the long story, but I hope it still is clear enough to understand what I did. For completeness here are the modifications I made to projector_serial.py and the output of the test program.

# At the top of the file to get the debug output from the logger, not sure how to enable it otherwise
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
_LOGGER.addHandler(console_handler)
_LOGGER.setLevel(logging.DEBUG)

# Updated these functions
    async def send_command(self, command, timeout):
        """Send command to Epson."""
        response = await self.send_request(
            timeout=timeout,
#            command="BLAH"+CR)
            command=command+CR)
        if response is None:
            return False
        return response

    async def send_request(self, timeout, command):
        """Send request to Epson over serial."""
        if self._writer is None:
            await self.async_init()
        if self._writer and not self._writer.is_closing() and command:
            try:
                with async_timeout.timeout(timeout):
                    self._writer.write(command.encode())
                    response = await self._reader.readuntil(
                        COLON.encode())
                    _LOGGER.info("Response from Epson before %s", response)
                    response = response[:-2].decode()
                    _LOGGER.info("Response from Epson %s", response)
                    if response == ERROR:
                        _LOGGER.error("Error request")
                    else:
                        _LOGGER.info("Return response")
                        return response
            except asyncio.TimeoutError:
                _LOGGER.error("Timeout error during sending request")
        return False
pi@automation-pi3:~/epson_projector $ python3.7 test_serial.py --log=DEBUG
2019-04-15 22:47:34,408 - epson_projector.projector_serial - INFO - Connection established,                                       but wrong response.
2019-04-15 22:47:34,409 - epson_projector.projector_serial - INFO - Cannot open serial to Epson
2019-04-15 22:47:34,463 - epson_projector.projector_serial - INFO - Response from Epson before b'PWR=01\r:'
2019-04-15 22:47:34,463 - epson_projector.projector_serial - INFO - Response from Epson PWR=01
2019-04-15 22:47:34,464 - epson_projector.projector_serial - INFO - Return response
01
2019-04-15 22:47:34,505 - epson_projector.projector_serial - INFO - Response from Epson before b':'
2019-04-15 22:47:34,506 - epson_projector.projector_serial - INFO - Response from Epson
2019-04-15 22:47:34,506 - epson_projector.projector_serial - INFO - Return response

Note that the last line in the test_serial output is an empty line.

henryk commented 5 years ago

I've implemented the COLON change (and slightly improved the logging around that).

Side note for future users: In the case where the projector is not directly connected to the computer running this library (for example with home-assistant, note: the home-assistant epson plugin still doesn't support serial, needs a release of this change first) you can use ser2net on a linux box connected to the projector like this:

In ser2net conf:

2000:raw:0:/dev/ttyUSB0:9600 8DATABITS NONE 1STOPBIT

Then epson_projector connect string (instead of '/dev/ttyUSB0'): 'socket://ip.add.re.ss:2000'. (Note: https://pyserial.readthedocs.io/en/latest/url_handlers.html supports rfc2217 connection type, which equals telnet instead of raw in the ser2net configuration, but doesn't support timeouts and therefore doesn't work.)