ENPAF — Python + Web framework for building Android APK applications
Project description
🚀 ENPAF — Engine for Native Python App Framework
Создавайте 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 │ │ ваш код │ │
│ └────────────┘ └──────────────────── │
└─────────────────────────────────────────────────────────────┘
📑 Содержание
- Возможности
- Установка
- Требования
- Быстрый старт
- Создание базового приложения с нуля
- CLI: команда
paf - Структура проекта
- Конфигурация
enpaf.json - Python API
- JavaScript SDK (
enpaf.js) - Датчики и сенсоры устройства
- Модули устройства (Wi-Fi, Bluetooth, камера…)
- NFC: чтение и запись меток
- Запрос разрешений во время работы (runtime)
- Панель настроек ⚙
- Разрешения и фичи устройства
- Deep links (диплинки)
- Уведомления и нативные возможности
- Сборка APK
- Тесты и CI
- Что можно импортировать
- Решение проблем
- Документация (Wiki)
- Лицензия
✨ Возможности
- 🐍 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/датчики можно
запускать прямо из Python (в main.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)
Что происходит:
- Проверяется окружение (Python/JDK/Android SDK).
- Подбирается совместимая JDK 17–21 (учитывается
JAVA_HOME; иначе ищется в стандартных местах, включая JBR из Android Studio). - Генерируется Gradle-проект (Chaquopy), скачивается официальный Gradle wrapper.
- 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:
- Home · Installation · Quick Start
- CLI Reference · enpaf.json · Project Structure
- Python API · JavaScript Bridge · Storage · Events
- Device Capabilities · Building APKs · Release & Signing
- Companion App · Architecture · Testing & CI · Troubleshooting
📄 Лицензия
ENPAF распространяется по лицензии PolyForm Noncommercial License 1.0.0 — см. LICENSE.
- ✅ Бесплатно для некоммерческого использования — личные проекты, исследования, обучение, некоммерческие организации, госучреждения.
- 💼 Коммерческое использование — платное: требуется отдельная коммерческая лицензия. По вопросам её получения свяжитесь с автором.
Это source-available лицензия, а не OSI open-source. Подробности — Wiki → License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6b2ca3f67ed3a28dd2e9a445f2ecaed280996227a31a69030ba529f227a283b
|
|
| MD5 |
209d888232fdf72a25898c3c33121887
|
|
| BLAKE2b-256 |
2e13faab380ca99467d643a31a825bf30357fcf5f81091a396c391b36afd4c38
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8d5d302f574bbc69fe5fa727177ebcad3b0d479bc4fd7ef13b48cdc4672e246
|
|
| MD5 |
8899081e90a91ed28626feebc4b8c230
|
|
| BLAKE2b-256 |
eaee7298739c0652664d77e2014314a189abf5ba44bab18106fe99721be1d4b6
|