Привет, Хабр!
Сегодня расскажу о том, как управлять компьютером с мобильного устройства. Нет, это не очередной аналог radmin'a, и не пример того, как можно поиздеваться над компьютером друга. Речь пойдет об удаленном управлении демоном, а точнее — о создании интерфейса для управления демоном, написанном на Python.
Архитектура довольно простая:
Заинтересовавшимся — добро пожаловать под кат.
Перед тем, как приступать к реализации, предлагаю «подготовить» каталог проекта. Нужно:
Начнем с серверной части — Django-приложения. Создадим новое приложение и добавим superuser'а:
Рекомендую сразу же его добавить в используемые Django-проектом приложения (web\settings.py или вместо «web» — имя вашего Djnago-проекта):
Создадим БД и superuser'а:
Настройки завершены, приступаем к реализации приложения.
Модель в архитектуре одна — это Команда, на которую должен отреагировать демон. Поля модели:
Подробнее о командах и статусах — см. ниже.
Немного «проапгрейдим» модель:
1. Расширим стандартный менеджер. Добавим методы для получения команд в состоянии «Создана» и в состоянии «В обработке».
И добавим его в модель:
2. Добавим методы проверки состояния и методы установки состояния команды:
Примечание: Конечно, можно обойтись и без этих методов. В таком случае в коде, работающим с Django ORM, потребуется использовать константы и описывать логику (хоть двухстрочную, но все же) обновления команды, что, имхо, не совсем удобно. Намного удобнее дергать необходимые методы. Но если такой подход противоречит концепции — с удовольствием выслушаю аргументы в комментариях.
Примечание: Здесь и далее нам понадобится приложение «django-ipware» для определения IP клиента, установим:
Здесь все проходит нативно: регистрируем модель в админ-панели, описываем отображаемые столбцы в таблице и поля на форме. Единственный нюанс — для сохранения IP клиента в объекте необходимо переопределить метод сохранения:
Не забываем применить изменения в моделях к базе данных:
Приступим к реализации логики обработки команд.
Как было написано выше, в нашем распоряжении 4 команды:
При создании, команде присваивается состояние «Создана» (спасибо, Кэп!). В процессе обработки команда может быть «Выполнена» (если состояние системы удовлетворяет всем необходимым условиям) или «Отклонена» (в противном случае). Состояние «В обработке» применимо для «долгоиграющих» команд — на их выполнение может потребоваться продолжительный период времени. К примеру, получив команду «Приостановить» код всего лишь меняет значение флага, а команда «Перезапуск» инициирует выполнение более комплексной логики.
Логика обработки команд следующая:
«Точкой входа» в классе является метод .check_commands() — в нем реализована описанная выше логика. Этот же метод будем вызывать в основном цикле демона. В случае получения команды «Приостановить», в методе создается цикл, условием выхода из которого является получение команды «Возобновить» — таким образом достигается желаемый эффект паузы в работе демона.
Модуль, в котором опишем реализацию IRemoteControl, предлагаю разместить в каталоге приложения. Так мы получим удобно транспортируемое Django-app.
Если модель сферического демона в вакууме можно представить в таком виде:
то внедрение интерфейса пульта управления происходит безболезненно:
В результате призванная нечисть управляется, но только с админ-панели.
Поместим данный код в файл, к примеру, daemon.py и пойдем дальше — напишем мобильный клиент.
Но для начала неплохо было бы реализовать интерфейс для общения мобильного клиента и серверной части. Приступим.
Установим Django REST framework:
Начнем с описания набора возвращаемых данных интерфейсом REST. Здесь нам пригодятся те загадочные методы из описания модели (.status_dsp() и .code_dsp()), которые возвращают текстовое название состояния и кода команды соответственно:
Методы REST API в архитектуре Django-приложения — это те же представления, только… вы поняли.
Для общения с клиентом достаточно трехбукв слов API-методов (эхх, идеальный мир...):
Для минимизации кода используем плюшки, поставляемые в комплекте с Django REST framework:
Опишем end-point'ы реализованных API-методов.
И подключим их к проекту (web\urls.py):
Интерфейс для общения реализован. Переходим к самому вкусному.
Для общения с сервером используем UrlRequest (kivy.network.urlrequest.UrlRequest). Из всех его достоинств нам понадобятся следующие:
Для простоты реализации будем использовать схему аутентификации Basic. При желании, можно одну из следующих статей посвятить другим способам аутентификации на web-ресурсах с помощью UrlRequest — пишите в комментариях.
Надеюсь, комментариев в коде достаточно для понимания. Если все же недостаточно — сообщайте, буду вносить правки.
На этом баловство с кодом завершается и на сцену выходит
О Buildozer'е можно говорить долго, потому что о нем сказано мало. Есть и статьи на хабре (об установке и настройке и о сборке релиз-версии и публикации на Google Play), конечно же, есть и документация… Но есть и нюансы,о которых можно написать целую статью которые разбросаны по разным источникам. Постараюсь собрать основные моменты здесь.
Ну и немного кода в помощь счастливым обладателям Debian и Ubuntu (остальным потребуется «тщательно обработать напильником»)
Теперь, когда Buildozer установлен, инициализируем его:
В результате работы этой команды в каталоге создастся файл конфигурации сборки (buildozer.spec). В нем находим указанные ниже ключи и присваиваем им соответствующие значения:
Активируем wunderwaffe:
и на выходе имеем .apk, который можно установить на Android-девайс.
Готово. С чем я вас и поздравляю!
И давайте посмотрим, как все это работает. Не зря же так долго старались :)
Запускаем Django-сервер, параметром указываем IP вашей машины в локальной сети:
Призываем нечисть:
Стартуем приложение на Android-девайсе и видим нечто подобное:
Примечание: Для записи видео использовалась финальная версия проекта, которую можно найти на github. От кода, приведенного в статье, отличается расширением функционала. В серверную часть добавлена поддержка пользовательских команд и отладочные сообщения (для наглядности), а в клиент добавлены: форма авторизации, запрос на подтверждение выполнения команды и некоторые удобства в интерфейс.
Что мы получили в результате?
Может это слишком громко сказано, но… Теперь меня мучает вопрос — а можно ли реализовать аналогичную архитектуру, используя другие языки и технологии (кроме Python), приложив при этом (хотя бы) не больше усилий и написав не больше кода?
На этом все.
Всем приятного кодинга и удачных сборок.
«RemoteControlInterface» на github
Доки по Django
Доки по Django REST framework
Доки по Kivy
Установка Kivy
Установка Buildozer
Сегодня расскажу о том, как управлять компьютером с мобильного устройства. Нет, это не очередной аналог radmin'a, и не пример того, как можно поиздеваться над компьютером друга. Речь пойдет об удаленном управлении демоном, а точнее — о создании интерфейса для управления демоном, написанном на Python.
Архитектура довольно простая:
- «Remote control App» — Kivy-приложение, реализующее клиентскую часть для мобильных устройств.
- «Remote control» — Django-приложение, реализующее REST API и взаимодействие с БД;
- IRemoteControl — Класс, реализующий логику обработки поступивших команд (будет использован в демоне);
Заинтересовавшимся — добро пожаловать под кат.
Перед тем, как приступать к реализации, предлагаю «подготовить» каталог проекта. Нужно:
- создать отдельный Python virtual environment
virtualenv .env - создать новый Django-проект (например — web)
Все операции с Django будем выполнять относительно этого каталога;django-admin startproject web - создать каталог для Android-приложения (например — ui_app). Все операции касательно мобильного приложения будем выполнять относительно этого каталога.
«Remote control»
Начнем с серверной части — Django-приложения. Создадим новое приложение и добавим superuser'а:
python manage.py startapp remotecontrol
Рекомендую сразу же его добавить в используемые Django-проектом приложения (web\settings.py или вместо «web» — имя вашего Djnago-проекта):
INSTALLED_APPS = [ ....... 'remotecontrol', ]
Создадим БД и superuser'а:
python manage.py migrate python manage.py createsuperuser
Настройки завершены, приступаем к реализации приложения.
Модели (remotecontrol\models.py)
Модель в архитектуре одна — это Команда, на которую должен отреагировать демон. Поля модели:
- Код команды — будем использовать 4 команды: «Приостановить», «Возобновить», «Перезапуск», «Отключить пульт управления»
- Состояние команды — возможны 4 состояния: «Создана», «В обработке», «Выполнена», «Отклонена».
- IP
- Дата создания объекта
Подробнее о командах и статусах — см. ниже.
Опишем модель:
# -*- coding: utf-8 -*- from django.db import models # Константы команд CODE_PAUSE = 1 # код команды "Приостановить" CODE_RESUME = 2 # код команды "Возобновить" CODE_RESTART = 3 # код команды "Перезапуск" CODE_REMOTE_OFF = 4 # код команды "Отключить пульт управления" COMMANDS = ( (CODE_RESTART, 'Restart'), (CODE_PAUSE, 'Pause'), (CODE_RESUME, 'Resume'), (CODE_REMOTE_OFF, 'Disable remote control'), ) class Command(models.Model): # Константы состояний STATUS_CREATE = 1 # код статуса "Создана" STATUS_PROCESS = 2 # код статуса "В обработке" STATUS_DONE = 3 # код статуса "Выполнена" STATUS_DECLINE = 4 # код статуса "Отклонена" STATUS_CHOICES = ( (STATUS_CREATE, 'Created'), (STATUS_PROCESS, 'In progress...'), (STATUS_DONE, 'DONE'), (STATUS_DECLINE, 'Declined'), ) # Поля модели created = models.DateTimeField(auto_now_add=True) ip = models.GenericIPAddressField() code = models.IntegerField(choices=COMMANDS) status = models.IntegerField(choices=STATUS_CHOICES, default=STATUS_CREATE)
Немного «проапгрейдим» модель:
1. Расширим стандартный менеджер. Добавим методы для получения команд в состоянии «Создана» и в состоянии «В обработке».
Опишем свой менеджер:
class CommandManager(models.Manager): # Команды в состоянии "Создана", сортированы по дате создания в порядке возрастания def created(self): return super(CommandManager, self).get_queryset().filter( status=Command.STATUS_CREATE).order_by('created') # Команды в состоянии "В обработке", сортированы по дате создания в порядке возрастания def processing(self): return super(CommandManager, self).get_queryset().filter( status=Command.STATUS_PROCESS).order_by('created')
И добавим его в модель:
class Command(models.Model): ....... objects = CommandManager()
2. Добавим методы проверки состояния и методы установки состояния команды:
Доп. методы:
class Command(models.Model): ....... # Методы проверки состояния def is_created(self): return self.status == self.STATUS_CREATE def is_processing(self): return self.status == self.STATUS_PROCESS def is_done(self): return self.status == self.STATUS_DONE def is_declined(self): return self.status == self.STATUS_DECLINE # Методы установки состояния def __update_command(self, status): self.status = status self.save() def set_process(self): self.__update_command(Command.STATUS_PROCESS) def set_done(self): self.__update_command(Command.STATUS_DONE) def set_decline(self): self.__update_command(Command.STATUS_DECLINE)
Примечание: Конечно, можно обойтись и без этих методов. В таком случае в коде, работающим с Django ORM, потребуется использовать константы и описывать логику (хоть двухстрочную, но все же) обновления команды, что, имхо, не совсем удобно. Намного удобнее дергать необходимые методы. Но если такой подход противоречит концепции — с удовольствием выслушаю аргументы в комментариях.
Полный листинг models.py:
# -*- coding: utf-8 -*- from django.db import models # Константы команд CODE_PAUSE = 1 # код команды "Приостановить" CODE_RESUME = 2 # код команды "Возобновить" CODE_RESTART = 3 # код команды "Перезапуск" CODE_REMOTE_OFF = 4 # код команды "Отключить пульт управления" COMMANDS = ( (CODE_RESTART, 'Restart'), (CODE_PAUSE, 'Pause'), (CODE_RESUME, 'Resume'), (CODE_REMOTE_OFF, 'Disable remote control'), ) class CommandManager(models.Manager): # Команды в состоянии "Создана", сортированы по дате создания в порядке возрастания def created(self): return super(CommandManager, self).get_queryset().filter( status=Command.STATUS_CREATE).order_by('created') # Команды в состоянии "В обработке", сортированы по дате создания в порядке возрастания def processing(self): return super(CommandManager, self).get_queryset().filter( status=Command.STATUS_PROCESS).order_by('created') class Command(models.Model): # Константы состояний STATUS_CREATE = 1 # код статуса "Создана" STATUS_PROCESS = 2 # код статуса "В обработке" STATUS_DONE = 3 # код статуса "Выполнена" STATUS_DECLINE = 4 # код статуса "Отклонена" STATUS_CHOICES = ( (STATUS_CREATE, 'Created'), (STATUS_PROCESS, 'In progress...'), (STATUS_DONE, 'DONE'), (STATUS_DECLINE, 'Declined'), ) # Поля модели created = models.DateTimeField(auto_now_add=True) ip = models.GenericIPAddressField() code = models.IntegerField(choices=COMMANDS) status = models.IntegerField(choices=STATUS_CHOICES, default=STATUS_CREATE) objects = CommandManager() # Методы проверки состояния def is_created(self): return self.status == self.STATUS_CREATE def is_processing(self): return self.status == self.STATUS_PROCESS def is_done(self): return self.status == self.STATUS_DONE def is_declined(self): return self.status == self.STATUS_DECLINE # Методы установки состояния def set_process(self): self.__update_command(Command.STATUS_PROCESS) def set_done(self): self.__update_command(Command.STATUS_DONE) def set_decline(self): self.__update_command(Command.STATUS_DECLINE) def __update_command(self, status): self.status = status self.save() # Оформление для админ-панели STATUS_COLORS = { STATUS_CREATE: '000000', STATUS_PROCESS: 'FFBB00', STATUS_DONE: '00BB00', STATUS_DECLINE: 'FF0000', } def colored_status(self): return '<span style="color: #%s;">%s</span>' % (self.STATUS_COLORS[self.status], self.get_status_display()) colored_status.allow_tags = True colored_status.short_description = 'Status' # Эти методы понадобятся для REST API def status_dsp(self): return self.get_status_display() def code_dsp(self): return self.get_code_display()
Админ-панель (remotecontrol\admin.py)
Примечание: Здесь и далее нам понадобится приложение «django-ipware» для определения IP клиента, установим:
pip install django-ipware
Здесь все проходит нативно: регистрируем модель в админ-панели, описываем отображаемые столбцы в таблице и поля на форме. Единственный нюанс — для сохранения IP клиента в объекте необходимо переопределить метод сохранения:
Листинг admin.py:
# -*- coding: utf-8 -*- from django.contrib import admin from ipware.ip import get_ip from .models import Command @admin.register(Command) class CommandAdmin(admin.ModelAdmin): # Отображаемые поля на странице списка объектов list_display = ('created', 'code', 'colored_status', 'ip') # Допустимые фильтры на странице списка объектов list_filter = ('code', 'status', 'ip') # Допустимые поля для формы создания\редактирования объекта fields = (('code', 'status'), ) # Переопределяем метод сохранения объекта def save_model(self, request, obj, form, change): if obj.ip is None: # Определяем и запоминаем IP только при отсутствии такового obj.ip = get_ip(request) obj.save()
Не забываем применить изменения в моделях к базе данных:
python manage.py makemigrations remotecontrol python manage.py migrate remotecontrol
В результате имеем возможность создавать\редактировать объекты...

