Привет!
Сегодня расскажу о том, как управлять компьютером с мобильного устройства. Нет, это не очередной аналог radmin'a, и не пример того, как можно поиздеваться над компьютером друга. Речь пойдет об удаленном управлении демоном, а точнее — о создании интерфейса для управления демоном, написанном на Python.
Архитектура довольно простая:
- «Remote control App» — Kivy-приложение, реализующее клиентскую часть для мобильных устройств.
- «Remote control» — Django-приложение, реализующее REST API и взаимодействие с БД;
- IRemoteControl — Класс, реализующий логику обработки поступивших команд (будет использован в демоне);
Заинтересовавшимся — добро пожаловать под кат.
Перед тем, как приступать к реализации, предлагаю «подготовить» каталог проекта. Нужно:
- создать отдельный Python virtual environment
virtualenv .env
- создать новый Django-проект (например — web)
django-admin startproject web
Все операции с Django будем выполнять относительно этого каталога;
- создать каталог для Android-приложения (например — ui_app). Все операции касательно мобильного приложения будем выполнять относительно этого каталога.
«Remote control»
Начнем с серверной части — Django-приложения. Создадим новое приложение и добавим superuser'а:
python manage.py startapp remotecontrol
Рекомендую сразу же его добавить в используемые Django-проектом приложения (websettings.py или вместо «web» — имя вашего Djnago-проекта):
INSTALLED_APPS = [
.......
'remotecontrol',
]
Создадим БД и superuser'а:
python manage.py migrate
python manage.py createsuperuser
Настройки завершены, приступаем к реализации приложения.
Модели (remotecontrolmodels.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, потребуется использовать константы и описывать логику (хоть двухстрочную, но все же) обновления команды, что, имхо, не совсем удобно. Намного удобнее дергать необходимые методы. Но если такой подход противоречит концепции — с удовольствием выслушаю аргументы в комментариях.
# -*- 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()
Админ-панель (remotecontroladmin.py)
Примечание: Здесь и далее нам понадобится приложение «django-ipware» для определения IP клиента, установим:
pip install django-ipware
Здесь все проходит нативно: регистрируем модель в админ-панели, описываем отображаемые столбцы в таблице и поля на форме. Единственный нюанс — для сохранения IP клиента в объекте необходимо переопределить метод сохранения:
# -*- 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 (remotecontrolcontrol.py)
Модуль, в котором опишем реализацию IRemoteControl, предлагаю разместить в каталоге приложения. Так мы получим удобно транспортируемое Django-app.
# -*- 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:
pip install djangorestframework
подключим (websettings.py):
INSTALLED_APPS = [
.......
'rest_framework',
]
и настроим (там же, добавляем в конец файла):
REST_FRAMEWORK = {
# Разрешаем доступ пользователю с правами superuser'а
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',),
# Запрещаем использовать встроенный браузер API, оставляем только JSON
'DEFAULT_RENDERER_CLASSES': ('rest_framework.renderers.JSONRenderer',),
}
Сериализаторы (remotecontrolserializers.py)
Начнем с описания набора возвращаемых данных интерфейсом REST. Здесь нам пригодятся те загадочные методы из описания модели (.status_dsp() и .code_dsp()), которые возвращают текстовое название состояния и кода команды соответственно:
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')
Представления данных (remotecontrolviews.py)
Методы REST API в архитектуре Django-приложения — это те же представления, только… вы поняли.
Для общения с клиентом достаточно трех букв слов API-методов (эхх, идеальный мир...):
- commands_available — возвращает список доступных кодов команд и список кодов состояний, в которых команда считается обработанной;
- commands — используется для создания нового объекта команды. Список имеющихся в БД объектов не потребуется;
- commands/<id_объекта> — используется для определения состояния объекта команды.
Для минимизации кода используем плюшки, поставляемые в комплекте с Django REST framework:
- @api_view — декоратор для function based view, параметром указывается список допустимых http-методов;
- generics.CreateAPIView — класс для методов создания объектов, поддерживает только POST;
- generics.RetrieveAPIView — класс для получения подробной информации об объекте, поддерживает только GET.
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'ы (remotecontrolurls.py)
Опишем end-point'ы реализованных API-методов.
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()),
]
И подключим их к проекту (weburls.py):
urlpatterns = [
.......
url(r'^remotecontrol/', include('remotecontrol.urls')),
]
Интерфейс для общения реализован. Переходим к самому вкусному.
«Remote Control App»
Для общения с сервером используем UrlRequest (kivy.network.urlrequest.UrlRequest). Из всех его достоинств нам понадобятся следующие:
- поддержка асинхронного режима;
- автоматическая конвертация полученного в ответ корректного JSON в Python dict.
Для простоты реализации будем использовать схему аутентификации Basic. При желании, можно одну из следующих статей посвятить другим способам аутентификации на web-ресурсах с помощью UrlRequest — пишите в комментариях.
# -*- 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), конечно же, есть и документация… Но есть и нюансы, о которых можно написать целую статью которые разбросаны по разным источникам. Постараюсь собрать основные моменты здесь.
- Для сборки 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 (остальным потребуется «тщательно обработать напильником»)
# 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
# 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). В нем находим указанные ниже ключи и присваиваем им соответствующие значения:
# (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
Автор: ophermit