Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,9 @@ docs/mobile-app-design.md
inksight_tech/
lab/
.cursor/

# Claude Code local session/worktree state
.claude/

# Firmware build output
firmware.bin
11 changes: 11 additions & 0 deletions backend-lite/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# backend-lite 环境变量

# LLM key 加密密钥(core/crypto.py 用)。首次启动可用 `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` 生成。
LLM_ENCRYPTION_KEY=

# 服务监听(仅在使用 `python run.py` 启动时生效)
LITE_HOST=0.0.0.0
LITE_PORT=8090

# 上游 backend/ 目录的绝对路径(默认同级 ../backend)。仅在非标准布局下需要覆盖。
INKSIGHT_BACKEND_DIR=
21 changes: 21 additions & 0 deletions backend-lite/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 运行时数据库
*.db
*.db-shm
*.db-wal

# 虚拟环境
.venv/
venv/

# OTA 上传的固件
ota_files/

# 环境
.env

# 字体(由 setup_fonts.py 下载,不入库)
fonts/

# Python 缓存
__pycache__/
*.pyc
129 changes: 129 additions & 0 deletions backend-lite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# InkSight backend-lite

InkSight 的轻量单用户后端。直接复用上游 `backend/core/` 渲染内核(保持上游可同步),
自己只维护薄路由 + 单用户 store + 单页管理页。100% 兼容现有固件协议。

## 特性

- 单用户、单设备,无账户/共享/配额/analytics 等多用户平台重量。
- 兼容固件核心链路:render / token / heartbeat / config / state / runtime / refresh。
- 支持焦点提醒 alert-bmp、始终活跃 always_active、专注监听 focus_listening。
- 支持 OTA 固件更新(后端下发 ota_url + 固件 state 解析器补丁)。
- 管理页:单静态 HTML,配置自己的 LLM API key(加密存 DB)、选模式、预览、远程刷新。
- 复用上游全部 30 个内置模式;自定义模式直接放 `backend/core/modes/custom/` 即可。
- 渲染固定 400×300 / 2bpp / 4 色(黑白红黄)。

不支持(固件遇非 200 自动降级):vocab 词汇复习、voice 语音对话、mode marketplace、自定义模式编辑器 UI。

## 管理页截图

### 管理页总览

![backend-lite 管理页总览](imgs/1.png)

### 设备配置与预览

![backend-lite 设备配置与预览](imgs/2.png)

### 焦点提醒、OTA 与设备控制

![backend-lite 焦点提醒、OTA 与设备控制](imgs/3.png)

## 与上游的关系

```
backend/core/ ← 上游原样,git pull 即同步渲染/模式/LLM 逻辑
backend-lite/ ← 本目录,只依赖 core/ 的公开 API
api/ 薄路由(固件端点 + 管理端点)
adaptee/ 渲染适配 + 上游 DB 路径重定向
store/ 单用户 SQLite(lite.db)
static/ 单页管理 UI
```

`backend-lite/adaptee/db_redirect.py` 在启动时把上游 `core/` 的 `inksight.db`/`cache.db`
路径重定向到本目录,避免污染上游 `backend/`,并调上游 `init_stats_db()`/`init_db()`
建 `content_history` 等表(LLM 去重提示需要)。

## 安装

```bash
cd backend-lite
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
# 上游 core/ 运行时依赖(zhdate/dashscope/tenacity 等)
pip install -r ../backend/requirements.txt

# 字体(渲染需要,~70MB,首次必做)
python ../backend/scripts/setup_fonts.py

# 配置环境
cp .env.example .env
# 生成加密密钥填入 LLM_ENCRYPTION_KEY:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```

## 运行

推荐使用本目录自带启动入口,它会读取 `.env` 中的 `LITE_HOST` / `LITE_PORT`:

```bash
. .venv/bin/activate
python run.py
```

默认监听 `0.0.0.0:8090`;如果你修改了 `.env`,例如:

```env
LITE_HOST=0.0.0.0
LITE_PORT=21568
```

那么重新运行 `python run.py` 后就会监听在对应地址。

> 如果你直接运行 `uvicorn api.index:app --host ... --port ...`,命令行参数会覆盖 `.env`,这也是之前看起来“改了 `.env` 不生效”的原因。

管理页:浏览器打开 `http://<本机IP>:<LITE_PORT>/`(局域网免认证)。

## 配置流程

1. 管理页「LLM API Key」填 provider + key(deepseek/aliyun/moonshot/openai_compat),保存。
2. 「设备配置」选模式、设刷新策略/间隔/城市,保存。
3. 固件 captive portal 里把后端地址填成 `http://<本机IP>:8090`。
4. 设备开机即取图。开「始终活跃」可远程触发刷新 / 切模式。

## 固件 OTA(可选)

