Skip to main content

ENPAF — Python + Web framework for building Android APK applications

Project description

🚀 ENPAF — Engine for Native Python App Framework

CI Python Version License: PolyForm NC 1.0.0

Создавайте Android-приложения на Python + HTML/CSS/JS и собирайте их в APK.

📖 Полная документация — в Wiki. 💾 Готовые сборки (wheel фреймворка + APK приложений) — на странице Releases. 📄 Лицензия PolyForm Noncommercial 1.0.0 — бесплатно для некоммерческого использования, для коммерческого нужна платная лицензия (см. Лицензия).

ENPAF — это фреймворк, который позволяет писать мобильные приложения, используя привычный веб-стек (HTML/CSS/JS) для интерфейса и Python для логики. В режиме разработки приложение работает как обычный сайт (Flask + WebSocket + hot-reload), а готовый продукт собирается в настоящий APK на базе WebView + встроенного Python (Chaquopy).

┌──────────────────────── Android APK ────────────────────────┐
│   ┌────────────┐   Bridge (JSON)   ┌────────────────────┐   │
│   │  WebView   │ ◄───────────────► │   Python (main.py) │   │
│   │ HTML/CSS/JS│                   │   enpaf core       │   │
│   │ + enpaf.js │                   │   ваш код          │   │
│   └────────────┘                   └────────────────────    │
└─────────────────────────────────────────────────────────────┘

📑 Содержание


✨ Возможности

  • 🐍 Python для логики, HTML/CSS/JS для интерфейса.
  • 🔗 Двусторонний мост Python ↔ JavaScript (вызовы и события).
  • 💾 Встроенное хранилище — key-value и коллекции на SQLite.
  • Hot-reload в режиме разработки.
  • ⚙️ Веб-панель настроек — иконка, имя, ориентация, цвета, разрешения, фичи и диплинки настраиваются прямо в браузере и пишутся в enpaf.json.
  • 📱 Нативные API — toast, вибрация, уведомления, буфер обмена, шаринг, ориентация.
  • 🛰 Чтение датчиков из Python — геолокация, акселерометр, гироскоп, магнитометр, освещённость, NFC, Bluetooth, микрофон, батарея, сеть.
  • 🧩 Модули устройстваapp.wifi, app.bluetooth, app.location, app.sensors, app.nfc, app.battery, app.audio… единый стиль app.<имя> / enpaf.<имя>.* (камера — прямо в WebView).
  • 📶 Bluetooth и Wi-Fi — поиск устройств/сетей, подключение, обмен сообщениями.
  • 📷 Камера прямо в WebView (live‑превью + снимок).
  • 🔐 Разрешения по запросу — запрашивайте доступ в нужный момент прямой функцией из Python (app.api.request_permissions([...])), а не при запуске.
  • 🧩 uses-feature (камера, NFC, датчики, Bluetooth, GPS…).
  • 🔗 Deep links (кастомные схемы и App Links).
  • 📦 Сборка в APK/AAB на Windows/macOS/Linux через Gradle + Chaquopy.

📥 Установка

Из исходников (рекомендуется)

git clone https://github.com/aaalllexxx/enpaf
cd enpaf
pip install setuptools wheel        # нужны для установки
pip install -e .                    # editable: правки сразу подхватываются

После установки доступна команда paf и пакет enpaf для импорта. Editable‑режим удобен тем, что изменения в коде фреймворка применяются без переустановки.

Из релиза (wheel)

Скачайте enpaf-<версия>-py3-none-any.whl со страницы Releases и установите:

pip install enpaf-1.1.3-py3-none-any.whl

📋 Требования

Что Зачем Версия
Python фреймворк и CLI 3.9+
Java JDK сборка APK 17–21 (рекомендуется 17)
Android SDK сборка APK Android Studio или command-line tools

Для разработки (paf run) нужен только Python. JDK и Android SDK нужны только для сборки APK (paf build). Проверить окружение: paf doctor.


⚡ Быстрый старт

paf create myapp          # создать проект
cd myapp
paf run                   # запустить dev-сервер → http://127.0.0.1:8080
# ... разрабатываете, правите app/ и main.py, страница перезагружается сама ...
paf build apk             # собрать APK → dist/myapp-1.0.0.apk

Установить APK на телефон:

adb install dist/myapp-1.0.0.apk
# либо просто перекиньте .apk на телефон и откройте

🧱 Создание базового приложения с нуля

Соберём небольшое приложение «Заметки + датчик»: кнопка зовёт Python, заметки хранятся в SQLite, а отдельная кнопка читает геолокацию с устройства (предварительно запросив разрешение). Все файлы — настоящие, можно копировать как есть.

Шаг 1. Создать проект

paf create myapp
cd myapp

Получится дерево (см. Структура проекта). Дальше правим четыре файла: enpaf.json, main.py, app/index.html, app/js/app.js.

Шаг 2. enpaf.json — метаданные и разрешения

permissions лишь объявляют разрешения в манифесте; сам системный диалог мы покажем позже из Python (см. Шаг 5).

{
    "name": "MyApp",
    "package": "com.example.myapp",
    "version": "1.0.0",
    "orientation": "portrait",
    "permissions": ["INTERNET", "FINE_LOCATION", "VIBRATE"],
    "features": [
        { "key": "GPS", "required": false }
    ],
    "min_sdk": 24,
    "target_sdk": 34,
    "theme": { "primary_color": "#6C5CE7", "status_bar_color": "#5A4BD1" }
}

Шаг 3. app/index.html — интерфейс

enpaf.js подключать не нужно: dev-сервер и сборщик APK внедряют мост сами.