...и просматривать список объектов в админ-панели:

Приступим к реализации логики обработки команд.
Класс IRemoteControl
Как было написано выше, в нашем распоряжении 4 команды:
- «Приостановить» — приостанавливает основной цикл демона и игнорирует все команды, кроме «Возобновить», «Перезапуск» и «Отключить пульт»;
- «Возобновить» — возобновляет основной цикл демона;
- «Перезапуск» — выполняет ре-инициализацию демона, повторное считывание конфигурации итд. Данная команда выполняется и в случае действия команды «Приостановить», но после перезапуска возобновляет основной цикл;
- «Отключить пульт управления» — прекращает обрабатывать поступающие команды (все дальнейшие команды будут игнорироваться). Данная команда выполняется и в случае действия команды «Приостановить».
При создании, команде присваивается состояние «Создана» (спасибо, Кэп!). В процессе обработки команда может быть «Выполнена» (если состояние системы удовлетворяет всем необходимым условиям) или «Отклонена» (в противном случае). Состояние «В обработке» применимо для «долгоиграющих» команд — на их выполнение может потребоваться продолжительный период времени. К примеру, получив команду «Приостановить» код всего лишь меняет значение флага, а команда «Перезапуск» инициирует выполнение более комплексной логики.
Логика обработки команд следующая:
- За одну итерацию обрабатывается одна команда;
- Получаем самую «старую» команду в состоянии «В обработке». Если таких нет — получаем самую «старую» в состоянии «Создана». Если нет — итерация завершена;
- Если команда получена с недопустимого IP — устанавливаем состояние «Отклонена». Итерация завершена;
- Если пульт управления отключен — устанавливаем команде состояние «Отклонена». Итерация завершена;
- Если команда недопустима для текущего состояния демона — устанавливаем состояние «Отклонена». Итерация завершена;
- Устанавливаем состояние «В обработке» (если требуется), выполняем команду, устанавливаем состояние «Выполнена». Итерация завершена.
«Точкой входа» в классе является метод .check_commands() — в нем реализована описанная выше логика. Этот же метод будем вызывать в основном цикле демона. В случае получения команды «Приостановить», в методе создается цикл, условием выхода из которого является получение команды «Возобновить» — таким образом достигается желаемый эффект паузы в работе демона.
Модуль control.py (remotecontrol\control.py)
Модуль, в котором опишем реализацию IRemoteControl, предлагаю разместить в каталоге приложения. Так мы получим удобно транспортируемое Django-app.
Листинг control.py
# -*- coding: utf-8 -*- import django django.setup() from time import sleep from remotecontrol.models import * class IRemoteControl(object): # Список допустимых IP. Оставьте список пустым, если хотите отключить ограничение. IP_WHITE_LIST = ['127.0.0.1'] # Флаг используемый командой CODE_REMOTE_OFF REMOTE_ENABLED = True # Метод для получения объектов команд def __get_command(self): commands = Command.objects.processing() if len(commands) == 0: commands = Command.objects.created() if len(commands) == 0: return None command = commands[0] if self.IP_WHITE_LIST and command.ip not in self.IP_WHITE_LIST: print('Wrong IP: %s' % command.ip) elif not self.REMOTE_ENABLED: print('Remote is disabled') else: return command self.__update_command(command.set_decline) # Эмуляция логики команды "Перезапуск" def __restart(self, command): if command.is_created(): self.__update_command(command.set_process) print('... Restarting ...') sleep(5) self.__update_command(command.set_done) print('... Restart complete ...') # Обертка для выполнения методов установки состояния def __update_command(self, method): try: method() except Exception as e: print('Cannot update command. Reason: %s' % e) # Логика обработки поступающих команд def check_commands(self): pause = False enter = True while enter or pause: enter = False command = self.__get_command() if command is not None: if command.code == CODE_REMOTE_OFF: self.__update_command(command.set_done) print('... !!! WARNING !!! Remote control is DISABLED ...') self.REMOTE_ENABLED = False elif command.code == CODE_RESTART: self.__restart(command) pause = False elif pause: if command.code == CODE_RESUME: self.__update_command(command.set_done) print('... Resuming ...') pause = False else: self.__update_command(command.set_decline) else: if command.code == CODE_PAUSE: self.__update_command(command.set_done) print('... Waiting for resume ...') pause = True elif pause: sleep(1)
Черная магия
Если модель сферического демона в вакууме можно представить в таком виде:
# -*- coding: utf-8 -*- class MyDaemon(object): def magic(self): # логика демона ....... def summon(self): # основной цикл while True: self.magic() MyDaemon().summon()
то внедрение интерфейса пульта управления происходит безболезненно:
# -*- coding: utf-8 -*- import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings") # Импорт модуля control возможен только после установки DJANGO_SETTINGS_MODULE # т.к. при инициализации модуля вызывается django.setup() from remotecontrol.control import * class MyDaemon(IRemoteControl): def magic(self): ....... def summon(self): while True: # Делаем прививку self.check_commands() self.magic() MyDaemon().summon()
В результате призванная нечисть управляется, но только с админ-панели.
Поместим данный код в файл, к примеру, daemon.py и пойдем дальше — напишем мобильный клиент.
REST API
Но для начала неплохо было бы реализовать интерфейс для общения мобильного клиента и серверной части. Приступим.
Подготовительный этап
Установим Django REST framework:
подключим (web\settings.py):pip install djangorestframework
и настроим (там же, добавляем в конец файла):INSTALLED_APPS = [ ....... 'rest_framework', ]
REST_FRAMEWORK = { # Разрешаем доступ пользователю с правами superuser'а 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',), # Запрещаем использовать встроенный браузер API, оставляем только JSON 'DEFAULT_RENDERER_CLASSES': ('rest_framework.renderers.JSONRenderer',), }
Сериализаторы (remotecontrol\serializers.py)
Начнем с описания набора возвращаемых данных интерфейсом REST. Здесь нам пригодятся те загадочные методы из описания модели (.status_dsp() и .code_dsp()), которые возвращают текстовое название состояния и кода команды соответственно:
Листинг serializers.py:
from rest_framework import serializers from .models import Command class CommandSerializer(serializers.ModelSerializer): class Meta: model = Command fields = ('status', 'code', 'id', 'status_dsp', 'code_dsp', 'ip')
Представления данных (remotecontrol\views.py)
Методы REST API в архитектуре Django-приложения — это те же представления, только… вы поняли.
Для общения с клиентом достаточно трех
- commands_available — возвращает список доступных кодов команд и список кодов состояний, в которых команда считается обработанной;
- commands — используется для создания нового объекта команды. Список имеющихся в БД объектов не потребуется;
- commands/<id_объекта> — используется для определения состояния объекта команды.
Для минимизации кода используем плюшки, поставляемые в комплекте с Django REST framework:
- @api_view — декоратор для function based view, параметром указывается список допустимых http-методов;
- generics.CreateAPIView — класс для методов создания объектов, поддерживает только POST;
- generics.RetrieveAPIView — класс для получения подробной информации об объекте, поддерживает только GET.
Листинг views.py:
from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework import generics from ipware.ip import get_ip from .models import Command from .serializers import CommandSerializer @api_view(['GET']) def commands_available(request): # API-метод "список доступных кодов команд" response = { # Список доступных кодов команд. При желании CODE_REMOTE_OFF можно # исключить, чтобы не отображать "красную кнопку" в мобильном клиенте. 'commands': dict(Command.COMMAND_CHOICES), # Список кодов состояний, в которых команда считается обработанной. 'completed': [Command.STATUS_DONE, Command.STATUS_DECLINE], } return Response(response) class CommandList(generics.CreateAPIView): # API-метод "создать команду" serializer_class = CommandSerializer def post(self, request, *args, **kwargs): # Определяем и запоминаем IP клиента request.data[u'ip'] = u'' + get_ip(request) return super(CommandList, self).post(request, *args, **kwargs) class CommandDetail(generics.RetrieveAPIView): # API-метод "получить состояние команды" queryset = Command.objects.all() serializer_class = CommandSerializer
End-point'ы (remotecontrol\urls.py)
Опишем end-point'ы реализованных API-методов.
Листинг urls.py:
from django.conf.urls import url from . import views urlpatterns = [ url(r'^commands_available/$', views.commands_available), url(r'^commands/$', views.CommandList.as_view()), url(r'^commands/(?P<pk>[0-9]+)/$', views.CommandDetail.as_view()), ]
И подключим их к проекту (web\urls.py):
urlpatterns = [ ....... url(r'^remotecontrol/', include('remotecontrol.urls')), ]
Интерфейс для общения реализован. Переходим к самому вкусному.
«Remote Control App»
Для общения с сервером используем UrlRequest (kivy.network.urlrequest.UrlRequest). Из всех его достоинств нам понадобятся следующие:
- поддержка асинхронного режима;
- автоматическая конвертация полученного в ответ корректного JSON в Python dict.
Для простоты реализации будем использовать схему аутентификации Basic. При желании, можно одну из следующих статей посвятить другим способам аутентификации на web-ресурсах с помощью UrlRequest — пишите в комментариях.
Листинг main.py
# -*- coding: utf-8 -*- import kivy kivy.require('1.9.1') from kivy.network.urlrequest import UrlRequest from kivy.properties import StringProperty, Clock from kivy.uix.button import Button from kivy.app import App from kivy.uix.boxlayout import BoxLayout try: from kivy.garden.xpopup import XError, XProgress except: from xpopup import XError, XProgress from json import dumps import base64 class RemoteControlUI(BoxLayout): """ Реализация основного виджета приложения """ # Свойства для аутентификации на сервере login = StringProperty(u'') password = StringProperty(u'') host = StringProperty('') def __init__(self, **kwargs): # ID текущего обрабатываемого объекта команды self._cmd_id = None # Список кодов "завершенных" состояний self._completed = [] # Флаг потребности ожидания завершения обработки команды. # Сбрасывается при получении "завершенного" состояния или # при закрытии окна прогресса. self._wait_completion = False super(RemoteControlUI, self).__init__( orientation='vertical', spacing=2, padding=3, **kwargs) # Панель для командных кнопок self._pnl_commands = BoxLayout(orientation='vertical') self.add_widget(self._pnl_commands) # ============= Отправка http-запроса ============== def _get_auth(self): # Подготовка данных для заголовка "Authorization" cred = ('%s:%s' % (self.login, self.password)) return 'Basic %s' %\ base64.b64encode(cred.encode('ascii')).decode('ascii') def _send_request(self, url, success=None, error=None, params=None): # Отправка асинхронного запроса headers = { 'User-Agent': 'Mozilla/5.0', 'Content-type': 'application/json', 'Authorization': self._get_auth() } UrlRequest( url=self.host + url, timeout=30, req_headers=headers, req_body=None if params is None else dumps(params), on_success=success, on_error=error, on_failure=error) # =========== Получение списка доступных кодов команд =========== def _get_commands(self, instance=None): # Реализация обращения к API-методу "commands_available" self._progress_start('Trying to get command list') self._send_request( 'commands_available/', success=self._get_commands_result, error=self._get_commands_error) def _get_commands_result(self, request, response): # callback для парсинга ответа try: self._pnl_commands.clear_widgets() # Для каждого доступного кода команды создаем кнопку for code, command in sorted( response['commands'].items(), key=lambda x: int(x[0])): btn = Button( id=code, text=command, on_release=self._btn_command_click) self._pnl_commands.add_widget(btn) self._completed = response['completed'] self._progress_complete('Command list received successfully') except Exception as e: self._get_commands_error(request, str(e)) def _get_commands_error(self, request, error): # callback для обработки ошибки self._progress_complete() XError(text=str(error)[:256], buttons=['Retry', 'Exit'], on_dismiss=self._get_commands_error_dismiss) def _get_commands_error_dismiss(self, instance): # callback для окна ошибки if instance.button_pressed == 'Exit': App.get_running_app().stop() elif instance.button_pressed == 'Retry': self._get_commands() # ============= Отправка команды ============= def _btn_command_click(self, instance): # Реализация обращения к API-методу "commands" self._cmd_id = None self._wait_completion = True self._progress_start('Processing command "%s"' % instance.text) self._send_request( 'commands/', params={'code': instance.id}, success=self._send_command_result, error=self._send_command_error) def _send_command_result(self, request, response): # callback для парсинга ответа try: if response['status'] not in self._completed: # Команда обрабатывается - запоминаем ID объекта self._cmd_id = response['id'] # Запрос на проверку состояния будет отправляться до тех пор, # пока открыто окно с прогрессом if self._wait_completion: # Отправляем запрос для проверки состояния Clock.schedule_once(self._get_status, 1) else: # Команда обработана self._progress_complete( 'Command "%s" is %s' % (response['code_dsp'], response['status_dsp'])) except Exception as e: XError(text=str(e)[:256]) def _send_command_error(self, request, error): # callback для обработки ошибки self._progress_complete() XError(text=str(error)[:256]) # ========== Получение кода состояния команды ========== def _get_status(self, pdt=None): # Реализация обращения к API-методу "commands/<id_объекта>" if not self._cmd_id: return self._send_request( 'commands/%s/' % self._cmd_id, success=self._send_command_result, error=self._send_command_error) # ============= Методы для работы с окном прогресса ============== def _progress_start(self, text): self.popup = XProgress( title='RemoteControl', text=text, buttons=['Close'], on_dismiss=self._progress_dismiss) self.popup.autoprogress() def _progress_dismiss(self, instance): self._wait_completion = False def _progress_complete(self, text=''): if self.popup is not None: self.popup.complete(text=text, show_time=0 if text is None else 1) # ========================================= def start(self): self._get_commands() class RemoteControlApp(App): """ Реализация приложения """ remote = None def build(self): # Инициализируем интерфейс приложения self.remote = RemoteControlUI( login='test', password='qwerty123', host='http://localhost:8000/remotecontrol/') return self.remote def on_start(self): self.remote.start() # Запускаем приложение RemoteControlApp().run()
Надеюсь, комментариев в коде достаточно для понимания. Если все же недостаточно — сообщайте, буду вносить правки.
На этом баловство с кодом завершается и на сцену выходит
Тяжелая артиллерия
О Buildozer'е можно говорить долго, потому что о нем сказано мало. Есть и статьи на хабре (об установке и настройке и о сборке релиз-версии и публикации на Google Play), конечно же, есть и документация… Но есть и нюансы,
Несколько практических советов по борьбе с этим wunderwaffe:
- Для сборки Android-приложения все же потребуется Linux, можно обойтись и виртуальной машиной. Обусловлено это тем, что python-for-android (необходимый для сборки пакет) в текущей версии использует более свежую версию пакета sh (ранее pbs), в которой отсутствует поддержка Windows;
- На самом деле, процесс сборки затягивается надолго только в первый раз — здесь Buildozer устанавливает и настраивает необходимые Android-dev зависимости. Все последующие сборки (с учетом, что в конфигурации сборки не менялись параметры ndk, sdk или requirements) выполняются за 30-40 секунд;
- Перед установкой Buildozer убедитесь, что корректно установлен Kivy и Kivy-garden (последний должен установится автоматически с Kivy);
- Также, перед установкой Buildozer необходимо установить зависимости (подробнее — здесь). Сам Buildozer их не устанавливает, но могут возникнуть нештатные ситуации при установке или (что хуже) в процессе сборки.
- НИКОГДА не запускайте Buildozer под правами root;
Ну и немного кода в помощь счастливым обладателям Debian и Ubuntu (остальным потребуется «тщательно обработать напильником»)
kivy-install.sh
# Create virtualenv virtualenv --python=python2.7 .env # Activate virtualenv source .env/bin/activate # Make sure Pip, Virtualenv and Setuptools are updated pip install --upgrade pip virtualenv setuptools # Use correct Cython version here pip install --upgrade Cython==0.20 # Install necessary system packages sudo apt-get install --upgrade build-essential mercurial git python-dev libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libsdl1.2-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev zlib1g-dev # Install kivy pip install --upgrade kivy
buildozer-install.sh
# Activate virtualenv source .env/bin/activate # Android SDK has 32bit libs sudo dpkg --add-architecture i386 # add system dependencies sudo apt-get update sudo apt-get install --upgrade ccache sudo apt-get install --upgrade libncurses5:i386 libstdc++6:i386 zlib1g:i386 sudo apt-get install --upgrade openjdk-7-jdk sudo apt-get install --upgrade unzip # Install buildozer pip install --upgrade buildozer
Теперь, когда Buildozer установлен, инициализируем его:
buildozer init
В результате работы этой команды в каталоге создастся файл конфигурации сборки (buildozer.spec). В нем находим указанные ниже ключи и присваиваем им соответствующие значения:
Правки для buildozer.spec
# (list) Garden requirements
garden_requirements = xpopup
# (str) Supported orientation (one of landscape, portrait or all)
orientation = portrait
# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0
# (list) Permissions
android.permissions = INTERNET
# (int) Minimum API required
android.minapi = 13
# (int) Android SDK version to use
android.sdk = 21
Активируем wunderwaffe:
buildozer android debug
и на выходе имеем .apk, который можно установить на Android-девайс.
Готово. С чем я вас и поздравляю!
Тестирование
И давайте посмотрим, как все это работает. Не зря же так долго старались :)
Запускаем Django-сервер, параметром указываем IP вашей машины в локальной сети:
python manage.py 192.168.xxx.xxx:8000
Призываем нечисть:
python daemon.py
Стартуем приложение на Android-девайсе и видим нечто подобное:
Примечание: Для записи видео использовалась финальная версия проекта, которую можно найти на github. От кода, приведенного в статье, отличается расширением функционала. В серверную часть добавлена поддержка пользовательских команд и отладочные сообщения (для наглядности), а в клиент добавлены: форма авторизации, запрос на подтверждение выполнения команды и некоторые удобства в интерфейс.
Подведем итоги
Что мы получили в результате?
- Легко встраиваемый класс, реализующий логику реакции на удаленные команды;
- Серверное приложение, позволяющее управлять произвольным скриптом из web-интерфейса, и предоставляющее REST API;
- Android-приложение для управления скриптом посредством REST API.
Может это слишком громко сказано, но… Теперь меня мучает вопрос — а можно ли реализовать аналогичную архитектуру, используя другие языки и технологии (кроме Python), приложив при этом (хотя бы) не больше усилий и написав не больше кода?
На этом все.
Всем приятного кодинга и удачных сборок.
Полезные ссылки
«RemoteControlInterface» на github
Доки по Django
Доки по Django REST framework
Доки по Kivy
Установка Kivy
Установка Buildozer
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Стоит ли развивать проект «RemoteControlInterface»?
55.26%Однозначно да, в нем не хватает… (пожелания в комментариях или issue на github)21
5.26%Нет, проект уже содержит весь необходимый функционал.2
39.47%Да кому он нужен! Лучше использовать готовые решения (примеры — в комментариях).15
Проголосовали 38 пользователей. Воздержались 37 пользователей.
