diff --git a/.gitignore b/.gitignore index c3730f1..9c7d812 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **.pyc __pycache__/ **.egg-info/ - +data/ +.coverage \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a7855ad --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: python + +python: + - "3.4" + - "3.5" + - "3.6" + - "3.7-dev" + - "nightly" + +sudo: required + +install: + - make install-develop-requirements + - make install-tests-requirements + - make install-docs-requirements + - python setup.py develop + +script: + - lv2ls + - make test + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGES b/CHANGES index ef2a180..ffbe5f8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,10 @@ -Version 0.2.0 - released mm/dd/18 +Version 0.2.0 - released 03/dd/18 ================================= + - Initial release + - Change Android Controller -> WebServiceSerial: Now use WebService to reuse WebService methods - Defined Target: Initial target is Android, but is possible implements other targets for communication with other devices, as Arduino via USB Serial communication + - Try reconnect when connection is closed + - Check if is ADB has installed + - Try download the ADB if isn't installed diff --git a/README.rst b/README.rst index 3de075f..a8bbc04 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,23 @@ WebService Serial ================= +.. image:: https://travis-ci.org/PedalPi/WebServiceSerial.svg?branch=master + :target: https://travis-ci.org/PedalPi/WebServiceSerial + :alt: Build Status + +.. image:: https://codecov.io/gh/PedalPi/WebServiceSerial/branch/master/graph/badge.svg + :target: https://codecov.io/gh/PedalPi/WebServiceSerial + :alt: Code coverage + + WebService Serial disposes the `WebService`_ communication via TCP Serial communication. With it, is possible: -- Use `DisplayView`_, an Android application that provides pedalboard - data for live presentations. Your focus is a speed management in live - performances. - +- Use `DisplayView`_, an Android application for manages quickly the + current pedalboard. Ideal for adjusting live performances and band + rehearsal. **Documentation:** https://github.com/PedalPi/WebServiceSerial @@ -25,149 +33,165 @@ With it, is possible: .. _Apache License 2.0: https://github.com/PedalPi/WebServiceSerial/blob/master/LICENSE -~How to use~ FIXME ------------------- +Installation +------------ -Like described in `Application documentation`_, create a ``start.py`` -and register AndroidController component. +Install with pip: -.. code:: python +.. code-block:: bash - import sys - import tornado + pip install PedalPi-PluginsManager - # DEPRECATED - sys.path.append('application') - sys.path.append('android_controller') +Also is necessary install the `Android Debug Bridge (adb)`_ +for communication between the Pedal Pi and a Android device. In a Linux like, execute - from application.Application import Application - from android_controller.android_controller import AndroidController +.. code-block:: bash - address = 'localhost' - port = 3000 + sudo apt-get install android-tools-adb - application = Application(path_data="data/", address=address, test=True) - application.register(AndroidController(application, "adb")) +.. _Android Debug Bridge (adb): https://developer.android.com/studio/command-line/adb.html - application.start() +In embedded systems, the WebService Serial will try to download an adb pre-build +if the adb is not installed on the device. - tornado.ioloop.IOLoop.current().start() +Also, is possible compile the adb. See https://github.com/PedalPi/adb-arm -Dependencies -~~~~~~~~~~~~ +How to use +---------- -**WebService Serial** requires ``Tornado >= 4.2`` for TCP connection. +Like described in `Application documentation`_, create a ``start.py`` +and register WebService Serial component. Is necessary that **WebService Serial** +be registered after the **WebService** (WebService is dependency of WebService Serial +and it will be installed when WebService Serial is installed). -For communication with Android (over USB), also needs ``adb``. +.. code:: python -If you uses in a ARM architecture, maybe will be necessary compile -**adb**. In these cases, the project https://github.com/PedalPi/adb-arm -can help you. **adb-arm** PedalPi *fork* already contains some binaries -for RaspberryPi. + # Imports application + from application.application import Application -Protocol --------- + address = 'localhost' + application = Application(path_data="data/", address=address) -The communication are described here. For the possible command lists, -see the `WebService documentation`_. + # Register WebService before WebServiceSerial + from webservice.webservice import WebService + application.register(WebService(application)) -Request -~~~~~~~ + # Register WebServiceSerial after WebService + from webservice_serial.webservice_serial import WebServiceSerial + from webservice_serial.target.android.android_display_view import AndroidDisplayView -:: + target = AndroidDisplayView() + application.register(WebServiceSerial(application, target)) - \n - \n - EOF\n + # Start Application + application.start() -- ````: ``GET``, ``POST``, ``PUT``, ``DELETE``, ``SYSTEM`` -- ````: Json data. If none, send ``'{}'`` -- ````: http://pedalpi.github.io/WebService/ -- ``EOF``: The string “EOF”. + import tornado + try: + tornado.ioloop.IOLoop.current().start() + except KeyboardInterrupt: + application.stop() -Example: -:: +WebService Serial now has been configured to connect with a Android device that are +installed a app compatible with it. If haven't installed a app in your device, is recommended +the `Pedal Pi - Display View`_. With Display View, is possible manages the current pedalboard +quickly by a Android device connected with Pedal Pi by the USB. Read your recommendations for +details how to configure the device to enable the communication between the devices +over USB. - PUT /current/bank/1/pedalboard/3 - {} - EOF +.. _Pedal Pi - Display View: https://play.google.com/store/apps/details?id=io.github.com.pedalpi.displayview -Response -~~~~~~~~ +Protocol +-------- -:: +`WebService Serial` provides a way to communicate with ``WebService`` through a serial connection. - RESPONSE +`WebService Serial` provides a TCP client. For communication with a device, is necessary that +the device implements a socket TCP server listening the port ``8888``. -- ``RESPONSE``: String ``RESPONSES``; -- ````: Json data. If none, send ``'{}'`` +The communication are based in messages from device to the Pedal Pi (``Request`` messages) +and messages from Pedal Pi to the device (``Response`` and ``Event`` messages) -Notification -~~~~~~~~~~~~ +``Request`` Message +~~~~~~~~~~~~~~~~~~~ -This corresponds the websocket data notifications +With ``Request`` Message, a device can request data. The message format has the following format:: -:: + \n\nEOF\n - EVENT +:: -- ``EVENT``: String ``EVENT`` -- ````: Json data. If none, send ``'{}'`` + + + EOF + [empty line here] -Initialization -~~~~~~~~~~~~~~ +The communication are described here. For the possible command lists, +see the `WebService documentation`_. -After the connection has been realized, +- ````: ``int`` Unique id that defines the request. This value will be used in a response message, identifying the original request message; +- ````: ``string`` Possible values are: -1. Application send + + ``GET``, ``POST``, ``PUT``, ``DELETE`` Based in the `WebService documentation`_; + + ``SYSTEM`` Informs custom system messages. Actually this isn't used; -:: +- ````: Json data. If none, send an empty string; +- ````: Resource identifier. Is necessary to informs the API version too (``/v1/``). For the full list of resource, see http://pedalpi.github.io/WebService/ +- ``EOF``: The string “EOF”. - SYSTEM / - {"message": "connected"} - EOF +Example `Set the current pedalboard`_: :: -After initialization -~~~~~~~~~~~~~~~~~~~~ + 1 PUT /v1/current/bank/1/pedalboard/3 -The connected device can be request thinks, like: + EOF -- The current pedalboard number +.. _Set the current pedalboard: http://pedalpi.github.io/WebService/#current-management-manages-the-current-pedalboard-put -:: +``Response`` Message +~~~~~~~~~~~~~~~~~~~~ - GET /v1/current - {} - EOF +``Response`` messages contains a response of a request. For identify the +respective request, see the identifier. The message format has the following format:: -- Response + RESPONSE \n -:: +- ````: ``int`` A response returns the same Unique id that the respective request informs; +- ``RESPONSE``: ``string`` The string “RESPONSE”; +- ````: ``string`` Json encoded data. If none, it will be an empty string; - RESPONSE { "bank": 1, "pedalboard": 0 } +``Event`` Message +~~~~~~~~~~~~~~~~~ -- The pedalboard data +Changes that modify the Pedal Pi event can be applied by others Components. An example is +`Raspberry P0`_, that contains two buttons that when pressed changes the current pedalboard. +To maintain the application integrity, WebService Serial will send ``Event`` messages informing +the changes. -:: +This corresponds the WebService `websocket data notifications`_. - GET /v1/bank/1/pedalboard/0 - {} - EOF +.. _Raspberry P0: https://github.com/PedalPi/Raspberry-P0 +.. _websocket data notifications: http://pedalpi.github.io/WebService/#websocket -- Response +A ``Event`` message format is:: -:: + EVENT \n - RESPONSE { "name": "My pedalboard", "effects": [], "connections": [], "data": {} } +- ``EVENT``: ``string`` The string “EVENT”; +- ````: ``string`` Json encoded data. If none, it will be an empty string; .. _WebService: https://github.com/PedalPi/WebService .. _DisplayView: https://github.com/PedalPi/DisplayView .. _Application documentation: http://pedalpi-application.readthedocs.io/en/latest/ .. _WebService documentation: http://pedalpi.github.io/WebService/ -Scripts -======= +Development +=========== + +Install locally to develop:: + + python setup.py develop + +See makefile options:: -Install locally to develop -python setup.py develop + make help diff --git a/makefile b/makefile new file mode 100644 index 0000000..5a35814 --- /dev/null +++ b/makefile @@ -0,0 +1,94 @@ +BROWSER=firefox +BOLD=\033[1m +NORMAL=\033[0m + +default: help + +clean: clean-pyc clean-test clean-build clean-docs + +clean-build: + rm -rf .eggs + rm -rf build + rm -rf dist + +clean-pyc: + find . -name '*.pyc' -exec rm --force {} + + find . -name '*.pyo' -exec rm --force {} + + +clean-test: + rm -rf .cache + rm -f .coverage + rm -rf htmlcov + +clean-docs: + rm -rf docs/build + +docs: clean-docs + cd docs && $(MAKE) html + +docs-see: docs + $(BROWSER) docs/build/html/index.html + +install-develop-requirements: + sudo apt-get install -y portaudio19-dev python-all-dev --no-install-recommends + sudo apt-get install -y lilv-utils calf-plugins guitarix --no-install-recommends + sudo apt-get install -y lv2-dev --no-install-recommends + pip3 install -U setuptools + pip3 install cffi + sudo apt-get install -y android-tools-adb --no-install-recommends + +install-docs-requirements: + pip3 install sphinx + pip install sphinx_rtd_theme + +install-tests-requirements: + pip3 install pytest pytest-cov + # For midi tests - https://github.com/x42/midifilter.lv2 + cd /tmp && git clone git://github.com/x42/midifilter.lv2.git && \ + cd midifilter.lv2 && \ + make && \ + sudo make install PREFIX=/usr + +run: + @echo "Run option isn't created =)" + +test: clean-test + pytest --cov=webservice_serial + +test-docs: + python -m doctest *.rst -v + python -m doctest docs/*/*.rst -v + +test-details: test + coverage3 html + $(BROWSER) htmlcov/index.html + +help: cabecalho + @echo "" + @echo "Commands" + @echo " $(BOLD)clean$(NORMAL)" + @echo " Clean files" + @echo " $(BOLD)docs$(NORMAL)" + @echo " Make the docs" + @echo " $(BOLD)docs-see$(NORMAL)" + @echo " Make the docs and open it in BROWSER" + @echo " $(BOLD)install-develop-requirements$(NORMAL)" + @echo " Install the develop requirements" + @echo " $(BOLD)install-docs-requirements$(NORMAL)" + @echo " Install the docs requirements" + @echo " $(BOLD)install-tests-requirements$(NORMAL)" + @echo " Install the tests requirements" + @echo " $(BOLD)test$(NORMAL)" + @echo " Execute the tests" + @echo " $(BOLD)test-details$(NORMAL)" + @echo " Execute the tests and shows the result in BROWSER" + @echo " - BROWSER=firefox" + @echo " $(BOLD)help$(NORMAL)" + @echo " Show the valid commands" + +cabecalho: + @echo "$(BOLD)===================" + @echo "> WebService Serial" + @echo "===================" + @echo "" + @echo "Github$(NORMAL): https://pypi.org/project/PedalPi-WebServiceSerial/" diff --git a/setup.py b/setup.py index 073b524..4746533 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright 2017 SrMouraSilva +# Copyright 2018 SrMouraSilva # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ def readme(): packages=[ 'webservice_serial', 'webservice_serial/protocol', + 'webservice_serial/protocol/keyboard', 'webservice_serial/target', 'webservice_serial/target/android', ], @@ -48,7 +49,7 @@ def readme(): package_data={}, install_requires=[ - 'PedalPi-WebService>=0.3.0', + 'PedalPi-WebService==0.3.0', ], classifiers=[ diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/example_test.py b/test/example_test.py new file mode 100644 index 0000000..7225477 --- /dev/null +++ b/test/example_test.py @@ -0,0 +1,59 @@ +# Copyright 2017 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + + +#class ControllerTest(unittest.TestCase): +# +# def test_all(self): +# test() + + +def test(): + # Imports application + from application.application import Application + + address = 'localhost' + application = Application(path_data="data/", address=address, test=True) + + # Register WebService before WebServiceSerial + from webservice.webservice import WebService + + application.register(WebService(application, port=3000)) + + # Register WebServiceSerial after WebService + from webservice_serial.webservice_serial import WebServiceSerial + from webservice_serial.target.android.android_display_view import AndroidDisplayView + + target = AndroidDisplayView() + + application.register(WebServiceSerial(application, target)) + + # Start Application + application.start() + + #import tornado + + #try: + # tornado.ioloop.IOLoop.current().start() + #except KeyboardInterrupt: + # application.stop() + + import time + time.sleep(5) + + +if __name__ == '__main__': + test() diff --git a/webservice_serial/__init__.py b/webservice_serial/__init__.py index e69de29..98a3ea2 100644 --- a/webservice_serial/__init__.py +++ b/webservice_serial/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/webservice_serial/protocol/__init__.py b/webservice_serial/protocol/__init__.py index e69de29..98a3ea2 100644 --- a/webservice_serial/protocol/__init__.py +++ b/webservice_serial/protocol/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/webservice_serial/protocol/keyboard/__init__.py b/webservice_serial/protocol/keyboard/__init__.py new file mode 100644 index 0000000..98a3ea2 --- /dev/null +++ b/webservice_serial/protocol/keyboard/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/webservice_serial/protocol/keyboard/keyboard.py b/webservice_serial/protocol/keyboard/keyboard.py new file mode 100644 index 0000000..b42a690 --- /dev/null +++ b/webservice_serial/protocol/keyboard/keyboard.py @@ -0,0 +1,52 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum + + +class KeyCode(Enum): + DOWN = "DOWN" + UP = "EVENT" + + +class KeyNumber(Enum): + DPAD_UP = 0x00000013 + DPAD_DOWN = 0x00000014 + DPAD_LEFT = 0x00000015 + DPAD_RIGHT = 0x00000016 + + DPAD_CENTER = 0x00000017 + + PLUS = 0x00000051 + MINUS = 0x00000045 + + +class KeyEvent(object): + """ + :param KeyCode code: + :param KeyNumber number: + """ + + def __init__(self, code, number): + self.code = code + self.number = number + + def __dict__(self): + return { + 'code': self.code.value, + 'number': self.number.value + } + + def __str__(self): + return str(self.__dict__()) diff --git a/webservice_serial/protocol/message_builder.py b/webservice_serial/protocol/message_builder.py index 85e3820..abd5d83 100644 --- a/webservice_serial/protocol/message_builder.py +++ b/webservice_serial/protocol/message_builder.py @@ -1,3 +1,17 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import json from webservice_serial.protocol.request_message import RequestMessage @@ -19,12 +33,12 @@ def generate(message): buffer = MessageBuilder.clean_buffer() - verb, path = buffer[0].split(" ") + identifier, verb, path = buffer[0].split(" ") data = buffer[1] verb = MessageBuilder.discover_verb(verb) - return MessageBuilder.generate_request_message(verb, path, data) + return RequestMessage(identifier, verb, path, data) @staticmethod def clean_buffer(): @@ -39,13 +53,3 @@ def discover_verb(word): return verb return RequestVerb.SYSTEM - - @staticmethod - def generate_request_message(verb, path, data): - """ - :param RequestVerb verb: Verb - :param string path: Path - :param string data: Data - :return RequestMessage: message generated - """ - return RequestMessage(verb, path, json.loads(data)) diff --git a/webservice_serial/protocol/request_message.py b/webservice_serial/protocol/request_message.py index 534ff4d..28a0193 100644 --- a/webservice_serial/protocol/request_message.py +++ b/webservice_serial/protocol/request_message.py @@ -1,19 +1,37 @@ -from webservice_serial.protocol.request_verb import RequestVerb +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. class RequestMessage(object): """ Message send form the app to the AndroidController + :param int identifier: Sequence number :param RequestVerb verb: :param string path: - :param dict content: + :param string content: """ - def __init__(self, verb, path, content): + def __init__(self, identifier, verb, path, content): + self.identifier = identifier self.verb = verb self.path = path self.content = content + @property + def content_formatted(self): + return self.content if self.content != "" else None + def __str__(self): - return '{} {}\n{}\nEOF'.format(self.verb, self.path, self.content) + return '{} {} {}\n{}\nEOF'.format(self.identifier, self.verb, self.path, self.content) diff --git a/webservice_serial/protocol/request_verb.py b/webservice_serial/protocol/request_verb.py index 6728006..5d19561 100644 --- a/webservice_serial/protocol/request_verb.py +++ b/webservice_serial/protocol/request_verb.py @@ -1,3 +1,17 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from enum import Enum diff --git a/webservice_serial/protocol/response_message.py b/webservice_serial/protocol/response_message.py index e64702f..9263c5c 100644 --- a/webservice_serial/protocol/response_message.py +++ b/webservice_serial/protocol/response_message.py @@ -1,15 +1,36 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import json +from webservice_serial.protocol.response_verb import ResponseVerb class ResponseMessage(object): + """ + :param ResponseVerb verb: + :param object content: + :param int identifier: + """ + + @staticmethod + def error(message, identifier=0): + return ResponseMessage(ResponseVerb.ERROR, '{"message": "'+message+'"}', identifier=identifier) - def __init__(self, verb, content=None): - """ - :param ResponseVerb verb: - :param string content: - """ + def __init__(self, verb, content=None, identifier=0): + self.identifier = identifier self.verb = verb self.content = json.dumps({}) if content is None else content def __str__(self): - return "{} {}\n".format(self.verb, self.content) + return "{} {} {}\n".format(self.identifier, self.verb, str(self.content)) diff --git a/webservice_serial/protocol/response_verb.py b/webservice_serial/protocol/response_verb.py index 203f828..5769547 100644 --- a/webservice_serial/protocol/response_verb.py +++ b/webservice_serial/protocol/response_verb.py @@ -1,9 +1,26 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from enum import Enum class ResponseVerb(Enum): RESPONSE = "RESPONSE" EVENT = "EVENT" + KEYBOARD_EVENT = "KEYBOARD_EVENT" + + ERROR = "ERROR" def __str__(self): return self.value diff --git a/webservice_serial/request_message_processor.py b/webservice_serial/request_message_processor.py index 05a2bd9..52fcdea 100644 --- a/webservice_serial/request_message_processor.py +++ b/webservice_serial/request_message_processor.py @@ -1,4 +1,18 @@ -from tornado.httpclient import HTTPRequest, AsyncHTTPClient, HTTPError +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tornado.httpclient import HTTPRequest, AsyncHTTPClient from webservice_serial.protocol.request_verb import RequestVerb from webservice_serial.protocol.response_message import ResponseMessage @@ -10,10 +24,11 @@ class RequestMessageProcessor(object): :param port: Port that WebService are executing """ - def __init__(self, port): + def __init__(self, port, token=None): self.http_client = AsyncHTTPClient() self.url = 'http://localhost:{}'.format(port) self.processed_listener = lambda message, response: ... + self.token = token def process(self, message): """ @@ -22,34 +37,41 @@ def process(self, message): if message.verb is RequestVerb.SYSTEM: return - request = HTTPRequest(self.url + message.path, method=message.verb.value) + request = HTTPRequest(self.url + message.path, method=message.verb.value, headers=self.headers, + body=message.content_formatted) self.http_client.fetch( request, - lambda response: self.response(message, response=response) + lambda http_response: self.response(message, http_response) ) - def response(self, message, response): + @property + def headers(self): + if self.token is not None: + return {'x-xsrf-token': self.token} + else: + return None + + def response(self, request, http_response): """ - :param RequestMessage message: Request message - :param HTTPResponse response: Response message + :param RequestMessage request: Request message + :param HTTPResponse http_response: WebService response message :return: """ - try: - response_message = ResponseMessage(ResponseVerb.RESPONSE, response.body.decode('utf8')) - - except HTTPError as e: - # HTTPError is raised for non-200 responses; the response - # can be found in e.response. - print("Error: " + str(e)) - #FIXME - return + body = http_response.body.decode('utf8') if http_response.body is not None else None - except Exception as e: - # Other errors are possible, such as IOError. - print("Error: " + str(e)) - return + if http_response.code == 405: + response = ResponseMessage.error(body, request.identifier) + else: + response = ResponseMessage(ResponseVerb.RESPONSE, body, identifier=request.identifier) - self.processed_listener(message, response_message) + self.processed_listener(request, response) def close(self): self.http_client.close() + + def process_event(self, message): + """ + :param dict message: + :return ResponseMessage: + """ + return ResponseMessage(ResponseVerb.EVENT, message) diff --git a/webservice_serial/target/__init__.py b/webservice_serial/target/__init__.py index e69de29..98a3ea2 100644 --- a/webservice_serial/target/__init__.py +++ b/webservice_serial/target/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/webservice_serial/target/android/__init__.py b/webservice_serial/target/android/__init__.py index e69de29..98a3ea2 100644 --- a/webservice_serial/target/android/__init__.py +++ b/webservice_serial/target/android/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/webservice_serial/target/android/adb.py b/webservice_serial/target/android/adb.py index 6065d2b..c1c8a2f 100644 --- a/webservice_serial/target/android/adb.py +++ b/webservice_serial/target/android/adb.py @@ -1,4 +1,19 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os +import subprocess class Adb(object): @@ -14,8 +29,7 @@ def __init__(self, command="adb", log=None): self.log = log def start(self, port, activity): - self.execute('shell am start -n {}'.format(activity)) - #FIXME delay? + #self.execute('shell am start -n {}'.format(activity)) #self.execute('forward --remove-all') self.execute('forward tcp:{} tcp:{}'.format(port, port)) @@ -28,3 +42,16 @@ def execute(self, command): def close(self, port): self.execute('forward --remove tcp:{}'.format(port)) + + @staticmethod + def has_installed(): + """ + Check if the current system have the ``adb`` installed + :return: + """ + try: + subprocess.call(["adb"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except FileNotFoundError: + return False + + return True diff --git a/webservice_serial/target/android/android_display_view.py b/webservice_serial/target/android/android_display_view.py index d0be418..b265efb 100644 --- a/webservice_serial/target/android/android_display_view.py +++ b/webservice_serial/target/android/android_display_view.py @@ -1,24 +1,67 @@ -from webservice_serial.target.target import Target +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import subprocess + from webservice_serial.target.android.adb import Adb +from webservice_serial.target.target import Target class AndroidDisplayView(Target): - """ - :param string adb_command: Command that call the Android Debug Bridge - In Raspberry maybe be a `./adb` executable file - """ - activity = 'io.github.pedalpi.pedalpi_display/io.github.pedalpi.pedalpi_display.MainActivity' + activity = 'io.github.pedalpi.pedalpi_display/io.github.pedalpi.displayview.activity.ResumeActivity' - def __init__(self, adb_command="adb"): + def __init__(self): super(AndroidDisplayView, self).__init__() self.adb = None - self.adb_command = adb_command def init(self, application, port): super(AndroidDisplayView, self).init(application, port) - self.adb = Adb(self.adb_command, application.log) + adb_command = self._discover_adb_command() + self.application.log('AndroidDisplayView - Android Debug Bridge command "{}"', adb_command) + + self.adb = Adb(adb_command, application.log) self.adb.start(port, AndroidDisplayView.activity) def close(self): self.adb.close(self.port) + + def _discover_adb_command(self): + if Adb.has_installed(): + return "adb" + + path = self.application.path_data / "adb" + + if not path.is_file(): + self.application.log("AndroidDisplayView - Downloading adb pre-compiled") + self._download_adb(path) + + return path + + def _download_adb(self, path): + if self._version() == 'Raspberry 3': + command = "wget -O {} https://github.com/PedalPi/adb-arm/raw/master/adb-rpi3".format(path) + else: + command = "wget -O {} https://github.com/PedalPi/adb-arm/raw/master/adb-arm-binary".format(path) + + subprocess.call(command.split()) + subprocess.call("chmod +x {}".format(path).split()) + + def _version(self): + command = 'cat /sys/firmware/devicetree/base/model' + + if 'Raspberry Pi 3' in subprocess.check_output(command).decode('UTF-8').split('\n')[0]: + return 'Raspberry Pi 3' + + return "" diff --git a/webservice_serial/target/target.py b/webservice_serial/target/target.py index b706ba3..44cd1e2 100644 --- a/webservice_serial/target/target.py +++ b/webservice_serial/target/target.py @@ -1,8 +1,27 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + class Target(object): def __init__(self): self.application = None self.port = None + @property + def name(self): + return self.__class__.__name__ + def init(self, application, port): """ Target initialization diff --git a/webservice_serial/webservice_serial.py b/webservice_serial/webservice_serial.py index ab6c79e..4562c5c 100644 --- a/webservice_serial/webservice_serial.py +++ b/webservice_serial/webservice_serial.py @@ -1,7 +1,25 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from application.component.component import Component +from webservice_serial.protocol.response_verb import ResponseVerb -from webservice_serial.webservice_serial_client import WebServiceSerialClient from webservice_serial.request_message_processor import RequestMessageProcessor +from webservice_serial.webservice_serial_client import WebServiceSerialClient +from webservice_serial.websocket_client import WebSocketClient + +from time import sleep class WebServiceSerial(Component): @@ -18,30 +36,60 @@ def __init__(self, application, target, ws_port=3000): self.target = target self._client = WebServiceSerialClient('localhost', WebServiceSerial.port) self.request_message_processor = RequestMessageProcessor(ws_port) + self._websocket_client = WebSocketClient(ws_port) def init(self): self._client.connected_listener = self._on_connected self._client.message_listener = self._process_message + self._client.disconnected_listener = lambda: self._try_connect(5) self.request_message_processor.processed_listener = self._on_processed + self._websocket_client.token_defined_listener = self._on_token_defined + self._websocket_client.message_listener = self._on_event + + self._websocket_client.connect() + + self._try_connect() + + def _try_connect(self, delay=0): + self._log('Trying to connect with {}', self.target.name) self.target.init(self.application, WebServiceSerial.port) + sleep(delay) self._client.connect() + def _on_token_defined(self, token): + self.request_message_processor.token = token + def close(self): self.request_message_processor.close() self.target.close() + self._websocket_client.close() + self._client.close() def _on_connected(self): - self.application.log('AndroidController - DisplayView connected') + self._log('{} connected', self.target.name) def _process_message(self, message): - self.application.log('AndroidController - Message received: {}', message) + """ + :param RequestMessage message: + """ + self._log('Message received: {}', message) self.request_message_processor.process(message) def _on_processed(self, request_message, response_message): - self.application.log('AndroidController - Message sent: {}', response_message) - response_message = self.target.process(request_message, response_message) + + self._log('Message sent: {}', response_message) self._client.send(response_message) + + def _on_event(self, message): + response_message = self.request_message_processor.process_event(message) + response_message = self.target.process(None, response_message) + + self._log('Message sent: {}', response_message) + self._client.send(response_message) + + def _log(self, message, *args, **kwargs): + self.application.log('{} - {}'.format(self.__class__.__name__, message), *args, **kwargs) diff --git a/webservice_serial/webservice_serial_client.py b/webservice_serial/webservice_serial_client.py index d61aabf..0af223b 100644 --- a/webservice_serial/webservice_serial_client.py +++ b/webservice_serial/webservice_serial_client.py @@ -1,5 +1,21 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from tornado import gen +from tornado.ioloop import IOLoop from tornado.tcpclient import TCPClient +from tornado.iostream import StreamClosedError from webservice_serial.protocol.message_builder import MessageBuilder @@ -15,19 +31,58 @@ def __init__(self, address, port, encoding="utf-8"): self.message_listener = lambda message: ... self.connected_listener = lambda: ... - @gen.coroutine + self.disconnected_listener = lambda: print('Disconnected :(') + def connect(self): - self.stream = yield TCPClient().connect(self.address, self.port) + IOLoop.current().spawn_callback(lambda: self._connect()) + + @gen.coroutine + def _connect(self): + self.stream = yield self._try_connect() + if self.stream is None: + return + self.connected_listener() + yield self._start_read_data() + + @gen.coroutine + def _try_connect(self): + try: + stream = yield TCPClient().connect(self.address, self.port) + return stream + except StreamClosedError as e: + self.disconnected_listener() + return None + @gen.coroutine + def _start_read_data(self): while True: - data = yield self.stream.read_until('\n'.encode(self.encoding)) + data = yield self._read_data() + if data is None: + break data = data.decode(self.encoding).strip() generated = MessageBuilder.generate(data) if generated is not None: self.message_listener(generated) + @gen.coroutine + def _read_data(self): + try: + data = yield self.stream.read_until('\n'.encode(self.encoding)) + except StreamClosedError as e: + self.disconnected_listener() + return None + + return data + def send(self, message): - text = str(message).encode(self.encoding) - self.stream.write(text) + try: + text = str(message).encode(self.encoding) + self.stream.write(text) + except StreamClosedError as e: + self.disconnected_listener() + + def close(self): + if self.stream is not None and not self.stream.closed(): + self.stream.close() diff --git a/webservice_serial/websocket_client.py b/webservice_serial/websocket_client.py new file mode 100644 index 0000000..bc3a7ca --- /dev/null +++ b/webservice_serial/websocket_client.py @@ -0,0 +1,62 @@ +# Copyright 2018 SrMouraSilva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from tornado import gen +from tornado.ioloop import IOLoop +from tornado.websocket import websocket_connect + + +class WebSocketClient(object): + """ + :param int port: WebService port + """ + + def __init__(self, port): + self.port = port + self.connection = None + self.token_defined_listener = lambda token: ... + self.message_listener = lambda message: ... + + @property + def url(self): + return 'ws://localhost:{}/ws/'.format(self.port) + + @gen.coroutine + def connect(self): + IOLoop.current().spawn_callback(lambda: self._connect()) + + @gen.coroutine + def _connect(self): + self.connection = yield websocket_connect(self.url) + self._await_messages(self.connection) + + @gen.coroutine + def _await_messages(self, connection): + while True: + msg = yield connection.read_message() + if msg is None: + break + + message = json.loads(msg) + + if message['type'] == 'TOKEN': + self.token_defined_listener(message['value']) + else: + self.message_listener(message) + + def close(self): + if self.connection is not None: + self.connection.close()