<!-- файл: app/index.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>MyApp</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>MyApp</h1>

    <button onclick="sayHello()">Поздороваться с Python</button>
    <div id="greeting"></div>

    <h2>Заметки</h2>
    <input id="noteInput" placeholder="Текст заметки…">
    <button onclick="addNote()">Добавить</button>
    <ul id="notes"></ul>

    <h2>Где я?</h2>
    <button onclick="whereAmI()">Узнать геолокацию</button>
    <div id="location"></div>

    <!-- ваш код приложения -->
    <script src="js/app.js"></script>
</body>
</html>

Шаг 4. app/js/app.js — фронтенд-логика

Весь обмен с Python идёт через глобальный объект window.enpaf (он готов после загрузки страницы; дождаться можно через enpaf.ready(...)).

// файл: app/js/app.js

// Вызвать Python-функцию "hello" (зарегистрирована в main.py)
async function sayHello() {
    const res = await enpaf.call("hello", { name: "Alex" });
    document.getElementById("greeting").textContent = res.message;
}

// Сохранить заметку через Python -> SQLite
async function addNote() {
    const input = document.getElementById("noteInput");
    if (!input.value.trim()) return;
    await enpaf.call("save_note", { text: input.value.trim() });
    input.value = "";
    loadNotes();
}

// Загрузить заметки из Python
async function loadNotes() {
    const { notes } = await enpaf.call("get_notes", {});
    document.getElementById("notes").innerHTML =
        notes.map(n => `<li>${n.text}</li>`).join("");
}

// Запросить разрешение на геолокацию в нужный момент, затем прочитать датчик
async function whereAmI() {
    const out = document.getElementById("location");

    // Системный диалог появится именно сейчас, по нажатию кнопки:
    const grant = await enpaf.permissions.request(["FINE_LOCATION"]);
    if (grant.denied && grant.denied.length) {
        out.textContent = "Доступ к геолокации не выдан";
        return;
    }

    const loc = await enpaf.sensors.location();
    out.textContent = loc.fix
        ? `Широта ${loc.latitude}, долгота ${loc.longitude}`
        : "Координаты пока недоступны";
}

// Подгрузить заметки сразу после готовности моста
enpaf.ready(loadNotes);

Шаг 5. main.py — логика на Python

# файл: main.py
from enpaf import EnpafApp

app = EnpafApp(__name__)


# ─── Страница ────────────────────────────────────────────────
@app.route("/")
def index():
    return app.render("index.html", title=app.name)


# ─── Bridge-функции (их зовёт app/js/app.js через enpaf.call) ─
@app.bridge_handler("hello")
def hello(params):
    name = params.get("name", "World")
    app.api.vibrate(150)                       # нативная вибрация
    return {"message": f"Привет, {name}! 👋"}


@app.bridge_handler("save_note")
def save_note(params):
    text = params.get("text", "").strip()
    if not text:
        return {"success": False}
    note_id = app.storage.collection("notes").add({"text": text})
    return {"success": True, "id": note_id}


@app.bridge_handler("get_notes")
def get_notes(params):
    return {"notes": app.storage.collection("notes").all()}


# ─── Реакция на результат запроса разрешения (необязательно) ──
@app.on("permission_result")
def on_permission(data):
    print("Выданы:", data["granted"], "| отклонены:", data["denied"])


if __name__ == "__main__":
    app.run()

Шаг 6. Запустить и собрать

paf run            # http://127.0.0.1:8080 — проверяем в браузере (hot-reload)
paf build apk      # dist/myapp-1.0.0.apk — ставим на телефон

💡 В браузере (paf run) датчики и разрешения возвращают dev-заглушки (например, фиксированные координаты), чтобы интерфейс можно было отладить без телефона. На устройстве из APK читаются реальные значения.


🛠 CLI: команда paf

Команда Описание
paf create <name> Создать новый проект из шаблона
paf run Запустить dev-сервер (Flask + hot-reload)
paf build apk Собрать debug APK
paf build apk --release Собрать release APK
paf build aab Собрать release Android App Bundle (.aab)
paf doctor Проверить окружение (Python, JDK, Android SDK)
paf info Показать информацию о текущем проекте
paf update Обновить PAF до последней версии (PyPI)

Флаги update:

  • --pre — включая пред-релизные версии

Флаги create:

  • --package, -p — Android package id (по умолчанию com.enpaf.<name>)
  • --template, -t — шаблон проекта (по умолчанию default)

Флаги run:

  • --host (по умолчанию 127.0.0.1)
  • --port (по умолчанию 8080)
  • --no-browser — не открывать браузер автоматически
  • --debug — режим отладки

Флаги build:

  • --release — релизная сборка
  • --keystore <path> — keystore для подписи
  • --clean — чистая сборка (удалить кэш сборки)

🏗 Структура проекта

myapp/
├── app/                  # Интерфейс (HTML/CSS/JS) → попадёт в assets APK
│   ├── index.html        # Главная страница
│   ├── css/style.css
│   ├── js/app.js
│   ├── pages/            # Доп. страницы
│   └── img/
├── main.py               # Python-логика (точка входа)
├── enpaf.json            # Конфигурация проекта
├── icon.png              # (опционально) иконка приложения
├── data/                 # Локальная БД (SQLite) — создаётся автоматически
└── dist/                 # Готовые APK после сборки

⚙️ Конфигурация enpaf.json

Полный пример со всеми полями:

{
    "name": "My App",
    "package": "com.example.myapp",
    "version": "1.0.0",
    "description": "My ENPAF Application",
    "author": "Developer",
    "orientation": "portrait",
    "icon": "icon.png",
    "permissions": ["INTERNET", "CAMERA", "VIBRATE"],
    "features": [
        { "key": "CAMERA", "required": true },
        { "key": "NFC", "required": false }
    ],
    "deeplinks": [
        { "label": "Open profile", "scheme": "myapp", "host": "open",
          "path": "/profile", "pathType": "prefix", "autoVerify": false }
    ],
    "python_requirements": ["requests"],
    "min_sdk": 24,
    "target_sdk": 34,
    "theme": {
        "primary_color": "#6C5CE7",
        "status_bar_color": "#5A4BD1"
    }
}
Поле Тип Описание
name string Название приложения (ярлык на устройстве)
package string Android application id, напр. com.example.myapp
version string Версия (major.minor.patch)
description, author string Метаданные
orientation string portrait / landscape / auto / sensor / unspecified
icon string Путь к иконке (PNG/JPG/WebP) относительно проекта
permissions string[] Список ключей разрешений (см. таблицу)
features object[] `{ "key": "...", "required": true
deeplinks object[] Диплинки (см. раздел)
python_requirements string[] pip-зависимости, встраиваемые в APK через Chaquopy
min_sdk / target_sdk int Android API levels (по умолчанию 24 / 34)
theme.primary_color string Основной цвет (HEX)
theme.status_bar_color string Цвет статус-бара (HEX)
log_level string Уровень логирования (INFO, DEBUG, …)

💡 Эти поля удобнее всего редактировать через панель настроек ⚙, не открывая JSON вручную.


🐍 Python API

main.py — точка входа

from enpaf import EnpafApp

app = EnpafApp(__name__)

# ─── Маршруты страниц (Jinja2-шаблоны, режим разработки) ───
@app.route("/")
def index():
    return app.render("index.html", title=app.name)

# ─── Bridge-функции (вызываются из JavaScript) ───
@app.bridge_handler("get_user")
def get_user(params):
    user_id = params.get("id")
    return {"id": user_id, "name": "Alex"}

# ─── События жизненного цикла ───
@app.on("app_start")
def on_start():
    print("Приложение запущено!")

if __name__ == "__main__":
    app.run()

Класс EnpafApp

EnpafApp(import_name, app_dir="app", config_file="enpaf.json")

Декораторы / методы:

Метод Назначение
@app.route(path, methods=None) Зарегистрировать страницу-маршрут
@app.bridge_handler(name) / @app.bridge_func(name) Зарегистрировать функцию, вызываемую из JS
@app.on(event) Подписаться на событие
app.emit(event, data=None) Отправить событие (в Python и в JS)
app.render(template, **context) Отрендерить Jinja2-шаблон из app/
app.run(host, port, debug, open_browser) Запустить (dev-сервер или Android runtime)

Свойства / компоненты:

Свойство Тип Описание
app.config dict Содержимое enpaf.json
app.name str Имя приложения
app.storage Storage Локальное хранилище
app.events EventEmitter Система событий
app.bridge Bridge Мост Python↔JS
app.api DeviceAPI Доступ к функциям устройства
app.router Router Роутер/шаблонизатор

Хранилище — app.storage

Key-value:

app.storage.set("theme", "dark")          # значение (str/int/float/bool/dict/list)
theme = app.storage.get("theme", "light") # с дефолтом
app.storage.delete("theme")
app.storage.exists("theme")               # -> bool
app.storage.keys("user_%")                # LIKE-паттерн
app.storage.all()                         # -> dict всех пар
app.storage.clear()

Коллекции (мини документ-стор):

notes = app.storage.collection("notes")
note_id = notes.add({"text": "Привет"})   # -> int (id)
notes.all()                                # -> list[dict] (+ _id, _created_at)
notes.find({"text": "Привет"})            # -> list[dict]
notes.find_one({"text": "Привет"})        # -> dict | None
notes.update(note_id, {"text": "Пока"})
notes.delete(note_id)
notes.count()                              # -> int
notes.clear()

На Android БД автоматически пишется в записываемый каталог приложения (getFilesDir()), а не рядом с исходниками.

События — app.events

@app.on("app_start")
def _(): ...

app.events.once("page_load", handler)   # сработает один раз
app.events.off("app_start", handler)    # отписаться
app.events.emit("my_event", payload)    # вызвать

Встроенные lifecycle-события: app_start, app_stop, app_pause, app_resume, app_error, page_load, page_unload, bridge_connect, bridge_disconnect.

Функции устройства — app.api (DeviceAPI)

app.api.toast("Сохранено!", duration="short")  # "short" | "long"
app.api.vibrate(200)                            # мс
app.api.get_device_info()                       # -> dict
app.api.set_status_bar_color("#000000")
app.api.set_orientation("portrait")             # "portrait"|"landscape"|"auto"
app.api.open_url("https://example.com")
app.api.clipboard_set("текст")
app.api.clipboard_get()
app.api.share("Посмотри!", title="Моё приложение")

Датчики устройства — app.api (читаются из Python)

Каждый метод возвращает обычный dict (можно сразу return из bridge-функции). В режиме paf run (браузер) возвращается заглушка с полем "dev": true.

# файл: main.py
@app.bridge_handler("read_sensors")
def read_sensors(params):
    return {
        "gyro":     app.api.read_sensor("gyroscope"),     # {values:[x,y,z], ...}
        "accel":    app.api.read_sensor("accelerometer"),
        "location": app.api.get_location(),               # {latitude, longitude, accuracy, ...}
        "bt":       app.api.get_bluetooth(),              # {enabled, name, bonded:[...]}
        "nfc":      app.api.get_nfc(),                    # {present, enabled}
        "mic":      app.api.get_audio_level(),            # {amplitude, db}  (нужен RECORD_AUDIO)
        "battery":  app.api.get_battery(),                # {level, charging}
        "network":  app.api.get_network(),                # {connected, type}
    }
Метод Возвращает
app.api.read_sensor(name, timeout=2.0) Снимок датчика: accelerometer, gyroscope, magnetometer, light, proximity, pressure, gravity, rotation_vector, step_counter, heart_rate, …
app.api.list_sensors() Список всех датчиков устройства
app.api.get_location() Последняя известная геопозиция
app.api.get_bluetooth() Состояние Bluetooth + сопряжённые устройства
app.api.get_nfc() Наличие/состояние NFC
app.api.get_audio_level(duration=0.4) Пиковая громкость с микрофона
app.api.get_battery() Уровень заряда и зарядка
app.api.get_network() Тип подключения к сети
app.api.get_sensor_snapshot() Всё самое частое одним вызовом

Разрешения по запросу — app.api

# файл: main.py
@app.bridge_handler("enable_mic")
def enable_mic(params):
    # Показать системный диалог именно сейчас (не при запуске приложения).
    # Результат придёт в @app.on("permission_result") и в JS-событие.
    app.api.request_permissions(["RECORD_AUDIO"])
    return {"requested": True}

@app.bridge_handler("mic_ready")
def mic_ready(params):
    return app.api.check_permission("RECORD_AUDIO")   # {granted: bool}

Подробнее — в разделе Запрос разрешений во время работы.


🌐 JavaScript SDK (enpaf.js)

Мост enpaf.js подключается автоматически: в режиме разработки его внедряет сервер, а при сборке APK — билдер вставляет <script src="js/enpaf.js"> в ваши HTML-страницы. Глобальный объект — window.enpaf.

Вызовы и события

// Вызвать Python-функцию (-> Promise)
const user = await enpaf.call("get_user", { id: 42 });

// События из Python
enpaf.on("data_updated", (payload) => console.log(payload));
enpaf.off("data_updated");

// Отправить событие в Python
enpaf.emit("button_clicked", { id: "save" });

// Готовность моста
enpaf.ready(() => console.log("bridge ready"));

// Навигация между страницами
enpaf.navigate("/pages/about.html");

enpaf.version;    // "1.0.0"
enpaf.isAndroid;  // true в APK, false в браузере

Хранилище из JS

await enpaf.storage.set("theme", "dark");
const theme = await enpaf.storage.get("theme");
await enpaf.storage.delete("theme");

Функции устройства — enpaf.device

Метод Описание
enpaf.device.toast(msg, dur) Toast-уведомление ("short"/"long")
enpaf.device.vibrate(ms) Вибрация
enpaf.device.notify(title, text, id) Системное уведомление
enpaf.device.share(text, title) Системный «Поделиться»
enpaf.device.setOrientation(mode) "portrait"/"landscape"/"auto"
enpaf.device.clipboard(text) Скопировать в буфер обмена
enpaf.device.openUrl(url) Открыть ссылку во внешнем браузере
enpaf.device.getInfo() Информация об окружении (Promise)

В браузере (dev) методы используют веб-аналоги (Web Notifications, navigator.share, navigator.clipboard и т.д.), на Android — нативные вызовы.

Утилиты

enpaf.utils.formatDate(Date.now(), "ru-RU");
enpaf.utils.uid();   // случайный id

🛰 Датчики и сенсоры устройства

Датчики читаются в Python (app.api.*), а из интерфейса удобнее всего звать их через enpaf.sensors.* — каждый метод возвращает Promise с тем же dict, что и Python.

// файл: app/js/app.js
const gyro  = await enpaf.sensors.read("gyroscope");   // {values:[x,y,z], accuracy, ...}
const loc   = await enpaf.sensors.location();          // {latitude, longitude, accuracy, fix}
const bt    = await enpaf.sensors.bluetooth();         // {enabled, name, bonded:[...]}
const nfc   = await enpaf.sensors.nfc();               // {present, enabled}
const mic   = await enpaf.sensors.audioLevel();        // {amplitude, db}
const batt  = await enpaf.sensors.battery();           // {level, charging}
const net   = await enpaf.sensors.network();           // {connected, type}
const all   = await enpaf.sensors.snapshot();          // всё одним вызовом
const list  = await enpaf.sensors.list();              // список датчиков устройства
JS-метод Python-эквивалент
enpaf.sensors.read(name, opts?) app.api.read_sensor(name)
enpaf.sensors.list() app.api.list_sensors()
enpaf.sensors.location() app.api.get_location()
enpaf.sensors.bluetooth() app.api.get_bluetooth()
enpaf.sensors.nfc() app.api.get_nfc()
enpaf.sensors.audioLevel(dur?) app.api.get_audio_level()
enpaf.sensors.battery() app.api.get_battery()
enpaf.sensors.network() app.api.get_network()
enpaf.sensors.snapshot() app.api.get_sensor_snapshot()

Какие нужны разрешения: геолокация — FINE_LOCATION/COARSE_LOCATION; микрофон — RECORD_AUDIO; сопряжённые Bluetooth-устройства (Android 12+) — BLUETOOTH_CONNECT; NFC — NFC. Добавьте их в enpaf.json и запросите во время работы (следующий раздел). Акселерометр, гироскоп, освещённость и т.п. runtime-разрешений не требуют.

В браузере (paf run) методы возвращают правдоподобные dev-заглушки ({ "dev": true, ... }), на устройстве — реальные показания.


🧩 Модули устройства (Wi-Fi, Bluetooth, камера…)

Возможности устройства разбиты на модули. Каждый доступен:

  • из Python как app.<имя> (напр. app.wifi, app.bluetooth);
  • из JS как enpaf.<имя>.* (напр. enpaf.wifi.scan()).
Модуль Python / JS Что умеет
wifi app.wifi / enpaf.wifi состояние, скан сетей, подключение
bluetooth app.bluetooth / enpaf.bluetooth поиск, подключение, обмен сообщениями
location app.location / enpaf.location геопозиция
sensors app.sensors / enpaf.sensors акселерометр, гироскоп, свет…
nfc app.nfc / enpaf.nfc чтение/запись меток
audio app.audio / enpaf.audio уровень микрофона
battery app.battery / enpaf.battery заряд, сеть
notifications app.notifications системные уведомления
device app.device инфо, вибрация, буфер, шаринг
permissions app.permissions / enpaf.permissions проверка/запрос разрешений

В браузере (paf run) модули возвращают dev‑заглушки и симулируют события, поэтому всё кликается и тестируется без телефона.

Управление модулями из Python

Все модули — обычные объекты на app, поэтому Bluetooth/Wi-Fi/датчики можно запускать прямо из Pythonmain.py), а не только из JS:

# файл: main.py
@app.bridge_handler("scan_now")
def scan_now(params):
    wifi = app.wifi.scan_sync()["networks"]        # скан и сразу результат
    bt   = app.bluetooth.discover_sync()["devices"]
    return {"wifi": wifi, "bluetooth": bt}

# одиночные действия
app.wifi.connect(ssid="МояСеть", password="пароль")
app.bluetooth.send(text="привет")
loc = app.location.get()
gyro = app.sensors.gyroscope()

# Python тоже получает события модулей
@app.on("bluetooth_device_found")
def found(dev):
    print("найдено:", dev["name"], dev["address"])

Асинхронные операции (scan, discover) шлют события; для Python есть «синхронные» обёртки app.wifi.scan_sync() и app.bluetooth.discover_sync(), которые ждут и возвращают список (с таймаутом).

Wi-Fi — enpaf.wifi

// файл: app/js/app.js
const info = await enpaf.wifi.status();       // {enabled, ssid, rssi, ip, ...}
enpaf.wifi.onResult(n => console.log(n.ssid, n.rssi, n.secure));
enpaf.wifi.onFinished(() => console.log('скан завершён'));
await enpaf.wifi.scan();                       // события wifi_scan_result
await enpaf.wifi.connect('МояСеть', 'пароль'); // Android 10+ — через предложение сети
enpaf.wifi.enable();                           // откроет системную панель Wi-Fi

Bluetooth — enpaf.bluetooth (поиск, подключение, чат)

Классический Bluetooth (SPP): найти устройство, подключиться и обмениваться текстовыми сообщениями между двумя телефонами.

// файл: app/js/app.js
// 1) запросить разрешения и искать
await enpaf.permissions.request(['BLUETOOTH_SCAN', 'BLUETOOTH_CONNECT', 'FINE_LOCATION']);
enpaf.bluetooth.onFound(d => console.log(d.name, d.address, d.rssi));
await enpaf.bluetooth.discover();

// 2) одно устройство ждёт, второе подключается
await enpaf.bluetooth.listen('ENPAF');         // сервер
await enpaf.bluetooth.connect('AA:BB:CC:DD:EE:01');  // клиент

// 3) общаться
enpaf.bluetooth.onConnected(d => console.log('связь с', d.name, d.role));
enpaf.bluetooth.onData(d => console.log('получено:', d.text));
enpaf.bluetooth.send('Привет!');
enpaf.bluetooth.disconnect();

То же из Python: app.bluetooth.discover(), app.bluetooth.connect(address=...), app.bluetooth.send(text=...). Нужны разрешения BLUETOOTH_SCAN, BLUETOOTH_CONNECT (Android 12+), FINE_LOCATION (для поиска).

Камера — getUserMedia в WebView

Камера работает прямо в вебвью (живое превью + снимок), как в браузере. Нужно разрешение CAMERA (объявить в enpaf.json и запросить в рантайме). ENPAF сам выдаёт WebView доступ к камере. В paf run работает с веб‑камерой ноутбука.

// файл: app/js/app.js
async function startCamera() {
    if (enpaf.isAndroid) await enpaf.permissions.request(['CAMERA']);
    const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
    document.getElementById('cam').srcObject = stream;   // <video id="cam" autoplay playsinline>
}
// снимок: нарисовать кадр на <canvas> и взять canvas.toDataURL('image/png')

🏷 NFC: чтение и запись меток

ENPAF включает foreground dispatch: пока приложение открыто, поднесённая метка автоматически попадает в него (событие nfc_tag), а дальше её можно прочитать, перезаписать или заблокировать из Python или JS. Добавьте разрешение NFC в enpaf.json.

Чтение

// файл: app/js/app.js
enpaf.nfc.onTag(async (tag) => {                 // метку поднесли к телефону
    const data = await enpaf.nfc.read();
    // data.records: [{type:"text", text:"…"}] | [{type:"uri", uri:"…"}] | [{type:"raw", …}]
    console.log("ID:", data.id, "записи:", data.records);
});

Запись — все типы меток

⚠️ Важно: дескриптор метки «умирает», как только метку убрали от телефона. Поэтому надёжный способ — arm*-методы: они откладывают запись и выполняют её в момент следующего касания. Сначала нажмите запись → потом поднесите метку. Promise завершится результатом записи.

// файл: app/js/app.js  — РЕКОМЕНДУЕТСЯ (arm → поднести метку)
const r = await enpaf.nfc.armUri("https://example.com");  // ждёт касания метки
// r = {written: true, bytes: N} либо {written:false, note:"…"}
await enpaf.nfc.armText("Привет, NFC");
await enpaf.nfc.armWifi("МояСеть", "пароль123");
await enpaf.nfc.armApp("com.example.myapp");
await enpaf.nfc.armContact({ name: "Alex", phone: "+7999" });
await enpaf.nfc.armLock();                               // заблокировать след. метку

Прямые write*-методы (ниже) пишут в уже поднесённую метку — годятся внутри обработчика enpaf.nfc.onTag(...), когда метка точно в поле:

// файл: app/js/app.js
await enpaf.nfc.writeText("Привет, NFC");           // текст
await enpaf.nfc.writeUri("https://example.com");      // ссылка (URL)
await enpaf.nfc.writeUri("tel:+79991234567");       // телефон
await enpaf.nfc.writeUri("mailto:hi@example.com");  // email
await enpaf.nfc.writeUri("geo:55.75,37.61");        // координаты
await enpaf.nfc.writeApp("com.android.chrome");     // запуск приложения (AAR)
await enpaf.nfc.writeWifi("МояСеть", "пароль123");  // Wi-Fi (tap-to-connect)
await enpaf.nfc.writeContact({ name: "Alex", phone: "+7999", email: "a@b.c" }); // контакт (vCard)
await enpaf.nfc.writeMime("application/json", '{"id":42}');                      // MIME
// Несколько записей в одном сообщении:
await enpaf.nfc.writeRecords([
    { kind: "uri", uri: "https://example.com" },
    { kind: "app", package: "com.example.myapp" },
]);

То же из Python:

# файл: main.py
@app.bridge_handler("write_card")
def write_card(params):
    app.api.nfc_write_text("Привет")
    app.api.nfc_write_uri("https://example.com")
    app.api.nfc_write_app("com.example.myapp")
    app.api.nfc_write_wifi("МояСеть", "пароль123")
    return {"ok": True}
Метод (enpaf.nfc.* / app.api.*) Тип NDEF-записи
writeText(text) / nfc_write_text Текст (RTD_TEXT)
writeUri(uri) / nfc_write_uri URL / tel: / mailto: / geo: / sms:
writeApp(pkg) / nfc_write_app Запуск/установка приложения (AAR)
writeMime(mime,data) / nfc_write_mime MIME (json, vcard, …)
writeWifi(ssid,pass) / nfc_write_wifi Wi-Fi (vnd.wfa.wsc)
writeContact({…}) / nfc_write_contact Контакт (vCard)
writeRecords([…]) / nfc_write_records Любой набор записей
read() / nfc_read Прочитать содержимое метки
lock() / nfc_make_readonly Заблокировать метку навсегда (только чтение)

Пустые/неформатированные метки форматируются автоматически. lock() необратим — метку больше нельзя будет перезаписать.

Открытие URL‑метки при касании (вне приложения)

Когда приложение закрыто, поднесённую метку обрабатывает сама ОС, а не код ENPAF. Обычная URL‑метка (writeUri) корректна и открывается на большинстве устройств и на iPhone. Но поведение зависит от телефона:

  • На некоторых Android (например, Xiaomi / HyperOS) метка только считывается (вибрация), а браузер не открывается автоматически. Это настройка прошивки, а не содержимое метки. Проверьте: NFC включён, экран разблокирован, задан браузер по умолчанию, «безопасный элемент» NFC не перехватывается кошельком.
  • Чтобы открытие ссылки работало гарантированно на любом устройстве, запишите метку с записью запуска приложенияenpaf.nfc.writeApp(pkg, url) (или app.api.nfc_write_app). Тогда касание запускает ваше приложение, а оно само открывает URL (см. событие nfc_tag с from_launch).

🔓 Запрос разрешений во время работы (runtime)

«Опасные» разрешения (камера, геолокация, микрофон, контакты…) недостаточно объявить в enpaf.json — Android требует согласия пользователя в рантайме. ENPAF не показывает эти диалоги при запуске: вы вызываете их сами, в нужный момент — например, когда пользователь впервые жмёт «Записать аудио».

Из Python (main.py)

# файл: main.py

@app.bridge_handler("start_recording")
def start_recording(params):
    # Покажет системный диалог сейчас. Возвращает то, что уже выдано/запрошено.
    res = app.api.request_permissions(["RECORD_AUDIO"])
    return res        # {"requested":[...], "granted":[...], "pending": true|false}


# Итоговый ответ пользователя приходит сюда (и одновременно — в JS-событие):
@app.on("permission_result")
def on_permission_result(data):
    if "android.permission.RECORD_AUDIO" in data["granted"]:
        print("Микрофон разрешён — можно писать звук")
    else:
        print("Отклонено:", data["denied"])
Метод Python Назначение
app.api.request_permissions([...]) Показать системный диалог сейчас
app.api.check_permission("CAMERA") {granted: bool} — выдано ли одно
app.api.check_permissions([...]) Статус сразу нескольких

Имена можно писать коротко ("CAMERA", "FINE_LOCATION", "RECORD_AUDIO") или полностью ("android.permission.CAMERA").

Из JavaScript (app/js/app.js)

enpaf.permissions.request(...) возвращает Promise, который ждёт ответа пользователя и резолвится итогом — это самый удобный путь из интерфейса:

// файл: app/js/app.js
async function recordAudio() {
    const r = await enpaf.permissions.request(["RECORD_AUDIO"]);
    if (r.granted.includes("android.permission.RECORD_AUDIO")) {
        const level = await enpaf.sensors.audioLevel();
        console.log("Громкость:", level.db, "дБ");
    } else {
        enpaf.device.toast("Нужен доступ к микрофону");
    }
}

// Уже выдано?
const cam = await enpaf.permissions.check("CAMERA");      // {granted: bool}
const many = await enpaf.permissions.checkAll(["CAMERA", "RECORD_AUDIO"]);
JS-метод Назначение
enpaf.permissions.request(list) Диалог + Promise с результатом {granted, denied, results}
enpaf.permissions.check(name) {granted: bool}
enpaf.permissions.checkAll(list) {granted:[...], denied:[...]}

⚠️ Запрос разрешений работает только в собранном APK. Изменения в main.py применяются в новой сборке (paf build apk) — переустановите APK.


⚙️ Панель настроек ⚙

Запустите paf run и откройте http://127.0.0.1:8080/enpaf-settings (или нажмите плавающую кнопку в правом нижнем углу страницы). Панель позволяет настроить и сохранить в enpaf.json без ручного редактирования:

  • General — имя приложения, иконка (загрузка с превью), ориентация, основной цвет и цвет статус-бара;
  • Permissions — разрешения (<uses-permission>);
  • Hardware features — фичи устройства (<uses-feature>) с флагом «required»;
  • Deep Links — диплинки и App Links;
  • Manifest preview — живой предпросмотр того, что попадёт в AndroidManifest.xml.

После «Save» изменения применятся при следующей paf build.


🔐 Разрешения и фичи устройства

Разрешения

Указываются по коротким ключам в permissions (это лишь объявление в манифесте). Доступные ключи:

INTERNET, ACCESS_NETWORK_STATE, ACCESS_WIFI_STATE, VIBRATE, CAMERA, READ_STORAGE, WRITE_STORAGE, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO, FINE_LOCATION, COARSE_LOCATION, BACKGROUND_LOCATION, RECORD_AUDIO, BODY_SENSORS, ACTIVITY_RECOGNITION, READ_CONTACTS, CALL_PHONE, READ_PHONE_STATE, SEND_SMS, RECEIVE_SMS, BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_SCAN, BLUETOOTH_CONNECT, NFC, WAKE_LOCK, FOREGROUND_SERVICE, POST_NOTIFICATIONS.

Можно указать и полное имя, напр. android.permission.CAMERA.

🔓 «Опасные» разрешения (камера, геолокация, микрофон, контакты, Bluetooth-скан, уведомления…) надо ещё и запросить в рантаймеapp.api.request_permissions([...]) или enpaf.permissions.request([...]). См. Запрос разрешений во время работы.

Фичи (<uses-feature>)

Формат: { "key": "<KEY>", "required": true|false }. Доступные ключи:

CAMERA, CAMERA_FRONT, CAMERA_AUTOFOCUS, NFC, BLUETOOTH, BLUETOOTH_LE, GPS, LOCATION, MICROPHONE, WIFI, TELEPHONY, TOUCHSCREEN, FINGERPRINT, ACCELEROMETER, GYROSCOPE, COMPASS, PROXIMITY, LIGHT, BAROMETER, STEP_COUNTER, HEART_RATE.

required: false оставляет приложение устанавливаемым на устройствах без соответствующего железа.


🔗 Deep links (диплинки)

Каждый диплинк превращается в <intent-filter> на главной активности.

"deeplinks": [
    { "label": "Профиль", "scheme": "myapp", "host": "open",
      "path": "/profile", "pathType": "prefix", "autoVerify": false }
]
Поле Описание
scheme Обязательно. Напр. myapp или https
host Опционально. Напр. example.com
path Опционально. Напр. /profile
pathType path (точно) / prefix / pattern
autoVerify true для App Links (проверяемые https-ссылки)
label Только для UI-панели

Проверить диплинк на устройстве:

adb shell am start -a android.intent.action.VIEW -d "myapp://open/profile"

🔔 Уведомления и нативные возможности

Базовые уведомления (JavaScript)

Из JavaScript:

enpaf.device.notify("Заголовок", "Текст уведомления", 1);

Rich-уведомления (Python)

Из Python можно отправлять мощные системные уведомления с картинками и кнопками:

import base64

with open("app/img/logo.png", "rb") as f:
    img_b64 = base64.b64encode(f.read()).decode("utf-8")

app.api.notify(
    title="Новое сообщение",
    text="Привет!",
    notification_id=1,
    image_base64=img_b64,     # Показывает большую картинку
    action="open_chat",       # Передается при клике на уведомление
    payload="user_123",
    buttons=[
        {"text": "Ответить", "action": "reply"},
        {"text": "Закрыть", "action": "close"}
    ]
)

Нажатие на само уведомление или любую его кнопку автоматически разбудит/откроет приложение и сгенерирует событие в Python:

@app.on("notification_click")
def on_notif_click(data):
    print(f"Action: {data.get('action')}, Payload: {data.get('payload')}")

Важно: На Android 13+ приложение запрашивает разрешение POST_NOTIFICATIONS (добавьте его в permissions в вашем enpaf.json).

Доступные нативные методы моста (вызываются через enpaf.device.* в JS или app.api.* в Python): toast, vibrate, notify, share, setOrientation, clipboard, openUrl.


📦 Сборка APK

paf build apk            # debug
paf build apk --release  # release
paf build aab            # release bundle (.aab)

Что происходит:

  1. Проверяется окружение (Python/JDK/Android SDK).
  2. Подбирается совместимая JDK 17–21 (учитывается JAVA_HOME; иначе ищется в стандартных местах, включая JBR из Android Studio).
  3. Генерируется Gradle-проект (Chaquopy), скачивается официальный Gradle wrapper.
  4. Gradle собирает APK; результат копируется в dist/<name>-<version>.apk.

Первая сборка долгая — Gradle докачивает Android platform/build-tools и NDK (~1 ГБ) для Chaquopy. Дальнейшие сборки быстрее (всё кэшируется в ~/.gradle и Android SDK).

Подпись release-сборки

Android не устанавливает неподписанные release-APK. ENPAF подписывает их автоматически: при первой paf build apk --release создаётся keystore ~/.enpaf/keystores/<package>.jks (общий для всех последующих сборок этого пакета — подпись стабильна, обновления ставятся поверх). Release собирается без обфускации (minifyEnabled false), чтобы R8 не вырезал мост @JavascriptInterface и классы Chaquopy.

Свой keystore (для публикации в Google Play) — через --keystore или блок signing в enpaf.json:

"signing": {
    "keystore": "release.jks",
    "store_password": "••••••",
    "key_alias": "myapp",
    "key_password": "••••••"
}
paf build apk --release --keystore release.jks

OneDrive / облачные папки

Если проект лежит в OneDrive (или другой синхронизируемой папке), ENPAF автоматически выносит каталог сборки в %LOCALAPPDATA%\enpaf\builds\…, потому что синхронизация ломает удаление файлов Gradle. Путь можно переопределить переменной окружения ENPAF_BUILD_DIR. Итоговый APK всё равно копируется в dist/.


🧪 Тесты и CI

В репозитории есть набор тестов на pytest (покрывают dev-режим: хранилище, события, мост, роутер, DeviceAPI, модули, помощники сборки, CLI):

pip install -e ".[test]"   # пакет + pytest
pytest                      # запустить все тесты

GitHub Actions (.github/workflows/):

  • CI (ci.yml) — на каждый push в main и каждый PR: матрица Python 3.9–3.13 × Ubuntu + Windows, прогон pytest, сборка и проверка дистрибутива (python -m build + twine check).
  • Release (release.yml) — при пуше тега v* собирает wheel/sdist и прикрепляет их к GitHub Release.
git tag v1.2.0 && git push origin v1.2.0   # соберёт и опубликует дистрибутив

APK в CI не собираются (нужны Android SDK + Chaquopy) — собирайте их локально и прикрепляйте к релизу. Подробнее: Wiki → Testing & CI.


📥 Что можно импортировать

Основное (рекомендуется):

from enpaf import EnpafApp, __version__

Продвинутое (обычно используется через app.*, но доступно напрямую):

from enpaf.core.storage import Storage, Collection
from enpaf.core.events import EventEmitter
from enpaf.core.bridge import Bridge
from enpaf.core.router import Router
from enpaf.core.api import DeviceAPI

# Справочники для манифеста / панели настроек
from enpaf.android.permissions import PERMISSIONS, get_permission_catalog
from enpaf.android.features import FEATURES, get_feature_catalog
from enpaf.android.deeplinks import get_deeplink_xml

# Программная сборка APK
from enpaf.builder.apk_builder import APKBuilder

# Точка входа CLI
from enpaf.cli.main import main

Клиентский SDK для подключения вручную (если не используете авто-инъекцию):

<script src="js/enpaf.js"></script>

🩺 Решение проблем

Симптом Причина и решение
Приложение сразу закрывается Пересоберите APK — фиксы применяются только в новой сборке. Если повторяется — снимите лог: adb logcat (теги AndroidRuntime, python.stderr, chaquopy).
Bridge call failed / Unexpected token '<' в paf run Старый билд enpaf: HTTP-фолбэк моста и Socket.IO-клиент чинятся в свежей версии. Обновите пакет (pip install -U enpaf или pip install -e . из исходников) и перезапустите paf run.
enpaf is not defined Мост не подключён. Сборка вставляет js/enpaf.js автоматически; убедитесь, что собираете свежую версию.
Датчик/геолокация возвращает {"dev": true} Это нормально в браузере (paf run) — реальные показания доступны только в APK на устройстве.
request_permissions ничего не показывает Работает только в APK и только для «опасных» разрешений, которые объявлены в enpaf.json. Проверьте, что разрешение есть в permissions.
Синий/неожиданный цвет статус-бара Это theme.status_bar_color. Поменяйте его в панели ⚙ → General или в enpaf.json.
Unable to delete directory … python\sources OneDrive блокирует файлы. ENPAF собирает вне OneDrive автоматически; при желании задайте ENPAF_BUILD_DIR.
Incompatible Java version Нужна JDK 17–21. Установите JDK 17 и задайте JAVA_HOME, либо дайте ENPAF найти её автоматически.
release-APK не устанавливается Неподписанный release Android отклоняет. ENPAF подписывает release автоматически (keystore в ~/.enpaf/keystores/). Обновите пакет и пересоберите paf build apk --release. Если меняли keystore — сначала удалите старую версию приложения (конфликт подписи).
keytool not found при release keystore создаётся через keytool из JDK. Убедитесь, что JDK 17 установлен и найден (paf doctor).
URL‑метка не открывается на Xiaomi (только вибрация) Содержимое метки корректно (на iPhone открывается) — это поведение HyperOS. Проверьте NFC, разблокировку экрана и браузер по умолчанию, либо записывайте метку через enpaf.nfc.writeApp(pkg, url) — тогда касание запустит приложение, и оно откроет ссылку.
NFC‑запись «не доходит» до метки Дескриптор метки умирает, когда её убрали от телефона. Используйте enpaf.nfc.armText/armUri/... — сначала вызов, потом касание.
'""' is not recognized при Gradle Старый сломанный wrapper. Удалите каталог сборки и соберите заново (paf build apk --clean).

Полная диагностика окружения: paf doctor.


📚 Документация (Wiki)

Подробная документация ведётся в GitHub Wiki:


📄 Лицензия

ENPAF распространяется по лицензии PolyForm Noncommercial License 1.0.0 — см. LICENSE.

  • Бесплатно для некоммерческого использования — личные проекты, исследования, обучение, некоммерческие организации, госучреждения.
  • 💼 Коммерческое использование — платное: требуется отдельная коммерческая лицензия. По вопросам её получения свяжитесь с автором.

Это source-available лицензия, а не OSI open-source. Подробности — Wiki → License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

enpaf-1.1.3.tar.gz (189.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

enpaf-1.1.3-py3-none-any.whl (154.6 kB view details)

Uploaded Python 3

File details

Details for the file enpaf-1.1.3.tar.gz.

File metadata

  • Download URL: enpaf-1.1.3.tar.gz
  • Upload date:
  • Size: 189.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for enpaf-1.1.3.tar.gz
Algorithm Hash digest
SHA256 c6b2ca3f67ed3a28dd2e9a445f2ecaed280996227a31a69030ba529f227a283b
MD5 209d888232fdf72a25898c3c33121887
BLAKE2b-256 2e13faab380ca99467d643a31a825bf30357fcf5f81091a396c391b36afd4c38

See more details on using hashes here.

File details

Details for the file enpaf-1.1.3-py3-none-any.whl.

File metadata

  • Download URL: enpaf-1.1.3-py3-none-any.whl
  • Upload date:
  • Size: 154.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for enpaf-1.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 a8d5d302f574bbc69fe5fa727177ebcad3b0d479bc4fd7ef13b48cdc4672e246
MD5 8899081e90a91ed28626feebc4b8c230
BLAKE2b-256 eaee7298739c0652664d77e2014314a189abf5ba44bab18106fe99721be1d4b6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page