本目录附带固件补丁(`firmware/src/network.cpp` + `main.cpp`):
- `network.cpp` 的 `/state` 解析器新增提取 `ota_url`/`ota_version` 赋给 `g_pending_ota_*`。
- `main.cpp` 主循环新增 `checkAndPerformOTA()` 调用(上游机器已实现,原本缺触发)。

需用 PlatformIO 重新编译刷写固件:

```bash
cd firmware
pio run --target upload
```

之后管理页「OTA」上传 bin → 设备下次轮询 `/state` 自动下载刷写 → `/ota/progress` 上报进度。

## 端点速查

固件(设备调用):
- `POST /api/device/{mac}/token` · `POST /api/device/{mac}/heartbeat`
- `GET /api/render` · `GET /api/config/{mac}` · `GET /api/device/{mac}/state`
- `POST /api/config` · `POST /api/device/{mac}/runtime` · `POST /api/device/{mac}/refresh`
- `GET /api/device/{mac}/alert-bmp` · `POST /api/device/{mac}/ota/progress`
- `POST /api/device/{mac}/claim-token`

管理页:
- `GET/PUT /api/admin/config` · `GET /api/admin/modes` · `GET/PUT /api/admin/llm-key`
- `GET /api/admin/state` · `POST /api/admin/refresh` · `POST /api/admin/set-mode` · `POST /api/admin/runtime`
- `GET/PUT /api/admin/alert` · `POST /api/admin/ota/upload` · `POST /api/admin/ota/set` · `GET /api/admin/ota/file/{name}`
- `GET /api/preview?mode=&as_png=1`

## 数据库

- `lite.db` — backend-lite 自有:config / device_state / llm_key / heartbeats / alert_state。
- `inksight.db` — 上游 core 运行时表(content_history 去重等),重定向到本目录。
- `cache.db` — 上游渲染缓存(重定向到本目录)。
1 change: 1 addition & 0 deletions backend-lite/adaptee/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""backend-lite adaptee 包标记。"""
28 changes: 28 additions & 0 deletions backend-lite/adaptee/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""适配 core.context:取 date_ctx + weather。

单用户:城市取自 config(或经纬度),直接调 core 的缓存版本。
"""
from __future__ import annotations

from typing import Any

from core.context import get_date_context_cached, get_weather_cached


async def build_context(config: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
"""返回 (date_ctx, weather)。失败降级到空 dict / 默认天气。"""
date_ctx = await get_date_context_cached()

city = config.get("city") or ""
lat = config.get("latitude")
lon = config.get("longitude")
weather: dict[str, Any]
try:
if lat is not None and lon is not None:
from core.context import get_weather
weather = await get_weather(lat=float(lat), lon=float(lon))
else:
weather = await get_weather_cached(city=city or None)
except Exception:
weather = {"temp": 0, "weather_code": -1, "weather_str": "--°C"}
return date_ctx, weather
48 changes: 48 additions & 0 deletions backend-lite/adaptee/db_redirect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""把上游 core/ 的 SQLite 路径重定向到 backend-lite 目录,避免污染上游 backend/。

core 运行时(json_content/pipeline)会 lazy 调用 stats_store / config_store /
cache 的函数,它们读写 backend/inksight.db 和 backend/cache.db。这里把这些
模块级路径常量改写为 backend-lite 目录下的副本,并调上游建表函数初始化。

必须在任何 core DB 调用前调用 redirect_db_paths()。
"""
from __future__ import annotations

import os
from pathlib import Path

_LITE_DIR = Path(__file__).resolve().parent.parent
_INKSIGHT_DB = str(_LITE_DIR / "inksight.db")
_CACHE_DB = str(_LITE_DIR / "cache.db")


def redirect_db_paths() -> None:
"""改写 core 各模块的 DB 路径常量,指向 backend-lite 目录。"""
import core.db as cdb
import core.stats_store as stats
import core.config_store as cstore
import core.cache as cache

cdb._MAIN_DB_PATH = _INKSIGHT_DB
cdb._CACHE_DB_PATH = _CACHE_DB
stats.DB_PATH = _INKSIGHT_DB
cstore.DB_PATH = _INKSIGHT_DB
# cache 模块若用 _CACHE_DB_PATH 常量
if hasattr(cache, "_CACHE_DB_PATH"):
cache._CACHE_DB_PATH = _CACHE_DB
if hasattr(cache, "CACHE_DB_PATH"):
cache.CACHE_DB_PATH = _CACHE_DB


async def init_upstream_tables() -> None:
"""调用上游建表函数(建在重定向后的 backend-lite 库里)。

只建 core 运行时真正需要的表:stats_store(content_history 去重、
device_heartbeats 在线判断)、config_store(photo_frame_index 等)。
多用户相关表也会被 config_store.init_db 创建,空着不用,无害。
"""
from core.stats_store import init_stats_db
from core.config_store import init_db as config_init_db

await init_stats_db()
await config_init_db()
Loading
Loading