Compare commits
No commits in common. "3a5856c7f074f57a529065679cfdf9933f6ccde99f27c4d2d68fdf25ddd56134" and "4b371e142d32e40e284b99a009ab55bfd8139cea8ea76683bd241464f4ad3cc2" have entirely different histories.
3a5856c7f0
...
4b371e142d
38
.gitignore
vendored
38
.gitignore
vendored
@ -50,41 +50,3 @@ bin/
|
||||
|
||||
# временный debug token
|
||||
.debug-token
|
||||
|
||||
# Локальные артефакты и секреты Solana-модуля
|
||||
shine-solana/.git/
|
||||
shine-solana/.git-local-backup/
|
||||
shine-solana/.idea/
|
||||
shine-solana/shine/.idea/
|
||||
shine-solana/shine/.gradle/
|
||||
shine-solana/shine/.anchor/
|
||||
shine-solana/shine/.yarn/
|
||||
shine-solana/shine/.vendor/
|
||||
shine-solana/shine/node_modules/
|
||||
shine-solana/shine/target/
|
||||
shine-solana/shine/test-ledger/
|
||||
shine-solana/shine/old_vers/
|
||||
shine-solana/shine/program-keypair.json
|
||||
shine-solana/shine/keys/
|
||||
shine-solana/shine/validator.log
|
||||
shine-solana/shine/doc/КОШЕЛЬКИ_DEVNET_ТЕСТ.md
|
||||
shine-solana/shine/scripts/del/
|
||||
shine-solana/shine/scripts/**/keypairs/
|
||||
shine-solana/shine/scripts/**/runs/
|
||||
shine-solana/shine/scripts/**/*.env
|
||||
shine-solana/shine/scripts/**/TEMP_*.md
|
||||
|
||||
# Локальные артефакты и внешние материалы ESP32-подпроекта
|
||||
ESP32/**/.git/
|
||||
ESP32/**/.idea/
|
||||
ESP32/**/.arduino-build/
|
||||
ESP32/**/official-demo/
|
||||
ESP32/**/original-firmware/*.bin
|
||||
ESP32/**/original-firmware/*.bin.sha256
|
||||
ESP32/**/*.elf
|
||||
ESP32/**/*.map
|
||||
ESP32/**/*.merged.bin
|
||||
ESP32/**/*.uf2
|
||||
ESP32/**/*.o
|
||||
ESP32/**/*.d
|
||||
ESP32/**/*.a
|
||||
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@ -2,7 +2,5 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/shine-solana" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
33
AGENTS.md
33
AGENTS.md
@ -11,23 +11,7 @@
|
||||
## Сервис агента-кодера
|
||||
- В проекте есть локальный Telegram-бот-сервис агента-кодера в папке `SHiNE-agent-bot-coder/`.
|
||||
- Сервис принимает сообщения из Telegram, ведёт историю диалога, ставит задачи в очередь и вызывает Codex CLI для обработки запросов по проекту.
|
||||
- Автоматически читаемые инструкции для Codex внутри сервиса держать в `SHiNE-agent-bot-coder/AGENTS.md`.
|
||||
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
|
||||
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
|
||||
|
||||
## Solana-модуль
|
||||
- В проекте есть отдельный Solana/Anchor-модуль в папке `shine-solana/shine/`.
|
||||
- Модуль логически связан с SHiNE, но не должен автоматически подключаться к сборке или деплою основного сервера без отдельного решения.
|
||||
- В Solana-модуле действуют локальные инструкции `shine-solana/shine/AGENTS.md`; при изменениях внутри модуля сначала читать их.
|
||||
- В git добавлять исходники, lock-файлы, настройки проекта и документацию Solana-модуля, но не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
|
||||
- Для Solana deploy/push использовать правила из локального `shine-solana/shine/AGENTS.md`; не смешивать deploy Solana-модуля с `deployServer`/`deployUI` основного проекта.
|
||||
- Для регистрации пользователей в Solana (программа `shine_users`) единая актуальная инструкция по деплою/инициализации, адресам программ, и куда их прописывать в UI/сервере находится в:
|
||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
||||
- Этот файл считать основной справкой (single source of truth) по деплою и первичной инициализации Solana-регистрации в текущем проекте.
|
||||
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
|
||||
- `Dev_Docs/Solana_Architecture/README.md`
|
||||
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
||||
- `shine-solana/shine/doc/SHiNE-user-format-v.1.0.md`
|
||||
- Подробные правила работы сервиса, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
|
||||
|
||||
## Документация блокчейна
|
||||
- Актуальная документация по форматам блокчейна находится в `Dev_Docs/Blockchain/README.md`.
|
||||
@ -63,14 +47,13 @@
|
||||
- `client.version` — версия клиентского UI.
|
||||
- `server.version` — версия серверной части.
|
||||
- Базовое правило инкремента: `+1` по последнему числовому сегменту (patch), если не оговорено иное.
|
||||
- Обычные коммиты делать стандартным `git commit`; переменная `$GITEA_TOKEN` для коммитов не нужна и не используется.
|
||||
|
||||
## Deploy
|
||||
- Все документы и заметки по деплою хранить в папке `Dev_Docs/deploy/`.
|
||||
- Базовый целевой хост для деплоя по умолчанию: `player@93.170.12.154` (`shineup.me`).
|
||||
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`).
|
||||
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
|
||||
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
|
||||
- Для операций `git push` использовать токен из переменной окружения `$GITEA_TOKEN`.
|
||||
- Для серверного деплоя использовать один gradle task: `./gradlew deployServer`.
|
||||
- Для UI деплоя использовать один gradle task: `./gradlew deployUI`.
|
||||
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
|
||||
@ -106,18 +89,6 @@
|
||||
- После подтверждения, что фича проверена и работает корректно, соответствующий файл удалять.
|
||||
- В `Dev_Docs/Pending_Features/README.md` вести краткий регламент и поддерживать актуальность.
|
||||
|
||||
## Будущие фичи
|
||||
- Папка для задач, сознательно отложенных на будущее: `Dev_Docs/Future_Features/`.
|
||||
- Точка входа по планам: `Dev_Docs/Future_Features/README.md`.
|
||||
- Внутри планы разделены по горизонтам: `near/`, `medium/`, `far/`.
|
||||
- Если пользователь спрашивает, какие есть планы или что можно продолжить, сначала читать `Dev_Docs/Future_Features/README.md`, затем при необходимости конкретные файлы из горизонтов.
|
||||
- Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя.
|
||||
- Если часть кода временно отключена или закомментирована, в файле будущей фичи подробно описывать:
|
||||
- какие файлы и участки отключены;
|
||||
- что осталось в коде как заготовка;
|
||||
- какие документы нужно обновить при возврате;
|
||||
- с какого сценария продолжать разработку.
|
||||
|
||||
## Коммуникация по новым задачам (обязательно)
|
||||
- При получении нового задания сначала кратко пересказать задачу своими словами.
|
||||
- До начала реализации задать недостающие уточняющие вопросы (если они есть).
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
# API для разработчиков: Регистрация пользователя
|
||||
|
||||
Этот файл описывает раздел API, связанный с проверкой наличия пользователя на сервере и dev/test операциями.
|
||||
Этот файл описывает временный раздел API, связанный с заведением пользователя на сервере и проверкой, существует ли пользователь.
|
||||
|
||||
Сейчас здесь три метода:
|
||||
|
||||
- `AddUser` — операция отключена (регистрация только через Solana);
|
||||
- `AddUser` — временная серверная регистрация пользователя;
|
||||
- `GetUser` — временная серверная проверка существования пользователя и чтение его базовых данных;
|
||||
- `SearchUsers` — dev/test поиск логинов по префиксу.
|
||||
|
||||
Регистрация выполняется через Solana (`shine_users`). Сервер при входе может лениво импортировать пользователя из Solana PDA в локальную БД, если записи ещё нет.
|
||||
Их логика пока вспомогательная и dev-oriented: сервер сам хранит эти данные локально и сам отвечает на existence-check. В будущем оба сценария должны быть заменены на нормальную работу напрямую через Solana, но пока этот контракт нужен клиентам для разработки и интеграции.
|
||||
|
||||
## Статус документа
|
||||
|
||||
@ -22,7 +22,12 @@
|
||||
|
||||
### Назначение
|
||||
|
||||
Операция отключена. Используется только как явный ответ клиентам старых версий.
|
||||
Временная регистрация локального пользователя на сервере.
|
||||
|
||||
Сервер:
|
||||
|
||||
- создаёт запись в `solana_users`;
|
||||
- создаёт стартовое состояние в `blockchain_state`.
|
||||
|
||||
### Запрос
|
||||
|
||||
@ -41,16 +46,29 @@
|
||||
}
|
||||
```
|
||||
|
||||
### Пример ответа
|
||||
### Успешный ответ
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "AddUser",
|
||||
"requestId": "reg-001",
|
||||
"status": 410,
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"payload": {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Пример ошибки
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "AddUser",
|
||||
"requestId": "reg-001",
|
||||
"status": 409,
|
||||
"ok": false,
|
||||
"error": "ADD_USER_DISABLED",
|
||||
"message": "Серверная регистрация AddUser отключена. Используйте регистрацию через Solana.",
|
||||
"error": "USER_ALREADY_EXISTS",
|
||||
"message": "Пользователь с таким login уже существует",
|
||||
"payload": {
|
||||
}
|
||||
}
|
||||
@ -58,7 +76,14 @@
|
||||
|
||||
### Специфические коды ошибок `AddUser`
|
||||
|
||||
- `410 / ADD_USER_DISABLED` — серверная регистрация отключена, используйте Solana-first flow.
|
||||
- `400 / BAD_FIELDS` — не переданы обязательные поля регистрации.
|
||||
- `400 / BAD_BLOCKCHAIN_NAME` — `blockchainName` не соответствует формату `<login>-NNN`.
|
||||
- `400 / BAD_KEY_FORMAT` — один из ключей не является корректным `Base64(32 bytes)`.
|
||||
- `409 / USER_ALREADY_EXISTS` — пользователь с таким `login` уже есть.
|
||||
- `409 / BLOCKCHAIN_ALREADY_EXISTS` — такой `blockchainName` уже занят.
|
||||
- `409 / BLOCKCHAIN_STATE_ALREADY_EXISTS` — стартовое состояние blockchain уже существует.
|
||||
- `501 / DB_ERROR` — ошибка БД при создании пользователя.
|
||||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||
|
||||
---
|
||||
|
||||
@ -70,8 +95,9 @@
|
||||
|
||||
Важно:
|
||||
|
||||
- это server-side existence-check;
|
||||
- если пользователя нет в локальной БД, он может быть импортирован при авторизации из Solana PDA.
|
||||
- это временное решение;
|
||||
- позже клиент должен проверять existence/identity напрямую через Solana;
|
||||
- на финальный production flow не стоит жёстко завязывать архитектуру клиента на `GetUser`.
|
||||
|
||||
### Запрос
|
||||
|
||||
@ -99,24 +125,11 @@
|
||||
"blockchainName": "anya-001",
|
||||
"solanaKey": "BASE64_32_PUBLIC_KEY",
|
||||
"blockchainKey": "BASE64_32_PUBLIC_KEY",
|
||||
"deviceKey": "BASE64_32_PUBLIC_KEY",
|
||||
"serverLastGlobalNumber": 128,
|
||||
"serverLastGlobalHash": "4f...ab",
|
||||
"serverBlockchainSizeBytes": 45212,
|
||||
"serverBlockchainSizeLimitBytes": 100000,
|
||||
"serverBlocksCount": 129
|
||||
"deviceKey": "BASE64_32_PUBLIC_KEY"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Дополнительные серверные поля в `GetUser`:
|
||||
|
||||
- `serverLastGlobalNumber` — номер последнего блока в пользовательском блокчейне на сервере;
|
||||
- `serverLastGlobalHash` — hash последнего блока (hex-строка 64 символа);
|
||||
- `serverBlockchainSizeBytes` — текущий размер пользовательского блокчейна на сервере в байтах;
|
||||
- `serverBlockchainSizeLimitBytes` — текущий лимит размера блокчейна на сервере в байтах;
|
||||
- `serverBlocksCount` — количество блоков в пользовательском блокчейне на сервере;
|
||||
|
||||
### Успешный ответ: пользователя нет
|
||||
|
||||
```json
|
||||
@ -196,7 +209,7 @@
|
||||
|
||||
## 4. Короткое резюме
|
||||
|
||||
- `AddUser` — отключен (`410 / ADD_USER_DISABLED`).
|
||||
- `GetUser` — проверка существования пользователя на сервере.
|
||||
- `AddUser` — временная регистрация пользователя на сервере.
|
||||
- `GetUser` — временная проверка существования пользователя на сервере.
|
||||
- `SearchUsers` — временный поиск пользователей по префиксу.
|
||||
- Регистрация выполняется только через Solana.
|
||||
- И регистрация, и existence-check позже должны быть переведены на Solana.
|
||||
|
||||
@ -73,7 +73,6 @@ ed25519/BASE64_PUBLIC_KEY
|
||||
- `400 / EMPTY_LOGIN` — пустой `login`.
|
||||
- `400 / ALREADY_AUTHED` — по текущему соединению уже выполнена авторизация.
|
||||
- `422 / UNKNOWN_USER` — пользователь с таким `login` не найден.
|
||||
- `501 / SOLANA_IMPORT_FAILED` — сервер не смог проверить/импортировать пользователя из Solana при lazy-import.
|
||||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера, если появится вне штатного сценария.
|
||||
|
||||
---
|
||||
|
||||
@ -81,12 +81,11 @@
|
||||
- `bad_signature`, `signature_verify_failed`
|
||||
- `prev_line_block_not_found`, `bad_prev_line_hash`
|
||||
- `limit_exceeded`
|
||||
- `repost_disabled` — репосты временно отключены до будущей реализации
|
||||
- `internal_error`
|
||||
|
||||
## 5. Какие блоки реально можно добавлять через `AddBlock`
|
||||
|
||||
Через `AddBlock` можно писать поддержанные форматы, кроме явно отключённых временных фич:
|
||||
Через `AddBlock` можно писать все поддержанные форматы:
|
||||
|
||||
1. **TECH (type=0)**
|
||||
- `HEADER_COMPAT (subType=0)`
|
||||
@ -97,7 +96,7 @@
|
||||
- `TEXT_EDIT_POST (11)`
|
||||
- `TEXT_REPLY (20)`
|
||||
- `TEXT_EDIT_REPLY (21)`
|
||||
- `TEXT_REPOST (30)` — формат зарезервирован, но новые блоки временно отклоняются с `repost_disabled`
|
||||
- `TEXT_REPOST (30)`
|
||||
|
||||
3. **REACTION (type=2)**
|
||||
- `REACTION_LIKE (1)`
|
||||
|
||||
@ -202,13 +202,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
### MessageNode (дополнение)
|
||||
- `MessageNode` расширяет формат сообщения из `GetChannelMessages` и дополнительно содержит:
|
||||
- `channelInfo` — мета-информация о канале (если применимо);
|
||||
- `rawBlockB64` — сырой `block_bytes` текущего блока в Base64.
|
||||
- Поле `rawBlockB64` присутствует у узлов во всех частях ответа `GetMessageThread`: `focus`, `ancestors[]`, `descendants[]`.
|
||||
- В `GetChannelMessages` поле `rawBlockB64` **не добавляется** (лента канала без сырого блока, чтобы не раздувать ответ).
|
||||
|
||||
---
|
||||
|
||||
## 4) GetChannelsCounters
|
||||
|
||||
@ -113,11 +113,6 @@
|
||||
### GetMessageThread
|
||||
- `payload.ancestors[]`, `payload.focus`, `payload.descendants[]`.
|
||||
- у узлов должны быть версии и счетчики.
|
||||
- у каждого узла дополнительно может приходить `rawBlockB64` (Base64 сырого `block_bytes`).
|
||||
|
||||
### Важно по совместимости
|
||||
- `rawBlockB64` добавлен только в `GetMessageThread`.
|
||||
- `GetChannelMessages` не содержит `rawBlockB64` (без изменений формата ленты).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
|
||||
| Операция | Раздел документации | Кратко |
|
||||
| --- | --- | --- |
|
||||
| `AddUser` | `01_User_Registration_API.md` | отключено (`410 / ADD_USER_DISABLED`) |
|
||||
| `GetUser` | `01_User_Registration_API.md` | чтение/проверка пользователя + server-состояние его блокчейна |
|
||||
| `AddUser` | `01_User_Registration_API.md` | временная регистрация пользователя |
|
||||
| `GetUser` | `01_User_Registration_API.md` | чтение/проверка пользователя |
|
||||
| `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу |
|
||||
| `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии |
|
||||
| `CreateAuthSession` | `02_Authentication_API.md` | создание новой авторизованной сессии |
|
||||
|
||||
@ -24,8 +24,7 @@ TEXT-тип хранит сообщения и редактирования.
|
||||
5. `subType=30` — `TEXT_REPOST`
|
||||
- репост сообщения в линию канала;
|
||||
- содержит line-поля + target на оригинальное сообщение + текст комментария;
|
||||
- на текущем этапе продуктовой логики репост не редактируется (версии не накапливаются);
|
||||
- временно отключён для записи через `AddBlock` до будущей реализации репостов.
|
||||
- на текущем этапе продуктовой логики репост не редактируется (версии не накапливаются).
|
||||
|
||||
## Правило для edit
|
||||
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
# История изменений документации блокчейна
|
||||
|
||||
## 2026-05-24 11:40:00 +0300
|
||||
- Базовый коммит-ориентир: `abdce05`.
|
||||
- `TEXT_REPOST (subType=30)` оставлен как зарезервированный формат, но новые блоки репоста временно отключены на уровне `AddBlock`.
|
||||
- В `11_TEXT_Blocks.md` зафиксировано, что запись `TEXT_REPOST` временно не используется до будущей реализации.
|
||||
- В `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md` добавлен код отказа `repost_disabled`.
|
||||
|
||||
## 2026-05-21 19:05:00 +0300
|
||||
- Базовый коммит-ориентир: `5344c42`.
|
||||
- Добавлен новый TEXT-подтип `TEXT_REPOST (subType=30)`:
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
# Будущие фичи
|
||||
|
||||
Эта папка хранит задачи, которые сознательно отложены и сейчас не должны попадать в активную разработку или ручную проверку без отдельной команды пользователя.
|
||||
|
||||
## Горизонты планирования
|
||||
|
||||
- `near/` - ближайшие планы: задачи, к которым можно вернуться сегодня или завтра.
|
||||
- `medium/` - среднесрочные планы: задачи на ближайшие недели или 1-2 месяца.
|
||||
- `far/` - дальнее будущее: идеи без понятного срока возврата.
|
||||
|
||||
Если пользователь спрашивает, какие есть планы, агент должен смотреть эти три папки и кратко перечислять задачи по горизонтам.
|
||||
|
||||
## Как использовать
|
||||
|
||||
1. Каждая будущая фича описывается отдельным markdown-файлом в одном из горизонтов.
|
||||
2. В файле нужно фиксировать:
|
||||
- зачем нужна фича;
|
||||
- к какому сроку или горизонту она относится;
|
||||
- что нужно сделать;
|
||||
- какие вопросы нужно уточнить перед реализацией;
|
||||
- что уже было сделано в коде, если фича частично реализована;
|
||||
- что временно отключено или закомментировано, если применимо;
|
||||
- какие документы нужно обновить при возврате к задаче;
|
||||
- с какого места продолжать разработку.
|
||||
3. Агент не должен начинать реализацию файлов из этой папки без явной просьбы пользователя.
|
||||
|
||||
## Текущие планы
|
||||
|
||||
### Ближайшие
|
||||
|
||||
- `near/2026-05-25_1106_telegram_agent_players.md` - разрешённые пользователи Telegram для агента, отдельные папки игроков, персональные истории и публикация краткого вопроса/ответа в общий канал.
|
||||
- `near/2026-05-25_1106_wallet_topup_solana_arweave.md` - пополнение Solana и Arweave через внешний сервис покупки с подсказкой и копированием адреса.
|
||||
|
||||
### Среднесрочные
|
||||
|
||||
- `medium/2026-05-24_1140_репосты_в_каналах_и_тредах.md` - репосты в каналах и тредах.
|
||||
- `medium/2026-05-25_1106_shine_balance_wallet.md` - кошелёк и пополнение баланса сияния через блокчейн.
|
||||
- `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений.
|
||||
|
||||
### Дальнее будущее
|
||||
|
||||
- Сейчас задач нет.
|
||||
@ -1,5 +0,0 @@
|
||||
# Дальнее будущее
|
||||
|
||||
Сейчас в этом горизонте нет активных идей.
|
||||
|
||||
Сюда переносить задачи, у которых нет понятного срока возврата и которые не нужно учитывать в ближайшем или среднесрочном планировании.
|
||||
@ -1,99 +0,0 @@
|
||||
# Репосты в каналах и тредах
|
||||
|
||||
- Статус:
|
||||
`future`
|
||||
|
||||
- Горизонт:
|
||||
`medium`
|
||||
|
||||
- Ориентир:
|
||||
1-2 месяца
|
||||
|
||||
- Решение от 2026-05-24:
|
||||
Репосты временно убраны из активной разработки. Фича уже была частично реализована, но не доведена до финальной проверки. Чтобы она не мешала запуску проекта, пользовательский вход в репосты отключён в UI, а сервер больше не принимает новые `TEXT_REPOST` через `AddBlock`.
|
||||
|
||||
## Что должна делать фича
|
||||
|
||||
Репост должен позволять взять сообщение из канала или треда и опубликовать его в один из своих каналов с комментарием.
|
||||
|
||||
Ожидаемый пользовательский сценарий после возврата к задаче:
|
||||
|
||||
1. Пользователь открывает сообщение в канале или треде.
|
||||
2. Нажимает `Репост`.
|
||||
3. Выбирает один из своих каналов.
|
||||
4. Добавляет комментарий.
|
||||
5. Отправляет репост.
|
||||
6. В целевом канале появляется новый пост-репост.
|
||||
7. У репоста есть переход к исходному сообщению через действие `Оригинал`.
|
||||
|
||||
## Что уже есть в коде
|
||||
|
||||
- В блокчейн-формате зарезервирован и описан подтип `TEXT_REPOST (30)`.
|
||||
- Парсер блокчейна умеет распознавать тело репоста как `TextLineBody`.
|
||||
- В UI есть функция сборки тела репоста:
|
||||
`shine-UI/js/services/auth-service.js`, `makeTextRepostBodyBytes`.
|
||||
- В UI есть клиентская операция:
|
||||
`shine-UI/js/services/auth-service.js`, `addBlockRepost`.
|
||||
- В экране канала был обработчик репоста:
|
||||
`shine-UI/js/pages/channel-view.js`, `onRepost`.
|
||||
- В экране треда был обработчик репоста:
|
||||
`shine-UI/js/pages/channel-thread-view.js`, `onRepost`.
|
||||
- Серверная выдача каналов и тредов частично учитывает `TEXT_REPOST` и target-поля:
|
||||
`targetBlockchainName`, `targetBlockNumber`, `targetBlockHash`.
|
||||
- В списке подписок `TEXT_REPOST` считается публикацией канала.
|
||||
|
||||
## Что сейчас отключено
|
||||
|
||||
- В `shine-UI/js/pages/channel-view.js` кнопка `Репост` больше не создаётся и не добавляется в список действий сообщения.
|
||||
- В `shine-UI/js/pages/channel-thread-view.js` кнопка `Репост` больше не создаётся и не добавляется в список действий ответа/сообщения треда.
|
||||
- В `shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java` добавлена явная временная блокировка:
|
||||
если новый блок имеет `type=1` и `subType=TEXT_REPOST (30)`, `AddBlock` возвращает ошибку `repost_disabled`.
|
||||
|
||||
## Что осталось активным намеренно
|
||||
|
||||
- Константа `TEXT_REPOST (30)` остаётся в коде и документации как зарезервированный формат.
|
||||
- Парсер блокчейна продолжает знать формат `TEXT_REPOST`, чтобы не потерять уже написанную основу и не ломать потенциальное чтение старых тестовых данных.
|
||||
- Код формирования репоста в `auth-service.js` не удалён: его можно будет использовать как основу при возвращении к задаче.
|
||||
- Код отображения target-полей и перехода к оригиналу не удалён: он нужен для будущей проверки и возможной совместимости с уже созданными тестовыми блоками.
|
||||
|
||||
## Почему это не лежит в Pending_Features
|
||||
|
||||
`Dev_Docs/Pending_Features/` предназначена для фич, которые уже реализованы и ждут ручной проверки.
|
||||
|
||||
Репосты сейчас не подходят под этот статус: они не должны проверяться как готовая фича, потому что пользовательский сценарий временно закрыт, а серверная запись новых репостов заблокирована. Поэтому старый pending-файл удалён, а задача перенесена сюда как будущая.
|
||||
|
||||
## Что сделать при возврате к реализации
|
||||
|
||||
1. Решить, остаётся ли формат `TEXT_REPOST (30)` финальным.
|
||||
2. Если формат меняется, заранее предупредить пользователя и получить отдельное подтверждение на изменение блокчейн-формата.
|
||||
3. Вернуть UI-кнопки репоста в:
|
||||
- `shine-UI/js/pages/channel-view.js`;
|
||||
- `shine-UI/js/pages/channel-thread-view.js`.
|
||||
4. Снять временную блокировку `repost_disabled` в:
|
||||
`shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java`.
|
||||
5. Проверить `auth-service.js`:
|
||||
- `makeTextRepostBodyBytes`;
|
||||
- `addBlockRepost`;
|
||||
- актуализацию вершины блокчейна перед `AddBlock`;
|
||||
- корректность target-полей исходного сообщения.
|
||||
6. Проверить серверное чтение:
|
||||
- `GetChannelMessages`;
|
||||
- `GetMessageThread`;
|
||||
- отображение `targetBlockchainName`, `targetBlockNumber`, `targetBlockHash`.
|
||||
7. Добавить или обновить тесты на успешный репост и отказ некорректных target-полей.
|
||||
8. Обновить документацию:
|
||||
- `Dev_Docs/Blockchain/11_TEXT_Blocks.md`;
|
||||
- `Dev_Docs/Blockchain/CHANGELOG.md`;
|
||||
- `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md`;
|
||||
- документы API чтения каналов/тредов, если изменятся поля ответа.
|
||||
9. После реализации перенести задачу из `Dev_Docs/Future_Features/` в `Dev_Docs/Pending_Features/` как фичу, требующую ручной проверки.
|
||||
|
||||
## Минимальный чек-лист ручной проверки в будущем
|
||||
|
||||
1. Репост из сообщения канала в свой канал.
|
||||
2. Репост из ответа в треде в свой канал.
|
||||
3. Ошибка при попытке репоста в чужой канал.
|
||||
4. Переход `Оригинал` из репоста к исходному сообщению.
|
||||
5. Корректное отображение комментария к репосту.
|
||||
6. Корректная работа после перезагрузки страницы.
|
||||
7. Отсутствие поломки обычных постов, ответов, лайков и отправки ссылки.
|
||||
@ -1,62 +0,0 @@
|
||||
# Кошелёк и пополнение баланса сияния
|
||||
|
||||
- Горизонт:
|
||||
`medium`
|
||||
- Ориентир:
|
||||
среднесрочно
|
||||
- Статус:
|
||||
`proposal`
|
||||
|
||||
## Кратко
|
||||
|
||||
Нужно добавить кошелёк для внутреннего баланса сияния и пополнение этого баланса через блокчейн-логику проекта. Задача связана с регистрацией пользователя и будущим учётом баланса.
|
||||
|
||||
## Предполагаемый сценарий
|
||||
|
||||
1. Пользователь регистрируется и получает/подключает нужные кошельки.
|
||||
2. В интерфейсе появляется баланс сияния.
|
||||
3. Пользователь открывает пополнение баланса сияния.
|
||||
4. Система создаёт или принимает блокчейн-операцию пополнения.
|
||||
5. После подтверждения баланса UI обновляет значение.
|
||||
|
||||
## Что нужно продумать
|
||||
|
||||
1. Что именно является единицей баланса сияния.
|
||||
2. Где хранится состояние баланса: в существующем блокчейне SHiNE, Solana-модуле или комбинированно.
|
||||
3. Какая операция отвечает за пополнение.
|
||||
4. Нужно ли делать отдельную регистрацию кошелька сияния или использовать существующую регистрацию пользователя.
|
||||
5. Как баланс восстанавливается после перезагрузки клиента.
|
||||
6. Какие права нужны для пополнения и списания.
|
||||
7. Нужна ли история операций баланса.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
1. Пополнение баланса сияния должно идти через основной блокчейн SHiNE или через Solana-программу.
|
||||
2. Нужна ли конвертация из SOL/AR в сияние.
|
||||
3. Кто может выпускать или начислять сияние.
|
||||
4. Нужно ли поддерживать перевод сияния между пользователями.
|
||||
5. Нужны ли лимиты, комиссии или статусы подтверждения.
|
||||
6. Какой экран должен показывать баланс: регистрация, профиль, кошелёк или отдельная страница.
|
||||
7. Нужно ли отображать неподтверждённый баланс отдельно от подтверждённого.
|
||||
|
||||
## Важное ограничение
|
||||
|
||||
Если для баланса сияния потребуется новый формат блокчейн-блока или изменение существующего формата, перед реализацией нужно отдельно предупредить пользователя и получить явное подтверждение на изменение формата блокчейна.
|
||||
|
||||
Если потребуется новый серверный API или изменение существующих `op`, перед реализацией нужно отдельно предупредить пользователя и получить явное подтверждение на изменение API.
|
||||
|
||||
## Документы, которые обновить при реализации
|
||||
|
||||
- `Dev_Docs/Blockchain/`, если появятся или изменятся блоки баланса.
|
||||
- `Dev_Docs/Blockchain/CHANGELOG.md`, если меняется блокчейн-формат.
|
||||
- `Dev_Docs/API/`, если меняется серверный API.
|
||||
- `Dev_Docs/Pending_Features/` - добавить файл ручной проверки после реализации.
|
||||
- Документацию Solana-регистрации, если баланс будет связан с Solana-модулем.
|
||||
|
||||
## Минимальная проверка в будущем
|
||||
|
||||
1. Новый пользователь видит корректный начальный баланс.
|
||||
2. Пополнение создаёт правильную операцию.
|
||||
3. Баланс обновляется после подтверждения.
|
||||
4. После перезагрузки UI баланс остаётся корректным.
|
||||
5. Ошибочные или повторные операции не начисляют баланс дважды.
|
||||
@ -1,44 +0,0 @@
|
||||
# ESP32S3 как личное файловое хранилище SHiNE
|
||||
|
||||
## Горизонт
|
||||
|
||||
Среднесрочный: ближайшие недели или 1-2 месяца.
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
Нужно проработать маленький физический сервер на ESP32S3 как персональное или доверенное файловое хранилище SHiNE.
|
||||
|
||||
Идея: при обмене сообщениями пользователи смогут использовать такой сервер для хранения своих файлов, вложений, файлов общих переписок и связанных данных.
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
- Описать роль ESP32S3-сервера в общей архитектуре ключей и сессий.
|
||||
- Определить, какие ключи может хранить такое устройство.
|
||||
- Решить, хранит ли устройство только файлы или также подписывает пользовательские операции.
|
||||
- Описать протокол загрузки, скачивания и удаления файлов.
|
||||
- Определить правила шифрования файлов до отправки на устройство.
|
||||
- Продумать индексацию файлов для личных и общих переписок.
|
||||
- Решить, как устройство авторизуется на основном сервере SHiNE.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
- ESP32S3 должен работать как полностью локальное устройство или как публично доступный мини-сервер?
|
||||
- Нужен ли внешний relay, если устройство находится за NAT?
|
||||
- Какие ограничения по размеру файла считаем допустимыми?
|
||||
- Хранит ли устройство метаданные переписок или только зашифрованные blob-файлы?
|
||||
- Как восстанавливать доступ, если устройство потеряно или заменено?
|
||||
|
||||
## Что уже сделано
|
||||
|
||||
Код не реализован. Идея зафиксирована как будущая задача после описания модели ключей.
|
||||
|
||||
## Документы, которые нужно обновить при возврате
|
||||
|
||||
- `Dev_Docs/Keys/README.md`
|
||||
- `Dev_Docs/Personal_Messages/README.md`
|
||||
- `Dev_Docs/API/`
|
||||
- `Dev_Docs/Blockchain/`, если появятся новые блоки или команды для файлов.
|
||||
|
||||
## С какого места продолжать
|
||||
|
||||
Начать с короткого протокольного документа: роли устройства, авторизация, шифрование файлов, минимальные API-операции и сценарии восстановления.
|
||||
@ -1,94 +0,0 @@
|
||||
# Telegram-агент для разрешённых игроков
|
||||
|
||||
- Горизонт:
|
||||
`near`
|
||||
- Ориентир:
|
||||
сегодня/завтра
|
||||
- Статус:
|
||||
`proposal`
|
||||
|
||||
## Кратко
|
||||
|
||||
Нужно расширить `SHiNE-agent-bot-coder`, чтобы агент мог принимать личные сообщения от заранее разрешённых пользователей, вести по каждому отдельную рабочую папку и историю, помогать им с обсуждениями/документами без изменения кода, а краткий результат публиковать в общий канал.
|
||||
|
||||
## Пользовательский сценарий
|
||||
|
||||
1. Разрешённый пользователь пишет агенту в личные сообщения текстом или голосом.
|
||||
2. Голосовое сообщение распознаётся так же, как сейчас распознаются voice/audio-задачи.
|
||||
3. Сервис определяет пользователя по разрешённому списку логинов.
|
||||
4. Для пользователя используется отдельная папка в `Players/`.
|
||||
5. Codex запускается с системным контекстом: от имени какого человека он работает, где лежит его папка, какие у него локальные инструкции.
|
||||
6. Агент может читать код и документацию проекта, но писать должен только в папку этого пользователя, если нет отдельного согласования на изменение общего проекта.
|
||||
7. После ответа пользователю агент отправляет в общий канал короткую сводку двумя сообщениями или двумя блоками: вопрос пользователя и полученный ответ.
|
||||
8. Команда `/new` или `New` сбрасывает только сессию этого пользователя.
|
||||
|
||||
## Предлагаемая структура
|
||||
|
||||
- `Players/`
|
||||
- `Ivan/`
|
||||
- `AGENTS.md`
|
||||
- `history/`
|
||||
- `files/`
|
||||
- `Sergey/`
|
||||
- `AGENTS.md`
|
||||
- `history/`
|
||||
- `files/`
|
||||
- `Milana/`
|
||||
- `AGENTS.md`
|
||||
- `history/`
|
||||
- `files/`
|
||||
|
||||
Имена папок можно уточнить после получения точных Telegram-логинов.
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
1. Добавить конфигурацию разрешённых Telegram-пользователей.
|
||||
2. Описать соответствие `telegram username -> имя игрока -> папка`.
|
||||
3. Создавать или использовать отдельную историю диалога для каждого игрока.
|
||||
4. Поддержать личные сообщения от разрешённых пользователей.
|
||||
5. Запретить постановку задач от неизвестных пользователей.
|
||||
6. Для групп/каналов оставить текущую логику: команды Айдара имеют приоритет.
|
||||
7. При запуске Codex для игрока добавлять отдельный системный контекст:
|
||||
- имя пользователя;
|
||||
- путь к его папке;
|
||||
- правило записи только в эту папку;
|
||||
- путь к персональному `AGENTS.md`.
|
||||
8. После ответа игроку отправлять краткую сводку в общий канал.
|
||||
9. Поддержать `/new`/`New` как сброс только персональной сессии игрока.
|
||||
10. Добавить защиту от случайного изменения общего кода в режиме игрока.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
1. Точные Telegram-логины Ивана, Сергея и Миланы.
|
||||
2. Какой общий канал использовать для сводок: текущий `@shine_writing` или отдельный чат.
|
||||
3. Нужно ли отправлять в общий канал полный текст вопроса/ответа или краткую выжимку.
|
||||
4. Нужно ли пересылать вложения игроков в общий канал или только текстовые сводки.
|
||||
5. Разрешить ли игрокам читать все документы проекта, включая технические заметки деплоя.
|
||||
6. Что делать, если пользователь просит изменить код: отказать, создать предложение в своей папке или просить подтверждение Айдара.
|
||||
7. Нужны ли русские имена папок (`Иван`, `Сергей`, `Милана`) или ASCII-имена (`Ivan`, `Sergey`, `Milana`).
|
||||
8. Нужно ли хранить истории игроков в общей папке сервиса или внутри `Players/<name>/history/`.
|
||||
|
||||
## Риски и ограничения
|
||||
|
||||
- Нужно аккуратно разделить режим Айдара и режим игрока, чтобы игроки не могли случайно запустить изменение общего кода.
|
||||
- Нужно не смешать истории разных пользователей.
|
||||
- Нужно ограничить публикацию в общий канал, чтобы не утекали личные или слишком длинные ответы.
|
||||
- Нужна проверка Telegram-идентификации: username может меняться, поэтому желательно хранить и `user_id`.
|
||||
|
||||
## Документы, которые обновить при реализации
|
||||
|
||||
- `SHiNE-agent-bot-coder/AGENTS.md`
|
||||
- `SHiNE-agent-bot-coder/AGENT.md`
|
||||
- `SHiNE-agent-bot-coder/README.md`
|
||||
- `Dev_Docs/deploy/agent-bot-coder-local-systemd.md`, если появятся новые переменные окружения или настройки сервиса.
|
||||
|
||||
## Минимальная проверка
|
||||
|
||||
1. Айдар по-прежнему может ставить задачи из `@shine_writing`.
|
||||
2. Неизвестный пользователь не ставит задачу в очередь.
|
||||
3. Разрешённый игрок пишет личное текстовое сообщение и получает ответ.
|
||||
4. Разрешённый игрок отправляет voice, оно распознаётся и обрабатывается.
|
||||
5. История одного игрока не попадает в историю другого.
|
||||
6. `/new` сбрасывает только историю текущего игрока.
|
||||
7. Сводка вопрос/ответ появляется в общем канале.
|
||||
8. В режиме игрока агент не пишет за пределы `Players/<name>/` без отдельного подтверждения.
|
||||
@ -1,71 +0,0 @@
|
||||
# Пополнение Solana и Arweave через внешний сервис покупки
|
||||
|
||||
- Горизонт:
|
||||
`near`
|
||||
- Ориентир:
|
||||
сегодня/завтра
|
||||
- Статус:
|
||||
`proposal`
|
||||
|
||||
## Кратко
|
||||
|
||||
Нужно добавить удобное пополнение кошельков на экране регистрации/кошелька: для Solana и Arweave дать отдельные действия `Пополнить`, которые ведут на международный сервис покупки криптовалюты с карты и помогают пользователю скопировать адрес кошелька.
|
||||
|
||||
## Пользовательский сценарий
|
||||
|
||||
1. Пользователь видит адрес кошелька Solana или Arweave.
|
||||
2. Нажимает `Пополнить`.
|
||||
3. Открывается промежуточное окно с инструкцией:
|
||||
- сейчас пользователь перейдёт на страницу покупки/пополнения;
|
||||
- нужно указать или проверить адрес кошелька;
|
||||
- после оплаты нужно закрыть внешнюю страницу и вернуться назад;
|
||||
- Solana обычно приходит быстро, ориентир 10-15 секунд после подтверждения сети;
|
||||
- Arweave может идти дольше, точное время нужно уточнить по выбранному сервису.
|
||||
4. В окне есть кнопки:
|
||||
- `Скопировать адрес и перейти`;
|
||||
- `Перейти без копирования`.
|
||||
5. Для Solana и Arweave используются разные окна/инструкции и, возможно, разные внешние ссылки.
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
1. Найти текущий экран, где показываются кошельки при регистрации и пополнении.
|
||||
2. Найти текущую ссылку покупки Arweave, если она уже есть в UI.
|
||||
3. Выбрать международный сервис покупки Solana с карты, не российский.
|
||||
4. Проверить, поддерживает ли сервис deep link с предзаполненным адресом кошелька.
|
||||
5. Если deep link невозможен, реализовать промежуточное окно с копированием адреса.
|
||||
6. Добавить отдельные действия для Solana и Arweave.
|
||||
7. Сделать текст инструкции коротким и понятным.
|
||||
8. Проверить, что адрес копируется в буфер обмена в браузере.
|
||||
9. Проверить мобильный сценарий и desktop-сценарий.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
1. Какой сервис покупки Solana использовать: тот же провайдер, что для Arweave, или другой международный on-ramp.
|
||||
2. Нужно ли разрешать покупку только SOL или также USDC/SPL-токены на Solana.
|
||||
3. Где именно показывать кнопку `Пополнить`: только регистрация, настройки кошелька или оба места.
|
||||
4. Нужно ли показывать предупреждение о комиссиях и стороннем сервисе.
|
||||
5. Нужно ли открывать внешнюю страницу в новой вкладке или в текущем окне.
|
||||
6. Нужно ли логировать факт нажатия `Пополнить` на сервере.
|
||||
7. Какой точный текст использовать для времени прихода Arweave.
|
||||
|
||||
## Риски и ограничения
|
||||
|
||||
- On-ramp-сервисы меняют ссылки и параметры, поэтому deep link нужно проверять перед реализацией.
|
||||
- Clipboard API может требовать HTTPS и пользовательский жест.
|
||||
- Нельзя обещать точное время поступления средств: лучше писать ориентир и зависимость от сети/провайдера.
|
||||
- Внешний сервис может быть недоступен в отдельных странах или для отдельных карт.
|
||||
|
||||
## Документы, которые обновить при реализации
|
||||
|
||||
- Документацию UI/кошельков, если такая есть.
|
||||
- `Dev_Docs/Pending_Features/` - добавить файл ручной проверки после реализации.
|
||||
- `Dev_Docs/API/`, только если появится новый серверный API или логирование.
|
||||
|
||||
## Минимальная проверка
|
||||
|
||||
1. На Solana-кошельке открывается правильное окно пополнения.
|
||||
2. Кнопка `Скопировать адрес и перейти` копирует Solana-адрес и открывает внешний сервис.
|
||||
3. Кнопка `Перейти без копирования` открывает внешний сервис без копирования.
|
||||
4. Аналогичный сценарий работает для Arweave.
|
||||
5. На мобильном экране текст и кнопки не перекрываются.
|
||||
6. Возврат назад в приложение не ломает состояние регистрации/кошелька.
|
||||
@ -1,175 +0,0 @@
|
||||
# Ключи SHiNE
|
||||
|
||||
Этот документ описывает роли ключей в SHiNE и их связь с Solana, персональным блокчейном, личными сообщениями, сессиями и будущими аппаратными устройствами.
|
||||
|
||||
Документ является архитектурной справкой. Он не меняет текущие форматы API, DM-блоков или блокчейна сам по себе.
|
||||
|
||||
## Коротко
|
||||
|
||||
В SHiNE у пользователя есть несколько уровней ключей:
|
||||
|
||||
- `root key` - главный корневой ключ пользователя, он же основной Solana-ключ.
|
||||
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
|
||||
- `device key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
|
||||
- `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере.
|
||||
|
||||
Главная идея: самые важные ключи можно держать на доверенном серверном или аппаратном устройстве, а обычные клиентские устройства получают только ключи, нужные для текущей работы.
|
||||
|
||||
## `root key`
|
||||
|
||||
`root key` - главный ключ пользователя.
|
||||
|
||||
Назначение:
|
||||
|
||||
- регистрация пользователя в Solana;
|
||||
- создание и обновление пользовательской PDA-записи;
|
||||
- вызов критически важных Solana-функций;
|
||||
- изменение главных настроек пользователя;
|
||||
- управление остальными ключами;
|
||||
- подтверждение операций, которые должны иметь максимальный уровень доверия.
|
||||
|
||||
В текущей модели `root key` совпадает по смыслу с главным Solana-ключом пользователя.
|
||||
|
||||
На `root key` могут храниться значимые средства, если пользователь сознательно выбирает такую модель. Для мелких текущих расходов предпочтительнее использовать `device key`.
|
||||
|
||||
## `blockchain key`
|
||||
|
||||
`blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
|
||||
|
||||
Назначение:
|
||||
|
||||
- подпись записей в персональном блокчейне пользователя;
|
||||
- подтверждение действий, которые должны попасть в SHiNE-блокчейн;
|
||||
- разделение полномочий между главным Solana-ключом и ключом ежедневной записи.
|
||||
|
||||
У пользователя может быть несколько персональных блокчейнов или веток. При смене `blockchain key` фактически создаётся новая ветка записи:
|
||||
|
||||
- `username-001` - первая ветка;
|
||||
- `username-002` - вторая ветка;
|
||||
- `username-003` - третья ветка.
|
||||
|
||||
Рабочая логика по умолчанию должна использовать последнюю актуальную ветку. Старые ветки остаются читаемыми и показывают историю смены ключей.
|
||||
|
||||
## `device key`
|
||||
|
||||
`device key` - общий ключ, который знают доверенные устройства пользователя.
|
||||
|
||||
Назначение:
|
||||
|
||||
- повседневные входящие и исходящие личные сообщения;
|
||||
- звонки и связанные с ними сообщения;
|
||||
- self-messages, то есть внутренние сообщения пользователя самому себе;
|
||||
- мелкие Solana-расходы на текущие операции;
|
||||
- derivation Arweave-кошелька;
|
||||
- оплата или подготовка добавления данных в Arweave-кошелек по отдельному протоколу.
|
||||
|
||||
Arweave-кошелёк должен выводиться из `device key` по протоколу:
|
||||
|
||||
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md`
|
||||
|
||||
Если пользователь теряет только `device key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными.
|
||||
|
||||
## `session key`
|
||||
|
||||
`session key` - уникальный ключ конкретной сессии или устройства.
|
||||
|
||||
Возможные форматы:
|
||||
|
||||
- `Ed25519` - предпочтительный современный вариант;
|
||||
- `RSA` - legacy-вариант, полезный для устройств, где системное защищённое хранилище хорошо поддерживает RSA-ключи и не позволяет извлекать приватный ключ.
|
||||
|
||||
Назначение:
|
||||
|
||||
- авторизация сессии на сервере;
|
||||
- привязка устройства к пользователю;
|
||||
- подтверждение запросов от конкретной сессии;
|
||||
- доступ к зашифрованному `device key` после успешной авторизации.
|
||||
|
||||
Одна и та же сессия может быть пригодна для подключения к нескольким серверам пользователя, если архитектура конкретного пользователя это допускает.
|
||||
|
||||
У сессии должны быть:
|
||||
|
||||
- имя сессии;
|
||||
- тип сессии;
|
||||
- публичная часть ключа;
|
||||
- ссылка на пользователя;
|
||||
- информация о сервере или серверах, которым эта сессия доверена.
|
||||
|
||||
Имя сессии может создаваться автоматически из названия устройства и короткого случайного идентификатора, например `Android-a1b2c3`, `Ubuntu-f47a90`. Пользователь может переименовать сессию.
|
||||
|
||||
## Типы сессий
|
||||
|
||||
Базовые типы:
|
||||
|
||||
- обычная пользовательская сессия;
|
||||
- серверная сессия;
|
||||
- аппаратная или доверенная сессия с доступом к расширенным ключам.
|
||||
|
||||
Обычное устройство обычно имеет:
|
||||
|
||||
- собственный `session key`;
|
||||
- зашифрованный `device key`, который открывается после авторизации;
|
||||
- доступ к DM, звонкам и обычным пользовательским операциям.
|
||||
|
||||
Доверенное серверное или аппаратное устройство может иметь:
|
||||
|
||||
- `root key`;
|
||||
- `blockchain key`;
|
||||
- `device key`;
|
||||
- собственный `session key`.
|
||||
|
||||
Такая сессия может подписывать операции повышенной важности по запросам пользователя.
|
||||
|
||||
## Внутренние self-messages
|
||||
|
||||
Self-message - это сообщение пользователя самому себе.
|
||||
|
||||
Такие сообщения нужны, чтобы обычное устройство могло попросить доверенное устройство выполнить действие:
|
||||
|
||||
- подписать запись `blockchain key` и передать её в SHiNE-блокчейн;
|
||||
- подписать изменение настройки через `root key`;
|
||||
- обновить ключи;
|
||||
- сохранить внутреннюю команду или настройку;
|
||||
- отправить сообщение другому пользователю с сохранением копии себе;
|
||||
- сохранить сообщение только себе.
|
||||
|
||||
Важно: self-message не является публичной командой сервера. Это пользовательская внутренняя команда, которую сервер или доверенное устройство обрабатывает в рамках прав конкретного пользователя.
|
||||
|
||||
## Шифрование входящих сообщений
|
||||
|
||||
Входящее сообщение может быть зашифровано:
|
||||
|
||||
- `device key`;
|
||||
- `session key`;
|
||||
- отдельным ключом конкретного чата;
|
||||
- другим ключом, который уже известен клиенту.
|
||||
|
||||
В сообщении не должно быть лишнего раскрытия того, каким именно ключом оно зашифровано. Клиент пробует расшифровать сообщение доступными ключами по порядку. Если расшифровка не удалась, сообщение остаётся непонятным для этого устройства.
|
||||
|
||||
## Копии сообщений
|
||||
|
||||
Для отправки сообщений нужны несколько режимов:
|
||||
|
||||
- сообщение другому пользователю с исходящей копией себе;
|
||||
- сообщение другому пользователю без локальной исходящей копии;
|
||||
- сообщение только себе.
|
||||
|
||||
Это должно позволить строить обычные DM, внутренние команды, личные заметки и зашифрованные пользовательские чаты поверх одной общей модели сообщений.
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- `Dev_Docs/Personal_Messages/README.md` - текущая документация личных сообщений.
|
||||
- `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна.
|
||||
- `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств.
|
||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` - деплой и первичная инициализация Solana-регистрации.
|
||||
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md` - derivation Arweave-кошелька из `device key`.
|
||||
|
||||
## Что нужно уточнить перед реализацией
|
||||
|
||||
- точный формат записи списка ключей в Solana PDA;
|
||||
- как именно обозначать активную ветку персонального блокчейна;
|
||||
- какие операции требуют `root key`, а какие достаточно подписывать `blockchain key`;
|
||||
- формат self-message-команд;
|
||||
- порядок перебора ключей при расшифровке входящих сообщений;
|
||||
- правила ротации `device key` и восстановления доступа после потери устройства;
|
||||
- какие типы серверных и аппаратных сессий нужны в первой реализации.
|
||||
@ -0,0 +1,21 @@
|
||||
# Репосты в каналах и тредах
|
||||
|
||||
- Краткое описание:
|
||||
Добавлен подтип `TEXT_REPOST (30)` и UI-режим репоста с комментарием. Репост можно делать как из сообщения канала, так и из сообщения в треде. Для репоста выбирается один из своих каналов.
|
||||
|
||||
- Что проверять:
|
||||
1. В канале открыть любое сообщение и нажать `Репост`.
|
||||
2. Выбрать свой канал, ввести комментарий, отправить.
|
||||
3. Убедиться, что в целевом канале появился новый пост-репост.
|
||||
4. Нажать `Оригинал` у репоста и подтвердить переход.
|
||||
5. Проверить, что переход открывает исходное сообщение.
|
||||
6. Повторить сценарий из треда (для сообщения-ответа).
|
||||
|
||||
- Ожидаемый результат:
|
||||
- Репост успешно записывается в блокчейн как `TEXT_REPOST`.
|
||||
- В выдаче `GetChannelMessages`/`GetMessageThread` возвращаются поля target (`targetBlockchainName`, `targetBlockNumber`, `targetBlockHash`) для репоста.
|
||||
- Кнопка `Оригинал` открывает нужное исходное сообщение.
|
||||
- Для репоста не отображается история редактирования (одна версия).
|
||||
|
||||
- Статус:
|
||||
`pending`
|
||||
@ -0,0 +1,18 @@
|
||||
# SHiNE-agent-bot-coder: очередь, voice, codex, systemd
|
||||
|
||||
- краткое описание фичи:
|
||||
Добавлен новый сервис `SHiNE-agent-bot-coder` (Java), который обрабатывает сообщения от `@AidarKC`, ведёт JSONL-историю, использует файловую очередь, распознаёт voice через OpenAI, вызывает Codex CLI и поддерживает запуск как `systemd`-сервис.
|
||||
|
||||
- что именно проверять:
|
||||
1. Бот принимает текст от `@AidarKC`, ставит задачу в очередь и отправляет ответ от Codex.
|
||||
2. Бот принимает voice, отправляет текст распознавания и затем ответ от Codex.
|
||||
3. Одновременные сообщения обрабатываются строго по одному (без параллельных запусков Codex).
|
||||
4. После рестарта сервиса незавершённая активная задача повторно уходит в обработку.
|
||||
5. Команда `/new` архивирует текущую историю и создаёт новую.
|
||||
6. `systemd`-сервис стартует и автоматически перезапускается.
|
||||
|
||||
- ожидаемый результат:
|
||||
Все пункты выше отрабатывают без потери сообщений, с корректным обновлением `data/queue.jsonl`, `data/state.json` и `data/history/*.jsonl`.
|
||||
|
||||
- статус:
|
||||
`pending`
|
||||
@ -0,0 +1,22 @@
|
||||
# Агент-бот coder: устранение дублей и зависаний
|
||||
|
||||
- краткое описание фичи:
|
||||
- добавлен lock-файл `data/app.lock`, чтобы гарантировать единственный инстанс бота;
|
||||
- доработано завершение worker/codex при stop/restart, чтобы не было ложных retry после штатной остановки;
|
||||
- синхронизирован systemd-профиль под `--user` для `ai`;
|
||||
- улучшена отправка промежуточных статусов по событиям `codex --json`.
|
||||
|
||||
- что проверять:
|
||||
- при запущенном сервисе повторный запуск jar вручную завершается сразу с сообщением про занятый lock;
|
||||
- команда `/status` отвечает один раз (без дублей);
|
||||
- тестовая текстовая задача от `@AidarKC` обрабатывается и возвращает ответ;
|
||||
- при `/stop` активная задача завершается без последующего спама retry-ошибками;
|
||||
- в логах нет `409 Conflict` из-за второго poller после чистого перезапуска.
|
||||
|
||||
- ожидаемый результат:
|
||||
- одновременно работает только один процесс бота;
|
||||
- бот не дублирует ответы;
|
||||
- очередь не застревает на interrupted после штатного stop/restart.
|
||||
|
||||
- статус:
|
||||
- pending
|
||||
@ -0,0 +1,21 @@
|
||||
# Python-обвязка Telegram → Codex (упрощённый сервис)
|
||||
|
||||
- краткое описание фичи:
|
||||
- добавлен новый упрощённый сервис `SHiNE-agent-bot-coder/py_bot_service.py`;
|
||||
- сервис работает через long-polling Telegram, принимает только текст, ведёт историю в `JSONL`;
|
||||
- добавлены команды `/status`, `/queue`, `/stop`, `/cancel`, `/new`, `/help`;
|
||||
- systemd unit переключён на запуск Python-сервиса.
|
||||
|
||||
- что проверять:
|
||||
- `systemctl --user status shine-agent-bot-coder` показывает `active (running)`;
|
||||
- бот отвечает на `/status` одним сообщением без дублей;
|
||||
- тестовый текстовый запрос получает финальный ответ от Codex;
|
||||
- `/stop` корректно останавливает текущую задачу;
|
||||
- `/new` переносит текущую историю в `data/history/archive`.
|
||||
|
||||
- ожидаемый результат:
|
||||
- стабильная обработка текстовых задач без зависаний и двойной обработки;
|
||||
- только один инстанс сервиса (через lock `data/py_app.lock`).
|
||||
|
||||
- статус:
|
||||
- pending
|
||||
@ -1,43 +0,0 @@
|
||||
# Кошелёк: лимит/закрепление блокчейна Сияния
|
||||
|
||||
- статус: `pending`
|
||||
|
||||
## Кратко что сделано
|
||||
|
||||
- На экране `Кошелёк -> Блокчейн Сияния` добавлены 2 слоя данных:
|
||||
- фактическое состояние цепочки на сервере (`кол-во блоков`, `размер`, `крайний блок`, `hash`, `размер крайнего блока`);
|
||||
- закреплённое состояние в Solana PDA (`лимит`, `использовано`, `остаток`, `крайний блок`, `hash`).
|
||||
- Добавлены действия:
|
||||
- `Закрепить в Solana` — обновляет PDA до текущего состояния серверной цепочки;
|
||||
- `Увеличить лимит` — увеличивает `paid_limit_bytes` в PDA с учётом цены из economy PDA.
|
||||
- Если `rootKey`/`blockchainKey` не сохранены локально, экран запрашивает пароль, восстанавливает ключи через стандартную derivation-логику и предлагает сохранить их в зашифрованный контейнер.
|
||||
|
||||
## Что проверять вручную
|
||||
|
||||
1. Открыть `Кошелёк -> Блокчейн Сияния` под авторизованным пользователем.
|
||||
2. Проверить, что в блоке "Фактическое состояние на сервере" отображаются:
|
||||
- число блоков;
|
||||
- размер цепочки;
|
||||
- номер/хэш крайнего блока;
|
||||
- размер крайнего блока.
|
||||
3. Проверить, что в блоке "Закреплено в Solana" отображаются:
|
||||
- лимит;
|
||||
- израсходовано;
|
||||
- остаток;
|
||||
- номер/хэш крайнего закреплённого блока.
|
||||
4. Нажать `Закрепить в Solana` и убедиться, что:
|
||||
- приходит успешная транзакция;
|
||||
- после обновления Solana-показатели подтягиваются до серверных (или максимально близко по актуальному состоянию).
|
||||
5. Нажать `Увеличить лимит`, ввести значение кратное шагу, подтвердить списание и проверить:
|
||||
- лимит увеличился;
|
||||
- отображение цены/списания соответствует economy PDA.
|
||||
6. Повторить пункты 4-5 в сценарии, когда `rootKey`/`blockchainKey` не сохранены, и проверить:
|
||||
- появляется запрос пароля;
|
||||
- после ввода пароля операции выполняются;
|
||||
- предложение сохранить ключи показывается.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
- Экран корректно разделяет "фактическое состояние на сервере" и "закреплённое в Solana".
|
||||
- Обе операции (`Закрепить в Solana`, `Увеличить лимит`) выполняются без ошибок при валидных данных.
|
||||
- Восстановление ключей через пароль работает, а без нужных ключей операция не выполняется молча.
|
||||
@ -1,29 +0,0 @@
|
||||
# Озвучивание ответов агента
|
||||
|
||||
## Что сделано
|
||||
|
||||
В локальный Telegram-бот-сервис агента-кодера добавлены персональные настройки озвучивания финальных ответов:
|
||||
|
||||
- `/voice_on` включает озвучивание для текущего Telegram-пользователя;
|
||||
- `/voice_off` выключает озвучивание для текущего Telegram-пользователя;
|
||||
- `/voice_status` показывает текущее состояние;
|
||||
- если озвучивание включено, после текстового финального ответа сервис генерирует voice-файл через OpenAI TTS и отправляет его в Telegram;
|
||||
- длинные ответы делятся на несколько фрагментов озвучки.
|
||||
|
||||
## Что проверять
|
||||
|
||||
1. Перезапустить `shine-agent-bot-coder`.
|
||||
2. Отправить `/voice_status` и убедиться, что по умолчанию озвучивание выключено.
|
||||
3. Отправить `/voice_on`.
|
||||
4. Дать простую задачу агенту и проверить, что пришёл полный текстовый ответ и voice-файл с тем же ответом.
|
||||
5. Отправить `/voice_off`.
|
||||
6. Дать ещё одну простую задачу и проверить, что приходит только текст.
|
||||
7. При возможности проверить второго whitelist-пользователя: его настройка должна быть независимой.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
Настройка хранится персонально по username и сохраняется после перезапуска сервиса. При включённой настройке Telegram получает текстовый ответ и дополнительное voice-сообщение с озвучкой. При выключенной настройке поведение остаётся прежним.
|
||||
|
||||
## Статус
|
||||
|
||||
pending
|
||||
@ -1,23 +0,0 @@
|
||||
# Solana user_pda v2
|
||||
|
||||
## Краткое описание
|
||||
|
||||
Функции `create_user_pda` и `update_user_pda` в Solana-модуле переведены на блочный формат пользовательской PDA-записи `format_major = 2`.
|
||||
|
||||
## Что проверять
|
||||
|
||||
- Создание `user_pda` через `create_user_pda`.
|
||||
- Обновление `user_pda` через `update_user_pda`.
|
||||
- Проверку root-подписи записи.
|
||||
- Проверку подписи `LastBlockState` ключом `blockchain_public_key`.
|
||||
- Корректную запись блоков `RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `TrustedState`.
|
||||
- Рост `paid_limit_bytes`, `used_bytes` и `last_block_number` без возможности уменьшения.
|
||||
- Совместимость тестового клиента с актуальной IDL после `anchor build`.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
Пользовательская PDA создается и обновляется в формате `format_major = 2`, содержит один основной блокчейн `blockchain_type = 1` с именем `<login>-001`, а неверные подписи или попытки уменьшить счетчики отклоняются программой.
|
||||
|
||||
## Статус
|
||||
|
||||
pending
|
||||
@ -1,26 +0,0 @@
|
||||
# Solana: init регистрации + деплой обязательных программ
|
||||
|
||||
- дата: 2026-05-24 20:35 (Europe/Moscow)
|
||||
- статус: `pending`
|
||||
|
||||
## Кратко
|
||||
|
||||
Добавлена dev-страница в UI для вызова `init_users_economy_config` программы `shine_users` через подключённый кошелёк Phantom.
|
||||
Задеплоены и зафиксированы адреса двух обязательных программ регистрации: `shine_users` и `shine_login_guard`.
|
||||
|
||||
## Что проверять вручную
|
||||
|
||||
1. Открыть UI и перейти в `Настройки разработчика`.
|
||||
2. Нажать `Solana: init регистрации`.
|
||||
3. Подключить Phantom devnet-кошелёк.
|
||||
4. Выполнить `init_users_economy_config`.
|
||||
5. Проверить отображение статуса и хэша транзакции.
|
||||
6. Повторно нажать init и убедиться, что корректно показывается "уже инициализировано".
|
||||
7. Выполнить тестовую регистрацию пользователя и убедиться, что CPI-вызов `shine_login_guard` не падает.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
- Первая транзакция выполняется успешно (если PDA ещё не создан).
|
||||
- Вторая попытка возвращает ожидаемую ошибку о повторной инициализации.
|
||||
- UI не падает, статус понятный, Program ID отображается корректно.
|
||||
- Регистрация пользователя проходит с подключённым `shine_login_guard`.
|
||||
@ -1,26 +0,0 @@
|
||||
# Отчёт private-запросов агента в группу
|
||||
|
||||
## Что сделано
|
||||
|
||||
После успешной обработки задачи из личного чата Айдара Telegram-бот агента отправляет итоговую копию в группу `@shine_writing`:
|
||||
|
||||
- первым сообщением исходный запрос;
|
||||
- вторым сообщением, reply на первое, финальный ответ Codex.
|
||||
|
||||
Промежуточные статусы выполнения в группу не дублируются.
|
||||
|
||||
## Что проверять
|
||||
|
||||
1. Отправить боту личный текстовый запрос.
|
||||
2. Дождаться полного ответа в личном чате.
|
||||
3. Проверить, что в `@shine_writing` появилось сообщение с запросом.
|
||||
4. Проверить, что итоговый ответ опубликован reply на это сообщение.
|
||||
5. Отправить личный voice-запрос и проверить, что в отчёте есть распознанный текст.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
Личный чат работает как раньше, а группа получает только итоговую пару сообщений по завершённой задаче.
|
||||
|
||||
## Статус
|
||||
|
||||
pending
|
||||
@ -1,23 +0,0 @@
|
||||
# Отчёт voice/audio-запросов с исходным файлом
|
||||
|
||||
## Краткое описание
|
||||
|
||||
Публичный отчёт по приватным voice/audio-запросам агента должен отправлять исходный Telegram voice/audio-файл с подписью, где указан распознанный текст. `file_id` не должен показываться пользователям в тексте отчёта.
|
||||
|
||||
## Что проверить
|
||||
|
||||
1. Отправить боту приватный voice-запрос от Айдара.
|
||||
2. Дождаться обработки Codex.
|
||||
3. Проверить группу/канал публичных отчётов.
|
||||
4. Повторить сценарий для audio-файла, если он используется.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
- В публичном отчёте появляется исходное голосовое/audio-сообщение.
|
||||
- В подписи к нему есть распознанный текст.
|
||||
- В отчёте нет строки `Голосовой file_id` и самого `file_id`.
|
||||
- Итоговый ответ Codex отправляется ответом на сообщение с исходным файлом.
|
||||
|
||||
## Статус
|
||||
|
||||
pending
|
||||
@ -1,19 +0,0 @@
|
||||
# Улучшенная обработка длинных voice/audio
|
||||
|
||||
## Что сделано
|
||||
- Увеличены и вынесены в `.env` тайм-ауты скачивания Telegram-файла и OpenAI-распознавания.
|
||||
- Добавлены подробные логи стадий распознавания: старт, скачивание, размер файла, завершение, причина ошибки.
|
||||
- Ошибки voice/audio теперь показываются пользователю как ошибка распознавания с понятной причиной.
|
||||
|
||||
## Как проверять
|
||||
- Отправить короткое голосовое сообщение и убедиться, что оно распознаётся и передаётся в Codex.
|
||||
- Отправить длинное голосовое сообщение и убедиться, что сервис не падает на прежнем коротком тайм-ауте.
|
||||
- Смоделировать ошибку распознавания или временно указать неверный OpenAI-ключ и проверить текст ошибки в Telegram.
|
||||
|
||||
## Ожидаемый результат
|
||||
- При успешном распознавании пользователь видит распознанный текст и задача уходит в Codex.
|
||||
- При ошибке пользователь видит, что не удалось именно распознать voice/audio, и получает конкретную причину.
|
||||
- В логах сервиса видны стадия и техническая причина сбоя.
|
||||
|
||||
## Статус
|
||||
pending
|
||||
@ -1,365 +0,0 @@
|
||||
# Solana user_pda: итоговый целевой формат пользовательской записи
|
||||
|
||||
Документ описывает целевой формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
|
||||
|
||||
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `Dev_Docs/Blockchain/`.
|
||||
|
||||
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
|
||||
|
||||
## 1. Назначение user_pda
|
||||
|
||||
`user_pda` хранит публичное состояние пользователя в Solana:
|
||||
|
||||
- логин пользователя;
|
||||
- неизменяемые параметры создания записи;
|
||||
- корневой публичный ключ пользователя;
|
||||
- ключ устройства;
|
||||
- данные одного или нескольких пользовательских блокчейнов SHiNE;
|
||||
- серверные данные пользователя, если пользователь выступает сервером;
|
||||
- серверы доступа пользователя;
|
||||
- счетчики/лимиты;
|
||||
- подпись записи.
|
||||
|
||||
На первом этапе поддерживается один пользовательский блокчейн SHiNE, но формат блока блокчейна сразу допускает повторение таких блоков в будущем.
|
||||
|
||||
## 2. Адрес PDA
|
||||
|
||||
Адрес пользовательской PDA вычисляется по логину:
|
||||
|
||||
- seed prefix: `login=`;
|
||||
- второй seed: нормализованный логин в нижнем регистре;
|
||||
- program id: программа `shine_users`.
|
||||
|
||||
Один логин соответствует одной `user_pda`.
|
||||
|
||||
## 3. Общие правила кодирования
|
||||
|
||||
- Числа кодируются в Little Endian.
|
||||
- `u8`, `u16`, `u32`, `u64` имеют обычный фиксированный размер.
|
||||
- Публичный ключ Solana/Ed25519: 32 байта.
|
||||
- Ed25519-подпись: 64 байта.
|
||||
- SHA-256/Solana hash: 32 байта.
|
||||
- Строка переменной длины: `len: u8` + `bytes[len]` в UTF-8.
|
||||
- Arweave `tx_id`: строка переменной длины. Ожидаемая практическая длина base64url tx id - 43 байта, но формат хранит длину явно.
|
||||
- Все типизированные блоки после фиксированного заголовка начинаются с `block_type: u8` и `block_version: u8`.
|
||||
- Отдельный `block_len` у типизированных блоков не хранится: блоки парсятся по известным полям, счетчикам и строкам с `len: u8`.
|
||||
|
||||
## 4. Верхний формат записи
|
||||
|
||||
Первые 9 полей фиксированы и идут строго в указанном порядке. Это общий заголовок записи.
|
||||
|
||||
| N | Поле | Тип | Размер | Правило |
|
||||
|---|------|-----|--------|---------|
|
||||
| 1 | `magic` | bytes | 5 | Всегда `SHiNE`. |
|
||||
| 2 | `format_major` | `u8` | 1 | Для первого формата: `1`. |
|
||||
| 3 | `format_minor` | `u8` | 1 | Для первой версии нового формата: `0`. |
|
||||
| 4 | `record_len` | `u16` | 2 | Длина полезной записи от `magic` до `signature` включительно, без padding. |
|
||||
| 5 | `created_at_ms` | `u64` | 8 | Время создания записи, Unix time в миллисекундах. Не меняется. |
|
||||
| 6 | `updated_at_ms` | `u64` | 8 | Время последнего обновления записи. |
|
||||
| 7 | `record_number` | `u32` | 4 | Номер версии записи пользователя. При создании `0`, при обновлении +1. |
|
||||
| 8 | `prev_record_hash` | bytes | 32 | Хэш unsigned-части предыдущей записи. При создании 32 нулевых байта. |
|
||||
| 9 | `login` | string | `1 + len` | Логин пользователя. Не меняется. |
|
||||
|
||||
После первых 9 полей идет набор типизированных блоков:
|
||||
|
||||
```text
|
||||
UserPdaRecordV1
|
||||
- fixed_header: поля 1..9
|
||||
- blocks_count: u8
|
||||
- blocks: TypedBlock[blocks_count]
|
||||
- signature: [u8; 64]
|
||||
- padding: bytes до размера PDA, если нужен
|
||||
```
|
||||
|
||||
`blocks_count` входит в unsigned-часть записи и подписывается.
|
||||
|
||||
## 5. Типы блоков
|
||||
|
||||
Зарезервированные значения `block_type`:
|
||||
|
||||
| block_type | Блок | Назначение |
|
||||
|------------|------|------------|
|
||||
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
|
||||
| `2` | `DeviceKeyBlock` | Ключ устройства пользователя. |
|
||||
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
||||
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
||||
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
||||
| `50` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
||||
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
||||
|
||||
Правила:
|
||||
|
||||
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
|
||||
- обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`;
|
||||
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `TrustedStateBlock`;
|
||||
- каждый обязательный блок должен встречаться ровно один раз;
|
||||
- порядок блоков в записи фиксируется для простоты проверки:
|
||||
`RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `TrustedState`.
|
||||
|
||||
## 6. RootKeyBlock
|
||||
|
||||
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
|
||||
|
||||
```text
|
||||
RootKeyBlock
|
||||
- block_type: u8 = 1
|
||||
- block_version: u8 = 0
|
||||
- root_key: [u8; 32]
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- при создании задается корневой публичный ключ пользователя;
|
||||
- при обновлении `root_key` должен совпадать с предыдущей записью;
|
||||
- ротация root-key будет отдельным форматом/сценарием в будущем.
|
||||
|
||||
## 7. DeviceKeyBlock
|
||||
|
||||
Смена `device_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один ключ устройства.
|
||||
|
||||
```text
|
||||
DeviceKeyBlock
|
||||
- block_type: u8 = 2
|
||||
- block_version: u8 = 0
|
||||
- device_key: [u8; 32]
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- при создании задается текущий публичный ключ устройства;
|
||||
- при обновлении ключ устройства может быть обновлен только если это отдельно разрешено бизнес-логикой инструкции;
|
||||
- история устройств и несколько устройств в этом формате не хранятся.
|
||||
|
||||
## 8. BlockchainRegistryBlock
|
||||
|
||||
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
|
||||
|
||||
```text
|
||||
BlockchainRegistryBlock
|
||||
- block_type: u8 = 3
|
||||
- block_version: u8 = 0
|
||||
- blockchain_count: u8
|
||||
- blockchain_records: BlockchainRecord[blockchain_count]
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- на первом этапе `blockchain_count = 1`;
|
||||
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
|
||||
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
|
||||
|
||||
## 9. BlockchainRecord
|
||||
|
||||
```text
|
||||
BlockchainRecord
|
||||
- blockchain_type: u8
|
||||
- blockchain_name: string
|
||||
- blockchain_public_key: [u8; 32]
|
||||
- paid_limit_bytes: u64
|
||||
- used_bytes: u64
|
||||
- last_block_number: u32
|
||||
- last_block_hash: [u8; 32]
|
||||
- last_block_signature: [u8; 64]
|
||||
- arweave_present: u8
|
||||
- arweave_tx_id: string, только если arweave_present = 1
|
||||
```
|
||||
|
||||
`blockchain_type`:
|
||||
|
||||
| Значение | Смысл |
|
||||
|----------|-------|
|
||||
| `1` | Основной пользовательский SHiNE-блокчейн. |
|
||||
|
||||
Поля:
|
||||
|
||||
- `blockchain_name` - строковое имя пользовательского блокчейна, например `login-001`. На первом этапе для основного блокчейна пользователя используется имя вида `<login>-001`, потому что это первый блокчейн этого пользователя.
|
||||
- `blockchain_public_key` - публичный ключ блокчейна пользователя.
|
||||
- `paid_limit_bytes` - оплаченный лимит хранения/записей в байтах.
|
||||
- `used_bytes` - сколько байт уже занято в пользовательском SHiNE-блокчейне.
|
||||
- `last_block_number` - номер последнего известного блока пользовательского блокчейна.
|
||||
- `last_block_hash` - хэш последнего известного блока.
|
||||
- `last_block_signature` - подпись хэша специального сообщения о вершине блокчейна ключом `blockchain_public_key`.
|
||||
- `arweave_present` - `0`, если ссылки нет; `1`, если ссылка есть.
|
||||
- `arweave_tx_id` - Arweave transaction id, где лежит выгруженный пользовательский канал/состояние.
|
||||
|
||||
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
|
||||
|
||||
## 10. Правила обновления BlockchainRecord
|
||||
|
||||
При обновлении записи:
|
||||
|
||||
- `blockchain_type` для существующей записи не меняется;
|
||||
- `blockchain_public_key` пока не ротируется автоматически; смена ключа требует отдельного согласованного сценария;
|
||||
- `paid_limit_bytes` может только увеличиваться или оставаться прежним;
|
||||
- при увеличении `paid_limit_bytes` пользователь платит комиссию в Solana по тарифам программы;
|
||||
- `used_bytes` может только увеличиваться или оставаться прежним;
|
||||
- `last_block_number` может только увеличиваться или оставаться прежним;
|
||||
- `used_bytes <= paid_limit_bytes`;
|
||||
- если `last_block_number` увеличился, то должны быть переданы новый `last_block_hash` и новая `last_block_signature`;
|
||||
- `last_block_signature` проверяется через Ed25519-инструкцию Solana: подпись должна соответствовать хэшу сообщения `LastBlockState` и `blockchain_public_key`;
|
||||
- `arweave_tx_id` можно добавить или заменить на новый, если пользователь выгрузил более актуальное состояние в Arweave;
|
||||
- уменьшать лимит, число блоков или занятый размер нельзя.
|
||||
|
||||
Сообщение `LastBlockState`, которое хэшируется и подписывается ключом `blockchain_public_key`:
|
||||
|
||||
```text
|
||||
LastBlockState
|
||||
- constant: bytes = "SHiNE_LAST_BLOCK"
|
||||
- login: string
|
||||
- blockchain_name: string
|
||||
- last_block_number: u32
|
||||
- last_block_hash: [u8; 32]
|
||||
- used_bytes: u64
|
||||
```
|
||||
|
||||
Алгоритм:
|
||||
|
||||
```text
|
||||
message = SHA-256(LastBlockState bytes)
|
||||
last_block_signature = Ed25519(blockchain_public_key, message)
|
||||
```
|
||||
|
||||
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
|
||||
|
||||
## 11. ServerProfileBlock
|
||||
|
||||
Блок присутствует, если пользователь выступает сервером.
|
||||
|
||||
```text
|
||||
ServerProfileBlock
|
||||
- block_type: u8 = 30
|
||||
- block_version: u8 = 0
|
||||
- is_server: u8
|
||||
- server_key: [u8; 32], только если is_server = 1
|
||||
- server_address: string, только если is_server = 1
|
||||
- sync_servers_count: u8, только если is_server = 1
|
||||
- sync_servers: string[sync_servers_count], только если is_server = 1
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- `is_server = 0` означает, что серверных данных нет;
|
||||
- `is_server = 1` означает, что пользователь публикует серверный профиль;
|
||||
- `sync_servers_count` максимум `32`;
|
||||
- `server_address` - строковый адрес сервера в формате, который будет отдельно закреплен на уровне приложения;
|
||||
- `sync_servers` - логины пользователей системы, через которых этот сервер пытается синхронизироваться. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
|
||||
|
||||
## 12. AccessServersBlock
|
||||
|
||||
Блок хранит серверы доступа/relay для пользователя.
|
||||
|
||||
```text
|
||||
AccessServersBlock
|
||||
- block_type: u8 = 40
|
||||
- block_version: u8 = 0
|
||||
- access_servers_count: u8
|
||||
- access_servers: string[access_servers_count]
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- блок может отсутствовать, если серверы доступа не заданы;
|
||||
- список может обновляться при изменении маршрутизации пользователя;
|
||||
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
||||
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
|
||||
|
||||
## 13. TrustedStateBlock
|
||||
|
||||
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
|
||||
|
||||
```text
|
||||
TrustedStateBlock
|
||||
- block_type: u8 = 50
|
||||
- block_version: u8 = 0
|
||||
- trusted_count: u8 = 0
|
||||
```
|
||||
|
||||
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
|
||||
|
||||
## 14. Подпись user_pda
|
||||
|
||||
Подписывается не вся PDA целиком, а unsigned-часть записи:
|
||||
|
||||
- от `magic` до последнего байта последнего типизированного блока включительно;
|
||||
- включая `record_len`, `blocks_count`, все заголовки блоков и тела блоков;
|
||||
- без поля `signature`;
|
||||
- без padding.
|
||||
|
||||
Алгоритм:
|
||||
|
||||
```text
|
||||
message = hash(unsigned_record_bytes)
|
||||
signature = Ed25519(root_key, message)
|
||||
```
|
||||
|
||||
Solana-программа проверяет подпись через встроенную Ed25519-инструкцию. Подписантом должен быть `root_key` из `RootKeyBlock`.
|
||||
|
||||
Смену формата подписи сейчас не трогаем.
|
||||
|
||||
## 15. Регистрация пользователя
|
||||
|
||||
При регистрации:
|
||||
|
||||
- PDA еще не должна существовать;
|
||||
- логин проходит проверку формата и login guard;
|
||||
- `record_number = 0`;
|
||||
- `prev_record_hash = 0x00...00`;
|
||||
- `created_at_ms = updated_at_ms`;
|
||||
- обязательные блоки присутствуют;
|
||||
- создается минимум один `BlockchainRecord`;
|
||||
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
|
||||
- `used_bytes <= paid_limit_bytes`;
|
||||
- пользователь платит регистрационную комиссию;
|
||||
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
|
||||
- вся unsigned-часть записи подписана `root_key`.
|
||||
|
||||
## 16. Обновление пользователя
|
||||
|
||||
При обновлении:
|
||||
|
||||
- PDA должна существовать;
|
||||
- `login`, `created_at_ms`, `root_key` не меняются;
|
||||
- `record_number = previous_record_number + 1`;
|
||||
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
|
||||
- `updated_at_ms` обновляется;
|
||||
- unsigned-часть новой записи подписана `root_key`;
|
||||
- лимиты блокчейнов могут только увеличиваться;
|
||||
- занятый размер и номер последнего блока не могут уменьшаться;
|
||||
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
|
||||
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
|
||||
|
||||
## 17. Отличия от старого линейного формата
|
||||
|
||||
Старый формат после `login` хранил поля линейно:
|
||||
|
||||
- `root_key_status`;
|
||||
- `root_key`;
|
||||
- `blockchain_key_status`;
|
||||
- `blockchain_key`;
|
||||
- `device_key_status`;
|
||||
- `device_key`;
|
||||
- `chain_number`;
|
||||
- `balance`;
|
||||
- серверные поля;
|
||||
- access-серверы;
|
||||
- `trusted_count`;
|
||||
- `reserved`;
|
||||
- `signature`.
|
||||
|
||||
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
|
||||
|
||||
- ключи становятся отдельными блоками;
|
||||
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
|
||||
- серверные данные и access-серверы отделяются от данных блокчейна;
|
||||
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
|
||||
|
||||
## 18. Что пока не входит в формат
|
||||
|
||||
Пока не проектируем:
|
||||
|
||||
- ротацию `root_key`;
|
||||
- сложную ротацию `device_key`;
|
||||
- ротацию `blockchain_public_key`;
|
||||
- проверку содержимого Arweave transaction;
|
||||
- хранение полной истории пользовательского блокчейна внутри Solana;
|
||||
- подключение Solana-модуля к сборке/деплою основного сервера SHiNE.
|
||||
@ -1,166 +0,0 @@
|
||||
# Архитектура Solana-программ SHiNE
|
||||
|
||||
Документ описывает рабочую архитектуру Solana-части SHiNE: три Anchor-программы, DAO, ключи управления, PDA-счета и движение денег.
|
||||
|
||||
Это архитектурная справка. Она не меняет код, формат PDA-записи пользователя, серверный API или формат блокчейна SHiNE.
|
||||
|
||||
Статус: актуализировано по коду `shine-solana/shine/programs/*` на 2026-05-25.
|
||||
|
||||
Связанные документы:
|
||||
|
||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` — single source of truth по деплою и первичной инициализации регистрации пользователей.
|
||||
- `shine-solana/shine/doc/SHiNE-user-format-v.1.0.md` — точный формат `user_pda` для `shine_users`.
|
||||
- `shine-solana/shine/doc/FUNDS_FLOW.md` — короткая справка по денежным потокам внутри Solana-модуля.
|
||||
|
||||
## Кратко
|
||||
|
||||
В Solana-модуле сейчас три основные программы:
|
||||
|
||||
1. `shine_login_guard` — проверяет логин и возвращает класс логина: обычный, premium или trademark.
|
||||
2. `shine_users` — создает и обновляет пользовательскую PDA-запись, проверяет подписи и берет оплату за регистрацию/увеличение лимита.
|
||||
3. `shine_payments` — принимает входящий поток средств в `inflow_vault`, ведет очереди тикетов, позволяет DAO выдавать лимиты менеджерам и выполняет выплаты.
|
||||
|
||||
DAO в текущем виде не является отдельной Anchor-программой SHiNE внутри `programs/`. Это управляющая модель поверх кошельков, governance-скриптов и authority-адресов. Для проектирования ее удобно считать отдельным управляющим блоком: DAO голосует, назначает управляющие ключи, управляет казной и вызывает защищенные методы второй и третьей программ.
|
||||
|
||||
## Общая схема
|
||||
|
||||
Редактируемая Mermaid-схема находится в [schemes/architecture.mmd](schemes/architecture.mmd).
|
||||
|
||||
Картинки:
|
||||
|
||||
- [schemes/architecture.svg](schemes/architecture.svg)
|
||||
- [schemes/architecture.png](schemes/architecture.png)
|
||||
|
||||
## Программы и функции
|
||||
|
||||
| Блок | Папка/имя | Текущие функции из кода | Основной смысл |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `shine_login_guard` | `classify_login` | Проверка логина перед регистрацией. |
|
||||
| 2 | `shine_users` | `init_users_economy_config`, `update_users_economy_config`, `create_user_pda`, `update_user_pda` | Регистрация пользователя, обновление записи, экономика лимита. |
|
||||
| 3 | `shine_payments` | `init`, `update_coef_limit`, `grant_manager_limits`, `buy_ticket`, `buy_ticket_usd`, `buy_ticket_sol`, `manager_add_ticket`, `step_payout`, `change_ticket_recipient` | Vault, билеты, очереди, выплаты, DAO-настройки, лимиты менеджеров. |
|
||||
| DAO | governance/authority | Вызовы через governance и управляющие ключи | Управление правами, казной, настройками и будущими обновлениями программ. |
|
||||
|
||||
## Актуальные program id
|
||||
|
||||
Актуальные адреса заданы одновременно в `Anchor.toml`, `declare_id!` программ и `programs/common/src/deploy_config.rs`:
|
||||
|
||||
| Программа | Program ID |
|
||||
| --- | --- |
|
||||
| `shine_login_guard` | `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` |
|
||||
| `shine_users` | `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` |
|
||||
| `shine_payments` | `m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR` |
|
||||
|
||||
Если эти адреса меняются, нужно синхронно обновить:
|
||||
|
||||
1. `shine-solana/shine/Anchor.toml`
|
||||
2. `declare_id!` в `programs/*/src/lib.rs`
|
||||
3. `programs/common/src/deploy_config.rs`
|
||||
4. UI/серверные константы, перечисленные в `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
||||
|
||||
## Ключи и authority
|
||||
|
||||
Для удобного понимания на старте можно считать, что есть четыре группы ключей:
|
||||
|
||||
1. `key_1` / authority программы `shine_login_guard`.
|
||||
- Сейчас программа только классифицирует логин.
|
||||
- На первом этапе ее можно оставить под отдельным ключом.
|
||||
- В будущем право обновления можно передать DAO.
|
||||
|
||||
2. `key_2` / authority программы `shine_users`.
|
||||
- Отвечает за деплой/upgrade второй программы.
|
||||
- Защищенное обновление economy-конфига в коде уже проверяет `DAO_AUTHORITY`.
|
||||
- В целевой модели upgrade-authority второй программы нужно передать DAO.
|
||||
|
||||
3. `key_3` / authority программы `shine_payments`.
|
||||
- Отвечает за деплой/upgrade третьей программы.
|
||||
- Защищенные методы `update_coef_limit` и `grant_manager_limits` проверяют `dao_wallet` из `ConfigState`.
|
||||
- В целевой модели upgrade-authority третьей программы нужно передать DAO.
|
||||
|
||||
4. DAO-ключи.
|
||||
- Это управляющие кошельки/токены/realm governance.
|
||||
- DAO может добавлять и отзывать управляющие ключи по голосованию.
|
||||
- DAO-казна получает деньги от покупки тикетов и DAO-часть выплат из `inflow_vault`.
|
||||
|
||||
Адреса program id сейчас берутся из `programs/common/src/deploy_config.rs`. Для production/devnet можно подбирать vanity-адреса с понятным началом вроде `SHi...`, но это отдельная операция генерации ключей и деплоя.
|
||||
|
||||
## Счета и PDA
|
||||
|
||||
Постоянные PDA и счета:
|
||||
|
||||
1. `shine_users`
|
||||
- `user_pda` — пользовательская запись по seed `login=<login>`, создается для каждого логина.
|
||||
- `users_economy_config_pda` — общие параметры экономики регистрации и лимита.
|
||||
|
||||
2. `shine_payments`
|
||||
- `config_pda` — хранит `dao_wallet` и адрес `inflow_vault`.
|
||||
- `coef_limit_pda` — хранит коэффициент выплат, лимит очереди и награду вызывающему `step_payout`.
|
||||
- `queues_pda` — агрегаты очередей выплат.
|
||||
- `inflow_vault_pda` — PDA-вольт, куда `shine_users` переводит оплату регистрации и увеличения лимита.
|
||||
- `ticket_pda` — отдельная PDA-запись тикета на каждую покупку/менеджерскую выдачу.
|
||||
- `manager_allowance_pda` — PDA лимитов конкретного менеджера.
|
||||
|
||||
3. DAO
|
||||
- `dao_wallet` / treasury — казна DAO.
|
||||
- governance-аккаунты DAO — realm, governance, proposal/vote records и связанные аккаунты SPL Governance, если используется эта модель.
|
||||
|
||||
## Правило разделения с основным сервером
|
||||
|
||||
Solana-модуль лежит в основном репозитории как отдельная папка `shine-solana/shine/`, но не подключается автоматически к сборке или деплою основного сервера SHiNE. Команды `deployServer` и `deployUI` не должны деплоить Anchor-программы. Solana build/deploy выполняется отдельно из папки `shine-solana/shine/` по локальным правилам модуля.
|
||||
|
||||
## Движение денег
|
||||
|
||||
Основные потоки:
|
||||
|
||||
1. Регистрация пользователя через `shine_users::create_user_pda`.
|
||||
- Платит `signer`.
|
||||
- Деньги идут в `shine_payments::inflow_vault_pda`.
|
||||
- Сумма состоит из регистрационной комиссии и оплаты дополнительного лимита.
|
||||
|
||||
2. Увеличение лимита через `shine_users::update_user_pda`.
|
||||
- Платит `signer`.
|
||||
- Деньги идут в тот же `inflow_vault_pda`.
|
||||
- Сумма равна оплате дополнительного лимита.
|
||||
|
||||
3. Покупка тикета через `shine_payments::buy_ticket*`.
|
||||
- Платит покупатель.
|
||||
- Деньги сразу идут в `dao_wallet`.
|
||||
- Одновременно создается тикет на выплату.
|
||||
|
||||
4. Выплата через `shine_payments::step_payout`.
|
||||
- Вызвать может любой подписант.
|
||||
- Деньги берутся из `inflow_vault_pda`.
|
||||
- Часть идет получателю тикета.
|
||||
- Часть идет в `dao_wallet`.
|
||||
- Небольшая награда идет вызвавшему шаг выплат.
|
||||
- Если очереди пустые, весь доступный остаток `inflow_vault_pda` переводится в DAO.
|
||||
|
||||
## Передача прав DAO
|
||||
|
||||
Минимальная целевая модель:
|
||||
|
||||
1. `shine_login_guard`
|
||||
- Пока оставить на отдельном ключе `key_1`.
|
||||
- Передачу DAO сделать позже, когда логика premium/trademark стабилизируется.
|
||||
|
||||
2. `shine_users`
|
||||
- Economy-настройки уже должны обновляться DAO-authority.
|
||||
- Upgrade-authority программы после проверки можно передать DAO.
|
||||
|
||||
3. `shine_payments`
|
||||
- DAO уже управляет настройками выплат и лимитами менеджеров через `dao_wallet`.
|
||||
- Upgrade-authority программы после проверки можно передать DAO.
|
||||
|
||||
4. DAO
|
||||
- Управляет казной.
|
||||
- Принимает решения голосованием.
|
||||
- Добавляет/отзывает управляющие ключи.
|
||||
- Вызывает защищенные методы второй и третьей программ.
|
||||
- В будущем может принять управление первой программой.
|
||||
|
||||
## Детальные файлы
|
||||
|
||||
- [details/shine_login_guard.md](details/shine_login_guard.md)
|
||||
- [details/shine_users.md](details/shine_users.md)
|
||||
- [details/shine_payments.md](details/shine_payments.md)
|
||||
- [details/shine_dao.md](details/shine_dao.md)
|
||||
- [details/accounts_and_money_flow.md](details/accounts_and_money_flow.md)
|
||||
@ -1,110 +0,0 @@
|
||||
# Счета, ключи и движение денег
|
||||
|
||||
## Кратко
|
||||
|
||||
В архитектуре есть три типа объектов:
|
||||
|
||||
1. Ключи программ и DAO.
|
||||
2. PDA-счета состояния.
|
||||
3. Денежные счета, через которые проходят SOL/lamports.
|
||||
|
||||
## Ключи
|
||||
|
||||
Минимальный набор для понимания:
|
||||
|
||||
1. `key_1` — deploy/upgrade authority `shine_login_guard`.
|
||||
2. `key_2` — deploy/upgrade authority `shine_users`.
|
||||
3. `key_3` — deploy/upgrade authority `shine_payments`.
|
||||
4. `DAO_AUTHORITY` — адрес, который имеет право менять защищенные настройки.
|
||||
5. `DAO_TREASURY_WALLET` / `dao_wallet` — казна DAO.
|
||||
6. `manager_wallet` — кошелек менеджера, которому DAO выдает лимиты на создание тикетов.
|
||||
7. `user root_key` — корневой ключ пользователя для подписи пользовательской записи.
|
||||
8. `user device_key` — ключ устройства пользователя.
|
||||
9. `server_key` — ключ сервера пользователя, если пользователь является сервером.
|
||||
|
||||
Текущие адреса из `programs/common/src/deploy_config.rs`:
|
||||
|
||||
| Роль | Адрес |
|
||||
| --- | --- |
|
||||
| `SHINE_LOGIN_GUARD_PROGRAM_ID` | `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` |
|
||||
| `SHINE_USERS_PROGRAM_ID` | `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` |
|
||||
| `SHINE_PAYMENTS_PROGRAM_ID` | `m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR` |
|
||||
| `DAO_AUTHORITY` | `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P` |
|
||||
| `DAO_TREASURY_WALLET` | `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P` |
|
||||
|
||||
## Постоянные PDA
|
||||
|
||||
`shine_users`:
|
||||
|
||||
- `user_pda` — создается для каждого логина, seed `login=` + normalized login.
|
||||
- `users_economy_config_pda` — один PDA с экономикой регистрации, seed `shine_users_economy_config`.
|
||||
|
||||
`shine_payments`:
|
||||
|
||||
- `config_pda` — один PDA конфига, seed `shine_payments_config`.
|
||||
- `coef_limit_pda` — один PDA коэффициента/лимита/награды, seed `shine_payments_coef_limit`.
|
||||
- `queues_pda` — один PDA агрегатов очередей, seed `shine_payments_queues`.
|
||||
- `inflow_vault_pda` — один PDA-вольт входящих средств, seed `shine_payments_inflow_vault`.
|
||||
- `ticket_pda` — много PDA, по одному на тикет, seed `shine_payments_q1_ticket` или `shine_payments_q2_ticket` + индекс.
|
||||
- `manager_allowance_pda` — много PDA, по одному на менеджера, seed `shine_p_manager_allow` + адрес менеджера.
|
||||
|
||||
## Денежные потоки
|
||||
|
||||
### Регистрация
|
||||
|
||||
```text
|
||||
user signer -> shine_users::create_user_pda -> shine_payments::inflow_vault_pda
|
||||
```
|
||||
|
||||
Состав платежа:
|
||||
|
||||
- регистрационная комиссия;
|
||||
- оплата `additional_limit`.
|
||||
|
||||
### Увеличение лимита
|
||||
|
||||
```text
|
||||
user signer -> shine_users::update_user_pda -> shine_payments::inflow_vault_pda
|
||||
```
|
||||
|
||||
Состав платежа:
|
||||
|
||||
- только оплата `additional_limit`.
|
||||
|
||||
### Покупка тикета
|
||||
|
||||
```text
|
||||
buyer signer -> shine_payments::buy_ticket* -> dao_wallet
|
||||
```
|
||||
|
||||
При этом создается `ticket_pda`, но деньги в `inflow_vault_pda` на этом шаге не идут.
|
||||
|
||||
### Выплата
|
||||
|
||||
```text
|
||||
shine_payments::inflow_vault_pda -> ticket_recipient_wallet
|
||||
shine_payments::inflow_vault_pda -> dao_wallet
|
||||
shine_payments::inflow_vault_pda -> step_payout caller
|
||||
```
|
||||
|
||||
Если очереди пустые:
|
||||
|
||||
```text
|
||||
shine_payments::inflow_vault_pda -> dao_wallet
|
||||
```
|
||||
|
||||
## Что нужно создать на старте
|
||||
|
||||
Минимально:
|
||||
|
||||
1. Три program id для `shine_login_guard`, `shine_users`, `shine_payments`.
|
||||
2. Три upgrade-authority ключа или один временный deploy-ключ с четким планом передачи прав.
|
||||
3. DAO authority/treasury.
|
||||
4. `users_economy_config_pda`.
|
||||
5. `shine_payments` PDA: `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
|
||||
|
||||
Динамически будут создаваться:
|
||||
|
||||
- `user_pda` на каждого пользователя;
|
||||
- `ticket_pda` на каждый тикет;
|
||||
- `manager_allowance_pda` на каждого менеджера.
|
||||
@ -1,74 +0,0 @@
|
||||
# SHiNE DAO
|
||||
|
||||
## Кратко
|
||||
|
||||
DAO — управляющий слой Solana-части SHiNE. В текущем коде это не отдельная Anchor-программа в `programs/`, а модель управления через DAO-кошелек, DAO-authority, governance-скрипты и будущую передачу upgrade-authority программ.
|
||||
|
||||
## Что DAO должно уметь
|
||||
|
||||
1. Управлять казной.
|
||||
- Принимать средства на `dao_wallet`.
|
||||
- Выплачивать средства со счета DAO по решениям голосования.
|
||||
|
||||
2. Управлять настройками `shine_users`.
|
||||
- Обновлять регистрационную комиссию.
|
||||
- Обновлять цену шага лимита.
|
||||
- Обновлять стартовый бонус лимита.
|
||||
|
||||
3. Управлять настройками `shine_payments`.
|
||||
- Обновлять коэффициент выплат.
|
||||
- Обновлять лимит очереди.
|
||||
- Обновлять награду за вызов `step_payout`.
|
||||
|
||||
4. Управлять менеджерами.
|
||||
- Выдавать менеджеру лимит на добавление тикетов.
|
||||
- Отдельно учитывать лимиты Q1 и Q2.
|
||||
|
||||
5. Управлять правами программ.
|
||||
- Принять upgrade-authority `shine_users`.
|
||||
- Принять upgrade-authority `shine_payments`.
|
||||
- Позже принять upgrade-authority `shine_login_guard`, если это потребуется.
|
||||
|
||||
6. Управлять ключами DAO.
|
||||
- Добавлять управляющие ключи.
|
||||
- Отзывать или сжигать управляющие ключи.
|
||||
- Делать это через голосование, а не вручную одним админом.
|
||||
|
||||
7. Фиксировать решения.
|
||||
- Делать заявления/решения через governance-механику.
|
||||
- Привязывать важные изменения к proposal/vote/execute.
|
||||
|
||||
## Текущие адреса управления
|
||||
|
||||
В общем deploy-конфиге сейчас есть два важных адреса:
|
||||
|
||||
- `DAO_AUTHORITY` — используется `shine_users` для проверки права менять economy-конфиг.
|
||||
- `DAO_TREASURY_WALLET` — используется `shine_payments` как `dao_wallet`.
|
||||
|
||||
Сейчас они могут совпадать. В целевой DAO-модели их лучше рассматривать как разные роли:
|
||||
|
||||
- authority/governance signer — кто имеет право исполнять управленческие инструкции;
|
||||
- treasury wallet — счет, куда приходят деньги DAO.
|
||||
|
||||
## Передача прав
|
||||
|
||||
Рекомендуемый порядок:
|
||||
|
||||
1. Сначала стабилизировать и проверить `shine_users` и `shine_payments`.
|
||||
2. Передать DAO право обновлять настройки, если оно еще не передано.
|
||||
3. Передать DAO upgrade-authority второй и третьей программ.
|
||||
4. Оставить `shine_login_guard` на отдельном ключе до стабилизации словарей и правил логинов.
|
||||
5. После стабилизации решить отдельным голосованием, передавать ли первую программу DAO.
|
||||
|
||||
## Важное разделение
|
||||
|
||||
Есть два разных типа прав:
|
||||
|
||||
1. Право вызвать защищенную функцию программы.
|
||||
- Например, `update_coef_limit` или `grant_manager_limits`.
|
||||
- Проверяется внутри программы по `dao_wallet` или `DAO_AUTHORITY`.
|
||||
|
||||
2. Право обновить саму программу.
|
||||
- Это upgrade-authority Solana ProgramData.
|
||||
- Оно передается отдельной Solana-командой/DAO-транзакцией и не равно обычному PDA-счету.
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
# `shine_login_guard`
|
||||
|
||||
## Кратко
|
||||
|
||||
`shine_login_guard` — первая программа Solana-модуля SHiNE. Она проверяет логин перед регистрацией пользователя и возвращает класс логина.
|
||||
|
||||
Папка программы: `shine-solana/shine/programs/shine_login_guard/`.
|
||||
|
||||
## Текущая функция
|
||||
|
||||
1. `classify_login(login: String)`
|
||||
- Нормализует логин.
|
||||
- Проверяет длину и допустимые символы.
|
||||
- Сравнивает части логина со словарями premium/trademark.
|
||||
- Возвращает результат через `set_return_data`.
|
||||
|
||||
Классы результата:
|
||||
|
||||
- `0` — обычный логин, регистрацию можно продолжать.
|
||||
- `1` — premium-логин.
|
||||
- `2` — trademark-логин, нужна отдельная проверка/разрешение.
|
||||
|
||||
## Правила нормализации и классификации
|
||||
|
||||
Текущая логика из `programs/shine_login_guard/src/lib.rs`:
|
||||
|
||||
- пустой логин или логин длиннее 20 символов получает класс `premium`;
|
||||
- `_` при нормализации удаляется;
|
||||
- допустимы только ASCII-буквы и цифры, остальные символы дают класс `premium`;
|
||||
- после удаления `_` результат приводится к нижнему регистру;
|
||||
- логины длиной 7 символов или меньше считаются `premium`;
|
||||
- логин разбивается максимум на 3 словарных фрагмента;
|
||||
- если среди найденных фрагментов есть trademark-слово, результат `trademark`;
|
||||
- если найдены только premium-слова, результат `premium`;
|
||||
- если разбиение по словарям не найдено, результат `free`.
|
||||
|
||||
Словари собираются на этапе build из файлов:
|
||||
|
||||
- `programs/shine_login_guard/src/dictionaries/premium/*.txt`
|
||||
- `programs/shine_login_guard/src/dictionaries/trademarks/*.txt`
|
||||
|
||||
## Роль в общей схеме
|
||||
|
||||
`shine_users::create_user_pda` вызывает `shine_login_guard` через CPI и продолжает регистрацию только если логин получил класс `0`.
|
||||
|
||||
## Ключи и управление
|
||||
|
||||
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_1`.
|
||||
|
||||
Текущая рекомендация:
|
||||
|
||||
- пока оставить `shine_login_guard` под отдельным ключом;
|
||||
- не передавать ее DAO до стабилизации правил premium/trademark;
|
||||
- позже можно передать upgrade-authority DAO, чтобы изменения словарей и правил проходили через голосование.
|
||||
|
||||
## Счета
|
||||
|
||||
Собственных постоянных PDA-счетов у программы сейчас нет. Для проверки нужен только подписант транзакции в `ClassifyLogin`.
|
||||
@ -1,173 +0,0 @@
|
||||
# `shine_payments`
|
||||
|
||||
## Кратко
|
||||
|
||||
`shine_payments` — третья программа Solana-модуля SHiNE. Она отвечает за vault входящих средств, DAO-казну, покупку тикетов, менеджерские лимиты, очереди выплат и пошаговое исполнение выплат.
|
||||
|
||||
Папка программы: `shine-solana/shine/programs/shine_payments/`.
|
||||
|
||||
## Текущие функции
|
||||
|
||||
1. `init`
|
||||
- Создает основные PDA: `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
|
||||
- Записывает `dao_wallet` и стартовые параметры выплат.
|
||||
|
||||
2. `update_coef_limit`
|
||||
- Обновляет коэффициент выплаты, лимит очереди и награду вызвавшему `step_payout`.
|
||||
- Требует подпись DAO-кошелька из `ConfigState`.
|
||||
|
||||
3. `grant_manager_limits`
|
||||
- DAO выдает менеджеру лимиты на создание тикетов в очередях Q1/Q2.
|
||||
- Создает или обновляет `manager_allowance_pda`.
|
||||
|
||||
4. `buy_ticket`
|
||||
- Покупка тикета с суммой в lamports, пересчетом через Pyth SOL/USD.
|
||||
|
||||
5. `buy_ticket_usd`
|
||||
- Покупка тикета от USD-центов с защитой по максимальному платежу в lamports.
|
||||
|
||||
6. `buy_ticket_sol`
|
||||
- Покупка тикета в lamports с проверкой минимального ожидаемого USD-эквивалента.
|
||||
|
||||
7. `manager_add_ticket`
|
||||
- Менеджер создает тикет за счет выданного ему DAO-лимита.
|
||||
|
||||
8. `step_payout`
|
||||
- Любой подписант может вызвать шаг выплат.
|
||||
- Программа выплачивает следующий тикет, DAO-часть и награду вызывающему.
|
||||
|
||||
9. `change_ticket_recipient`
|
||||
- Текущий получатель тикета может поменять адрес получателя, если тикет еще не следующий на выплату.
|
||||
|
||||
## Аргументы инструкций
|
||||
|
||||
`init` аргументов не принимает.
|
||||
|
||||
`update_coef_limit`:
|
||||
|
||||
- `coef_ppm: u64`
|
||||
- `limit_usd_cents: u64`
|
||||
- `call_reward_lamports: u64`
|
||||
|
||||
`grant_manager_limits`:
|
||||
|
||||
- `manager_wallet: Pubkey`
|
||||
- `add_q1_usd_cents: u64`
|
||||
- `add_q2_usd_cents: u64`
|
||||
|
||||
`buy_ticket`:
|
||||
|
||||
- `amount_lamports: u64`
|
||||
- `recipient_wallet: Pubkey`
|
||||
|
||||
`buy_ticket_usd`:
|
||||
|
||||
- `amount_usd_cents: u64`
|
||||
- `max_pay_lamports: u64`
|
||||
- `recipient_wallet: Pubkey`
|
||||
|
||||
`buy_ticket_sol`:
|
||||
|
||||
- `amount_lamports: u64`
|
||||
- `min_expected_usd_cents: u64`
|
||||
- `recipient_wallet: Pubkey`
|
||||
|
||||
`manager_add_ticket`:
|
||||
|
||||
- `queue_id: u8` — только `1` или `2`
|
||||
- `recipient_wallet: Pubkey`
|
||||
- `payout_usd_cents: u64`
|
||||
|
||||
`change_ticket_recipient`:
|
||||
|
||||
- `new_recipient_wallet: Pubkey`
|
||||
|
||||
## Главные PDA
|
||||
|
||||
1. `config_pda`
|
||||
- Seed: `shine_payments_config`.
|
||||
- Хранит `dao_wallet` и `inflow_vault`.
|
||||
- Размер PDA: `8 + 160` байт.
|
||||
|
||||
2. `coef_limit_pda`
|
||||
- Seed: `shine_payments_coef_limit`.
|
||||
- Хранит коэффициент выплат, лимит и награду `step_payout`.
|
||||
- Размер PDA: `8 + 96` байт.
|
||||
|
||||
3. `queues_pda`
|
||||
- Seed: `shine_payments_queues`.
|
||||
- Хранит агрегаты очередей Q1/Q2.
|
||||
- Размер PDA: `8 + 192` байт.
|
||||
|
||||
4. `inflow_vault_pda`
|
||||
- Seed: `shine_payments_inflow_vault`.
|
||||
- Принимает деньги от `shine_users`.
|
||||
- Из него выполняются выплаты тикетам, DAO и вызывающему `step_payout`.
|
||||
- Размер PDA: `8 + 32` байт.
|
||||
|
||||
5. `ticket_pda`
|
||||
- Seed зависит от очереди и индекса тикета.
|
||||
- Отдельная PDA-запись на каждый тикет.
|
||||
- Q1 seed: `shine_payments_q1_ticket` + `ticket_index`.
|
||||
- Q2 seed: `shine_payments_q2_ticket` + `ticket_index`.
|
||||
- Размер PDA: `8 + 160` байт.
|
||||
|
||||
6. `manager_allowance_pda`
|
||||
- Seed: `shine_p_manager_allow` + адрес менеджера.
|
||||
- Хранит доступный лимит менеджера по Q1/Q2.
|
||||
- Размер PDA: `8 + 128` байт.
|
||||
|
||||
## Текущие параметры
|
||||
|
||||
Параметры initial config из `programs/shine_payments/src/settings.rs`:
|
||||
|
||||
| Поле | Значение | Смысл |
|
||||
| --- | --- | --- |
|
||||
| `START_COEF_PPM` | `5_000_000` | коэффициент 5.0x в ppm-масштабе |
|
||||
| `START_LIMIT_USD_CENTS` | `1_000_000` | стартовый лимит Q1: 10_000 USD |
|
||||
| `START_CALL_REWARD_LAMPORTS` | `8_000_000` | награда вызвавшему `step_payout`, 0.008 SOL |
|
||||
| `MAX_CALL_REWARD_LAMPORTS` | `10_000_000` | максимум награды, 0.01 SOL |
|
||||
| `ORACLE_MAX_AGE_SECS` | `120` | максимальный возраст цены Pyth |
|
||||
|
||||
Для расчетов используется Pyth SOL/USD:
|
||||
|
||||
- feed id: `0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d`
|
||||
- price update account: `7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE`
|
||||
|
||||
## Деньги
|
||||
|
||||
Входы:
|
||||
|
||||
- из `shine_users` в `inflow_vault_pda` при регистрации и увеличении лимита;
|
||||
- от покупателя тикета сразу в `dao_wallet` при `buy_ticket*`.
|
||||
|
||||
Выходы:
|
||||
|
||||
- из `inflow_vault_pda` получателю тикета;
|
||||
- из `inflow_vault_pda` в `dao_wallet`;
|
||||
- из `inflow_vault_pda` вызвавшему `step_payout`;
|
||||
- если очереди пустые, весь доступный остаток `inflow_vault_pda` переводится в DAO.
|
||||
|
||||
## Очереди и выплаты
|
||||
|
||||
Выплаты идут строго пошагово:
|
||||
|
||||
- если есть невыплаченные Q1-тикеты, `step_payout` берет следующий Q1;
|
||||
- если Q1 пустая, берется следующий Q2;
|
||||
- для Q1 DAO-часть равна сумме тикета в USD;
|
||||
- для Q2 DAO-часть равна двойной сумме тикета в USD;
|
||||
- перед выплатой суммы пересчитываются из USD-центов в lamports по Pyth SOL/USD;
|
||||
- если в `inflow_vault_pda` не хватает средств на тикет, DAO-часть и награду вызвавшему, шаг отклоняется.
|
||||
|
||||
`change_ticket_recipient` запрещает менять получателя у тикета, который является следующим на выплату.
|
||||
|
||||
## Ключи и управление
|
||||
|
||||
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_3`.
|
||||
|
||||
Целевая модель:
|
||||
|
||||
- `update_coef_limit` вызывает DAO;
|
||||
- `grant_manager_limits` вызывает DAO;
|
||||
- upgrade-authority программы после проверки передается DAO;
|
||||
- `step_payout` остается открытым для любого подписанта, чтобы выплаты не зависели от одного оператора.
|
||||
@ -1,136 +0,0 @@
|
||||
# `shine_users`
|
||||
|
||||
## Кратко
|
||||
|
||||
`shine_users` — вторая программа Solana-модуля SHiNE. Она отвечает за создание и обновление пользовательской PDA-записи, проверку подписи записи, проверку логина через `shine_login_guard` и оплату регистрации/дополнительного лимита.
|
||||
|
||||
Папка программы: `shine-solana/shine/programs/shine_users/`.
|
||||
|
||||
## Текущие функции
|
||||
|
||||
1. `init_users_economy_config`
|
||||
- Создает PDA с экономическими настройками пользователей.
|
||||
- Записывает стартовую регистрационную комиссию, цену шага лимита и стартовый бонус лимита.
|
||||
|
||||
2. `update_users_economy_config`
|
||||
- Обновляет экономические настройки.
|
||||
- Требует подпись `DAO_AUTHORITY` из общего deploy-конфига.
|
||||
|
||||
3. `create_user_pda`
|
||||
- Проверяет логин через `shine_login_guard`.
|
||||
- Проверяет структуру полей пользователя.
|
||||
- Проверяет подпись записи root-ключом пользователя.
|
||||
- Создает `user_pda` по seed `login=<normalized_login>`.
|
||||
- Переводит оплату регистрации и дополнительного лимита в `shine_payments::inflow_vault_pda`.
|
||||
|
||||
4. `update_user_pda`
|
||||
- Проверяет неизменяемые поля пользователя.
|
||||
- Проверяет `prev_hash`, новую подпись и новое состояние последнего блока.
|
||||
- При необходимости расширяет PDA.
|
||||
- Переводит оплату дополнительного лимита в `shine_payments::inflow_vault_pda`.
|
||||
|
||||
## Аргументы инструкций
|
||||
|
||||
`init_users_economy_config` аргументов не принимает.
|
||||
|
||||
`update_users_economy_config`:
|
||||
|
||||
- `registration_fee_lamports: u64`
|
||||
- `lamports_per_limit_step: u64`
|
||||
- `start_bonus_limit: u64`
|
||||
|
||||
`create_user_pda`:
|
||||
|
||||
- `login: String`
|
||||
- `root_key: Pubkey`
|
||||
- `created_at_ms: u64`
|
||||
- `additional_limit: u64`
|
||||
- `fields: UserMutableFields`
|
||||
- `signature: Vec<u8>`
|
||||
|
||||
`update_user_pda`:
|
||||
|
||||
- `login: String`
|
||||
- `root_key: Pubkey`
|
||||
- `created_at_ms: u64`
|
||||
- `updated_at_ms: u64`
|
||||
- `version: u32`
|
||||
- `prev_hash: Vec<u8>`
|
||||
- `additional_limit: u64`
|
||||
- `fields: UserMutableFields`
|
||||
- `signature: Vec<u8>`
|
||||
|
||||
`UserMutableFields`:
|
||||
|
||||
- `device_key: Pubkey`
|
||||
- `blockchain_public_key: Pubkey`
|
||||
- `blockchain_name: String`
|
||||
- `used_bytes: u64`
|
||||
- `last_block_number: u32`
|
||||
- `last_block_hash: Vec<u8>` — ровно 32 байта
|
||||
- `last_block_signature: Vec<u8>` — ровно 64 байта
|
||||
- `arweave_tx_id: String`
|
||||
- `is_server: bool`
|
||||
- `server_key: Pubkey`
|
||||
- `server_address: String`
|
||||
- `sync_servers: Vec<String>`
|
||||
- `access_servers: Vec<String>`
|
||||
- `trusted_count: u8`
|
||||
|
||||
## Главные PDA
|
||||
|
||||
1. `user_pda`
|
||||
- PDA записи пользователя.
|
||||
- Seed: `login=<normalized_login>`.
|
||||
- Создается отдельно для каждого логина.
|
||||
- Стартовый размер: `768` байт.
|
||||
- При обновлении может расширяться через `realloc`, но один auto-realloc ограничен `10_000` байт.
|
||||
|
||||
2. `users_economy_config_pda`
|
||||
- PDA с настройками экономики.
|
||||
- Seed: `shine_users_economy_config`.
|
||||
- Хранит регистрационную комиссию, цену шага лимита и стартовый бонус.
|
||||
- Размер PDA: `8 + 96` байт.
|
||||
|
||||
## Текущие параметры экономики
|
||||
|
||||
Параметры initial config из `programs/shine_users/src/settings.rs`:
|
||||
|
||||
| Поле | Значение | Смысл |
|
||||
| --- | --- | --- |
|
||||
| `START_REGISTRATION_FEE_LAMPORTS` | `10_000_000` | стартовая комиссия регистрации, 0.01 SOL |
|
||||
| `LIMIT_STEP` | `10_000` | шаг `additional_limit` |
|
||||
| `START_LAMPORTS_PER_LIMIT_STEP` | `100_000` | 0.0001 SOL за один шаг лимита |
|
||||
| `START_BONUS_LIMIT` | `100_000` | стартовый бесплатный лимит при регистрации |
|
||||
|
||||
`additional_limit` в create/update должен быть кратен `LIMIT_STEP`.
|
||||
|
||||
## Связь с другими программами
|
||||
|
||||
`shine_users` зависит от:
|
||||
|
||||
- `shine_login_guard` — для проверки логина при создании пользователя;
|
||||
- `shine_payments` — для вычисления и проверки `inflow_vault_pda`, куда уходят платежи.
|
||||
|
||||
`create_user_pda` делает CPI-вызов `shine_login_guard::classify_login` и принимает только результат `0`. Premium/trademark логины сейчас отклоняются ошибками `PremiumLogin` или `TrademarkLoginRequiresReview`.
|
||||
|
||||
Подпись `user_pda` и подпись состояния последнего блока проверяются через встроенную Solana Ed25519-инструкцию, которая должна идти раньше инструкции `shine_users` в той же транзакции.
|
||||
|
||||
## Деньги
|
||||
|
||||
Деньги из `shine_users` идут только в `inflow_vault_pda` программы `shine_payments`.
|
||||
|
||||
Потоки:
|
||||
|
||||
- `create_user_pda`: регистрационная комиссия + оплата `additional_limit`;
|
||||
- `update_user_pda`: оплата `additional_limit`, если она больше нуля.
|
||||
|
||||
## Ключи и управление
|
||||
|
||||
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_2`.
|
||||
|
||||
Целевая модель:
|
||||
|
||||
- economy-настройки меняет DAO-authority;
|
||||
- upgrade-authority программы после проверки передается DAO;
|
||||
- пользовательские операции `create_user_pda` и `update_user_pda` остаются доступными обычным пользователям при корректных подписях и оплате.
|
||||
@ -1,54 +0,0 @@
|
||||
flowchart LR
|
||||
U[Пользователь / signer]
|
||||
B[Покупатель тикета]
|
||||
M[Менеджер]
|
||||
C[Любой caller step_payout]
|
||||
|
||||
LG[1. shine_login_guard<br/>classify_login]
|
||||
USERS[2. shine_users<br/>create_user_pda / update_user_pda]
|
||||
PAY[3. shine_payments<br/>vault / tickets / payouts]
|
||||
DAO[SHiNE DAO<br/>governance / authority / treasury]
|
||||
|
||||
USERPDA[(user_pda<br/>по login)]
|
||||
ECON[(users_economy_config_pda)]
|
||||
CONFIG[(config_pda)]
|
||||
COEF[(coef_limit_pda)]
|
||||
QUEUES[(queues_pda)]
|
||||
VAULT[(inflow_vault_pda)]
|
||||
TICKET[(ticket_pda)]
|
||||
ALLOW[(manager_allowance_pda)]
|
||||
|
||||
U -->|логин| USERS
|
||||
USERS -->|CPI проверка| LG
|
||||
USERS -->|создает/обновляет| USERPDA
|
||||
USERS -->|читает экономику| ECON
|
||||
U -->|регистрация / лимит| VAULT
|
||||
|
||||
DAO -->|update economy| USERS
|
||||
DAO -->|update coef/limit| PAY
|
||||
DAO -->|grant manager limits| PAY
|
||||
DAO -->|создает/отзывает ключи| DAO
|
||||
|
||||
PAY --> CONFIG
|
||||
PAY --> COEF
|
||||
PAY --> QUEUES
|
||||
PAY --> VAULT
|
||||
PAY --> TICKET
|
||||
PAY --> ALLOW
|
||||
|
||||
B -->|buy_ticket*| PAY
|
||||
B -->|оплата покупки тикета| DAO
|
||||
PAY -->|создает тикет| TICKET
|
||||
|
||||
M -->|manager_add_ticket| PAY
|
||||
ALLOW -->|лимиты Q1/Q2| M
|
||||
|
||||
C -->|step_payout| PAY
|
||||
VAULT -->|выплата тикета| U
|
||||
VAULT -->|DAO-часть| DAO
|
||||
VAULT -->|call reward| C
|
||||
|
||||
DAO -. upgrade authority после передачи .-> USERS
|
||||
DAO -. upgrade authority после передачи .-> PAY
|
||||
DAO -. позже возможно .-> LG
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 234 KiB |
@ -1,139 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="900" viewBox="0 0 1400 900" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Архитектура Solana-программ SHiNE</title>
|
||||
<desc id="desc">Схема трех программ, DAO, PDA-счетов и движения денег.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#2f3a45"/>
|
||||
</marker>
|
||||
<marker id="moneyArrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#0a7f62"/>
|
||||
</marker>
|
||||
<style>
|
||||
.bg { fill: #f7f8fa; }
|
||||
.title { font: 700 30px Arial, sans-serif; fill: #1f2933; }
|
||||
.subtitle { font: 400 16px Arial, sans-serif; fill: #52606d; }
|
||||
.box { fill: #ffffff; stroke: #9aa5b1; stroke-width: 2; rx: 8; }
|
||||
.program { fill: #e8f1ff; stroke: #3465a4; }
|
||||
.dao { fill: #fff3d6; stroke: #b7791f; }
|
||||
.pda { fill: #edf7ed; stroke: #2f855a; }
|
||||
.actor { fill: #f3e8ff; stroke: #805ad5; }
|
||||
.txt { font: 700 17px Arial, sans-serif; fill: #1f2933; }
|
||||
.small { font: 400 13px Arial, sans-serif; fill: #3e4c59; }
|
||||
.line { stroke: #2f3a45; stroke-width: 2.2; fill: none; marker-end: url(#arrow); }
|
||||
.money { stroke: #0a7f62; stroke-width: 3; fill: none; marker-end: url(#moneyArrow); }
|
||||
.dashed { stroke-dasharray: 8 7; }
|
||||
.legend { font: 400 14px Arial, sans-serif; fill: #3e4c59; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect class="bg" x="0" y="0" width="1400" height="900"/>
|
||||
<text class="title" x="52" y="54">SHiNE Solana: программы, DAO, счета и движение денег</text>
|
||||
<text class="subtitle" x="52" y="82">Текущая модель: три Anchor-программы, DAO/authority как управляющий слой, inflow vault и DAO treasury.</text>
|
||||
|
||||
<rect class="box actor" x="52" y="150" width="210" height="78"/>
|
||||
<text class="txt" x="72" y="181">Пользователь</text>
|
||||
<text class="small" x="72" y="206">signer, root_key, device_key</text>
|
||||
|
||||
<rect class="box actor" x="52" y="310" width="210" height="78"/>
|
||||
<text class="txt" x="72" y="341">Покупатель тикета</text>
|
||||
<text class="small" x="72" y="366">buy_ticket*</text>
|
||||
|
||||
<rect class="box actor" x="52" y="470" width="210" height="78"/>
|
||||
<text class="txt" x="72" y="501">Менеджер</text>
|
||||
<text class="small" x="72" y="526">manager_add_ticket</text>
|
||||
|
||||
<rect class="box actor" x="52" y="630" width="210" height="78"/>
|
||||
<text class="txt" x="72" y="661">Любой caller</text>
|
||||
<text class="small" x="72" y="686">step_payout</text>
|
||||
|
||||
<rect class="box program" x="360" y="126" width="270" height="96"/>
|
||||
<text class="txt" x="382" y="160">1. shine_login_guard</text>
|
||||
<text class="small" x="382" y="186">classify_login</text>
|
||||
<text class="small" x="382" y="205">free / premium / trademark</text>
|
||||
|
||||
<rect class="box program" x="360" y="286" width="270" height="112"/>
|
||||
<text class="txt" x="382" y="320">2. shine_users</text>
|
||||
<text class="small" x="382" y="346">create_user_pda</text>
|
||||
<text class="small" x="382" y="365">update_user_pda</text>
|
||||
<text class="small" x="382" y="384">economy config</text>
|
||||
|
||||
<rect class="box program" x="360" y="518" width="270" height="122"/>
|
||||
<text class="txt" x="382" y="552">3. shine_payments</text>
|
||||
<text class="small" x="382" y="578">vault, tickets, queues</text>
|
||||
<text class="small" x="382" y="597">grant_manager_limits</text>
|
||||
<text class="small" x="382" y="616">step_payout</text>
|
||||
|
||||
<rect class="box dao" x="776" y="126" width="270" height="122"/>
|
||||
<text class="txt" x="798" y="160">SHiNE DAO</text>
|
||||
<text class="small" x="798" y="186">governance / authority</text>
|
||||
<text class="small" x="798" y="205">treasury dao_wallet</text>
|
||||
<text class="small" x="798" y="224">ключи через голосование</text>
|
||||
|
||||
<rect class="box pda" x="776" y="306" width="270" height="84"/>
|
||||
<text class="txt" x="798" y="340">shine_users PDA</text>
|
||||
<text class="small" x="798" y="365">user_pda, economy_config</text>
|
||||
|
||||
<rect class="box pda" x="776" y="500" width="270" height="150"/>
|
||||
<text class="txt" x="798" y="534">shine_payments PDA</text>
|
||||
<text class="small" x="798" y="560">config_pda, coef_limit_pda</text>
|
||||
<text class="small" x="798" y="579">queues_pda</text>
|
||||
<text class="small" x="798" y="598">inflow_vault_pda</text>
|
||||
<text class="small" x="798" y="617">ticket_pda, manager_allowance</text>
|
||||
|
||||
<rect class="box pda" x="1134" y="500" width="214" height="88"/>
|
||||
<text class="txt" x="1156" y="534">inflow_vault</text>
|
||||
<text class="small" x="1156" y="560">деньги регистрации</text>
|
||||
|
||||
<rect class="box dao" x="1134" y="170" width="214" height="88"/>
|
||||
<text class="txt" x="1156" y="204">DAO treasury</text>
|
||||
<text class="small" x="1156" y="230">dao_wallet</text>
|
||||
|
||||
<path class="line" d="M262 189 C300 189, 318 334, 360 334"/>
|
||||
<text class="small" x="270" y="286">регистрация / update</text>
|
||||
|
||||
<path class="line" d="M360 314 C322 250, 320 176, 360 174"/>
|
||||
<text class="small" x="330" y="250">CPI login</text>
|
||||
|
||||
<path class="line" d="M630 342 L776 342"/>
|
||||
<text class="small" x="646" y="329">создает/обновляет</text>
|
||||
|
||||
<path class="money" d="M262 205 C438 432, 1010 390, 1134 530"/>
|
||||
<text class="small" x="430" y="430">регистрация и лимит -> inflow_vault</text>
|
||||
|
||||
<path class="money" d="M262 349 C540 260, 870 244, 1134 214"/>
|
||||
<text class="small" x="538" y="270">покупка тикета -> DAO treasury</text>
|
||||
|
||||
<path class="line" d="M262 509 L360 579"/>
|
||||
<text class="small" x="276" y="540">создать тикет</text>
|
||||
|
||||
<path class="line" d="M630 579 L776 575"/>
|
||||
<text class="small" x="648" y="562">PDA состояния</text>
|
||||
|
||||
<path class="line" d="M1046 575 L1134 548"/>
|
||||
|
||||
<path class="money" d="M1134 560 C970 700, 580 728, 262 669"/>
|
||||
<text class="small" x="650" y="720">call reward caller</text>
|
||||
|
||||
<path class="money" d="M1134 536 C860 754, 426 238, 262 194"/>
|
||||
<text class="small" x="632" y="760">выплата получателю тикета</text>
|
||||
|
||||
<path class="money" d="M1241 500 L1241 258"/>
|
||||
<text class="small" x="1254" y="380">DAO-часть выплат</text>
|
||||
|
||||
<path class="line" d="M776 188 L630 342"/>
|
||||
<text class="small" x="642" y="250">update economy</text>
|
||||
|
||||
<path class="line" d="M776 216 C690 290, 666 516, 630 558"/>
|
||||
<text class="small" x="654" y="438">settings / managers</text>
|
||||
|
||||
<path class="line dashed" d="M910 248 C850 702, 620 720, 520 640"/>
|
||||
<text class="small" x="690" y="690">upgrade-authority: users/payments; login_guard позже</text>
|
||||
|
||||
<rect class="box" x="52" y="808" width="1296" height="54"/>
|
||||
<line x1="74" y1="835" x2="132" y2="835" class="line"/>
|
||||
<text class="legend" x="146" y="840">логические вызовы и управление</text>
|
||||
<line x1="374" y1="835" x2="432" y2="835" class="money"/>
|
||||
<text class="legend" x="446" y="840">движение SOL/lamports</text>
|
||||
<line x1="682" y1="835" x2="740" y2="835" class="line dashed"/>
|
||||
<text class="legend" x="754" y="840">будущая передача upgrade-authority DAO</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.1 KiB |
@ -18,13 +18,11 @@
|
||||
|
||||
- Целевая директория UI-деплоя: `/home/player/SHiNE/shine-ui`.
|
||||
- `Caddyfile` на сервере должен смотреть в ту же директорию через `root * /home/player/SHiNE/shine-ui`.
|
||||
- В `deploy_shine-PWA.sh` добавлена проверка: скрипт ищет блок `shineup.me { ... }` (или значение `EXPECTED_CADDY_SITE`) и проверяет `root` внутри этого блока.
|
||||
- Если `root` внутри целевого блока не совпадает, деплой прерывается с ошибкой.
|
||||
- В `deploy_shine-PWA.sh` добавлена проверка: если `root` в `Caddyfile` не совпадает, деплой прерывается с ошибкой.
|
||||
- Для ручного обхода проверки (только осознанно): `ALLOW_CADDY_MISMATCH=1 ./gradlew deployUI`.
|
||||
- При необходимости можно явно переопределить путь деплоя:
|
||||
- `REMOTE_UI_DIR=/нужный/путь ./gradlew deployUI`
|
||||
- `EXPECTED_CADDY_UI_ROOT=/нужный/путь ./gradlew deployUI`
|
||||
- `EXPECTED_CADDY_SITE=example.com ./gradlew deployUI`
|
||||
|
||||
### Важно для локального UI (history-router / Ctrl+F5)
|
||||
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
# Деплой и инициализация Solana-регистрации (две обязательные программы)
|
||||
|
||||
## Коротко
|
||||
|
||||
Для рабочей регистрации пользователя нужны **обе** программы:
|
||||
|
||||
1. `shine_users` — хранение и обновление `user_pda`, economy-конфиг, логика регистрации.
|
||||
2. `shine_login_guard` — проверка/классификация логина (CPI из `shine_users`).
|
||||
|
||||
Если задеплоена только одна из них — регистрация неработоспособна.
|
||||
|
||||
## Актуальные адреса (devnet)
|
||||
|
||||
- `shine_users` (регистрация пользователей):
|
||||
`FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
||||
- `shine_login_guard`:
|
||||
`3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo`
|
||||
- `shine_payments`:
|
||||
`m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR`
|
||||
|
||||
## Подтверждение деплоя
|
||||
|
||||
- Сеть: `https://api.devnet.solana.com`
|
||||
- `shine_users`:
|
||||
- `Program ID`: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
||||
- TX deploy: `5VzfpSirFCRqPUZfvAt3eADY9KnowW79PKZ1pCQAa2DJGiztj4dUYYXrSQNmWEhPVu6mPSDfcuHzFyEVmoKLa9DM`
|
||||
- `shine_login_guard`:
|
||||
- `Program ID`: `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo`
|
||||
- TX deploy: `5iptngPYrLLjPE3Xby24zyNW3edVUnBNLBx785vjojMoq5JNLFNQvLNAm3jNYHbpf2B36qtbpTNzcvUNyRDqm1Mf`
|
||||
|
||||
## Порядок деплоя (devnet)
|
||||
|
||||
1. Убедиться, что CLI смотрит в devnet и у кошелька есть SOL.
|
||||
2. Собрать и задеплоить `shine_login_guard`.
|
||||
3. Собрать и задеплоить `shine_users`.
|
||||
4. Проверить, что адреса совпадают между:
|
||||
- `Anchor.toml`
|
||||
- `declare_id!` в `programs/*/src/lib.rs`
|
||||
- UI/серверными константами.
|
||||
5. Выполнить `init_users_economy_config` (один раз на программу `shine_users`).
|
||||
|
||||
Пример команд:
|
||||
|
||||
```bash
|
||||
cd shine-solana/shine
|
||||
solana config get
|
||||
solana balance
|
||||
|
||||
anchor build -p shine_login_guard
|
||||
anchor deploy -p shine_login_guard
|
||||
|
||||
anchor build -p shine_users
|
||||
anchor deploy -p shine_users
|
||||
```
|
||||
|
||||
## Куда вписаны адреса в проекте
|
||||
|
||||
### UI
|
||||
|
||||
- Общие Solana-константы:
|
||||
- `shine-UI/js/solana-programs.js`
|
||||
- Страница инициализации:
|
||||
- `shine-UI/js/pages/solana-users-init-view.js`
|
||||
- Переход на страницу:
|
||||
- `shine-UI/js/pages/developer-settings-view.js`
|
||||
|
||||
### Сервер
|
||||
|
||||
- Серверные константы Solana:
|
||||
- `shine-server-config/src/main/java/utils/config/SolanaProgramsConfig.java`
|
||||
|
||||
## Как запустить инициализацию economy PDA
|
||||
|
||||
1. Открыть UI.
|
||||
2. Перейти: `Профиль -> Настройки -> Настройки разработчика -> Solana: init регистрации`.
|
||||
3. Подключить кошелёк (Phantom, devnet).
|
||||
4. Нажать `Запустить init_users_economy_config`.
|
||||
5. Дождаться статуса `Успешно`.
|
||||
|
||||
Страница сама вычисляет PDA `users_economy_config` по seed:
|
||||
|
||||
- seed: `shine_users_economy_config`
|
||||
- program: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
||||
|
||||
## Важно
|
||||
|
||||
- `init_users_economy_config` выполняется один раз на программу.
|
||||
Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение).
|
||||
- Серверные приватные ключи для Solana не используются: подписание делается кошельком пользователя в UI.
|
||||
- `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина.
|
||||
Несовпадение адреса приведёт к ошибке регистрации.
|
||||
@ -1,119 +0,0 @@
|
||||
# ESP32-S3-Touch-AMOLED-2.16 Codex Guide
|
||||
|
||||
Этот файл переносится в другие проекты как готовая инструкция для Codex по этой плате.
|
||||
|
||||
## 1) Что это за плата
|
||||
|
||||
- Модель: `Waveshare ESP32-S3-Touch-AMOLED-2.16`
|
||||
- MCU: `ESP32-S3` (flash 16MB, PSRAM 8MB)
|
||||
- Экран: AMOLED, физически 480x480, углы скруглены (часть крайних пикселей может быть невидима)
|
||||
- Touch: CST92xx
|
||||
- IMU: QMI8658
|
||||
- Аудио:
|
||||
- DAC/вывод (динамик): ES8311
|
||||
- ADC/вход (микрофоны): ES7210
|
||||
|
||||
## 2) Что уже установлено в этой среде
|
||||
|
||||
- Ubuntu
|
||||
- `arduino-cli 1.4.0`
|
||||
- `esp32:esp32` core `3.3.5`
|
||||
- `esptool` из `~/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool`
|
||||
- USB порт платы: обычно `/dev/ttyACM0`
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
arduino-cli version
|
||||
arduino-cli core list
|
||||
arduino-cli board list
|
||||
ls -l /dev/ttyACM0
|
||||
```
|
||||
|
||||
## 3) Структура подпроекта (эталон)
|
||||
|
||||
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
||||
- `original-firmware/` — backup/restore заводской прошивки
|
||||
- `test-device/` — прошивки и `burn.sh`
|
||||
- `reference/` — заметки и ссылки
|
||||
|
||||
## 4) Бэкап перед любыми экспериментами
|
||||
|
||||
```bash
|
||||
cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
|
||||
./backup_factory.sh
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
- `factory-full-16mb.bin`
|
||||
- `factory-full-16mb.bin.sha256`
|
||||
|
||||
Восстановление:
|
||||
|
||||
```bash
|
||||
./restore_factory_backup.sh
|
||||
```
|
||||
|
||||
## 5) Деплой (прошивка) — стандарт
|
||||
|
||||
Главный скрипт:
|
||||
|
||||
```bash
|
||||
cd ESP32-S3-Touch-AMOLED-2.16/test-device
|
||||
./burn.sh <mode>
|
||||
```
|
||||
|
||||
Режимы:
|
||||
- `hello` — базовый экран
|
||||
- `widgets` — экран+touch+IMU (официальный пример)
|
||||
- `audio` — тест аудио тракта
|
||||
- `simple` — кастомный интеграционный тест (экран, touch, запись/воспроизведение, VU, tilt)
|
||||
|
||||
## 6) Как писать код под эту плату (важно)
|
||||
|
||||
1. **Экран**
|
||||
- Рабочее разрешение использовать `480x480`.
|
||||
- Не рисовать критичный текст/кнопки впритык к краю; держать safe margin (`~20px+`) из-за скругленных углов.
|
||||
- Не делать полный `fillScreen` в каждом loop: только частичные обновления (`fillRect`/локальные перерисовки), иначе мерцание.
|
||||
|
||||
2. **Touch**
|
||||
- Настройка CST:
|
||||
- `setMaxCoordinates(480, 480)`
|
||||
- `setSwapXY(true)`
|
||||
- `setMirrorXY(true, false)`
|
||||
- Обрабатывать touch по IRQ + `getPoint`.
|
||||
- После смещения UI обязательно пересчитывать hitbox кнопок.
|
||||
|
||||
3. **Аудио**
|
||||
- Для динамика инициализировать `ES8311`.
|
||||
- Для микрофона обязательно инициализировать `ES7210`; без этого запись может быть пустой.
|
||||
- Для отладки записи показывать VU/peak на экране во время `RECORD`.
|
||||
- Для быстрой проверки тракта всегда держать кнопку `BEEP` (тон), чтобы отделить проблему динамика от проблемы микрофона.
|
||||
|
||||
4. **IMU**
|
||||
- QMI8658 обновлять с ограниченной частотой (например 80–150 мс для UI-строки), чтобы не шуметь перерисовками.
|
||||
|
||||
5. **Стабильность UI**
|
||||
- Статика: рисуется один раз в setup.
|
||||
- Динамика: отдельная зона, перерисовывать только по изменению данных.
|
||||
|
||||
## 7) Рекомендуемый workflow для Codex
|
||||
|
||||
1. Проверить порт и инструменты.
|
||||
2. Если новая плата/первый запуск — сделать backup flash.
|
||||
3. Собрать и залить `simple`.
|
||||
4. Пройти ручной чек:
|
||||
- экран отображает текст без обрезки,
|
||||
- touch срабатывает по кнопкам,
|
||||
- `BEEP` слышно,
|
||||
- VU двигается во время записи,
|
||||
- `PLAY` воспроизводит записанное,
|
||||
- `Tilt` меняется при повороте.
|
||||
5. Только после этого усложнять приложение.
|
||||
|
||||
## 8) Ссылки
|
||||
|
||||
- Product page: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
|
||||
- Docs: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
|
||||
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
|
||||
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16
|
||||
@ -1,25 +0,0 @@
|
||||
# ESP32-S3-Touch-AMOLED-2.16
|
||||
|
||||
Подпроект для Waveshare `ESP32-S3-Touch-AMOLED-2.16`.
|
||||
|
||||
Структура:
|
||||
|
||||
- `official-demo/` — официальный репозиторий примеров Waveshare
|
||||
- `original-firmware/` — резервная копия заводской прошивки
|
||||
- `test-device/` — скрипты быстрой проверки устройства
|
||||
- `reference/` — локальные заметки по документации и железу
|
||||
|
||||
Примечание по git:
|
||||
|
||||
- `official-demo/` держать как локальный внешний checkout из `https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16`, в основной git его не добавлять.
|
||||
- `original-firmware/*.bin` — локальный дамп конкретной платы, в git не добавлять.
|
||||
- `.arduino-build/` и готовые `.bin/.elf/.map` — сборочные артефакты, в git не добавлять.
|
||||
|
||||
Быстрый старт:
|
||||
|
||||
1. Сделать backup текущей прошивки:
|
||||
- `cd original-firmware && ./backup_factory.sh`
|
||||
2. Залить тест экрана/тача:
|
||||
- `cd ../test-device && ./burn.sh widgets`
|
||||
3. Залить тест динамика:
|
||||
- `cd ../test-device && ./burn.sh audio`
|
||||
@ -1,11 +0,0 @@
|
||||
# Factory Firmware Backup
|
||||
|
||||
Здесь хранится полный дамп флеша текущего состояния платы.
|
||||
|
||||
Файлы:
|
||||
- `factory-full-16mb.bin` — полный дамп флеша `0x000000..0xFFFFFF`
|
||||
- `factory-full-16mb.bin.sha256` — контрольная сумма
|
||||
|
||||
Скрипты:
|
||||
- `backup_factory.sh` — снять резервную копию
|
||||
- `restore_factory_backup.sh` — восстановить резервную копию на плату
|
||||
@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PORT="${PORT:-/dev/ttyACM0}"
|
||||
ESPTOOL="${ESPTOOL:-$HOME/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool}"
|
||||
BAUD="${BAUD:-921600}"
|
||||
OUT_BIN="${ROOT_DIR}/factory-full-16mb.bin"
|
||||
|
||||
echo "== Port: ${PORT}"
|
||||
echo "== Output: ${OUT_BIN}"
|
||||
echo "== Esptool: ${ESPTOOL}"
|
||||
echo "== Baud: ${BAUD}"
|
||||
|
||||
"${ESPTOOL}" --port "${PORT}" --baud "${BAUD}" read-flash 0 0x1000000 "${OUT_BIN}"
|
||||
sha256sum "${OUT_BIN}" | tee "${OUT_BIN}.sha256"
|
||||
|
||||
echo
|
||||
echo "== Backup done."
|
||||
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PORT="${PORT:-/dev/ttyACM0}"
|
||||
ESPTOOL="${ESPTOOL:-$HOME/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool}"
|
||||
IN_BIN="${IN_BIN:-${ROOT_DIR}/factory-full-16mb.bin}"
|
||||
|
||||
if [[ ! -f "${IN_BIN}" ]]; then
|
||||
echo "Backup file not found: ${IN_BIN}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== Port: ${PORT}"
|
||||
echo "== Input: ${IN_BIN}"
|
||||
echo "== Esptool: ${ESPTOOL}"
|
||||
|
||||
"${ESPTOOL}" --port "${PORT}" --baud 921600 write-flash 0 "${IN_BIN}"
|
||||
|
||||
echo
|
||||
echo "== Restore done."
|
||||
@ -1,17 +0,0 @@
|
||||
# Reference Notes
|
||||
|
||||
Модель: Waveshare `ESP32-S3-Touch-AMOLED-2.16` (SKU 33969/33970)
|
||||
|
||||
Полезные страницы:
|
||||
- Product: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
|
||||
- Docs home: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
|
||||
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
|
||||
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16
|
||||
|
||||
Ключевое по железу (по документации):
|
||||
- ESP32-S3R8, 16MB flash, 8MB PSRAM
|
||||
- AMOLED 2.16" 480x480
|
||||
- Capacitive touch (CST9220)
|
||||
- IMU QMI8658
|
||||
- Audio codec ES8311 (playback) и ES7210 (microphones)
|
||||
- TF slot (SD), сейчас без карты
|
||||
@ -1,18 +0,0 @@
|
||||
# Test Device
|
||||
|
||||
Скрипт заливает официальные Arduino-примеры для быстрой проверки платы.
|
||||
|
||||
Для режимов `widgets`, `audio` и `hello` рядом должен лежать локальный checkout `official-demo/` из официального репозитория Waveshare. В основной git он не добавляется, потому что это большой внешний набор примеров, библиотек, прошивок и артефактов.
|
||||
|
||||
Режимы:
|
||||
- `widgets` — экран + touch + IMU (пример `05_LVGL_Widgets`)
|
||||
- `audio` — динамик/аудио-кодек (пример `07_ES8311`)
|
||||
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
|
||||
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
|
||||
|
||||
Запуск:
|
||||
|
||||
- `./burn.sh widgets`
|
||||
- `./burn.sh audio`
|
||||
- `./burn.sh hello`
|
||||
- `./burn.sh simple`
|
||||
@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BOARD_DIR="$(cd "${ROOT_DIR}/.." && pwd)"
|
||||
DEMO_BASE="${BOARD_DIR}/official-demo/examples/Arduino-v3.3.5"
|
||||
MODE="${1:-widgets}"
|
||||
PORT="${PORT:-/dev/ttyACM0}"
|
||||
FQBN="${FQBN:-esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc,UploadSpeed=921600,CPUFreq=240,FlashMode=dio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,PSRAM=opi}"
|
||||
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/.arduino-build/build-${MODE}}"
|
||||
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/.arduino-build/out-${MODE}}"
|
||||
|
||||
case "${MODE}" in
|
||||
hello) SKETCH_DIR="${DEMO_BASE}/examples/01_HelloWorld" ;;
|
||||
widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;;
|
||||
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
|
||||
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
|
||||
*)
|
||||
echo "Unknown mode: ${MODE}" >&2
|
||||
echo "Use one of: hello, widgets, audio, simple" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "== Mode: ${MODE}"
|
||||
echo "== Sketch: ${SKETCH_DIR}"
|
||||
echo "== Port: ${PORT}"
|
||||
echo "== FQBN: ${FQBN}"
|
||||
|
||||
mkdir -p "${BUILD_DIR}" "${OUT_DIR}"
|
||||
|
||||
arduino-cli compile \
|
||||
--clean \
|
||||
--fqbn "${FQBN}" \
|
||||
--build-path "${BUILD_DIR}" \
|
||||
--output-dir "${OUT_DIR}" \
|
||||
--library "${DEMO_BASE}/libraries/GFX_Library_for_Arduino" \
|
||||
--library "${DEMO_BASE}/libraries/SensorLib" \
|
||||
--library "${DEMO_BASE}/libraries/XPowersLib" \
|
||||
--library "${DEMO_BASE}/libraries/lvgl" \
|
||||
--library "${DEMO_BASE}/libraries/Mylibrary" \
|
||||
"${SKETCH_DIR}"
|
||||
|
||||
arduino-cli upload \
|
||||
-p "${PORT}" \
|
||||
--fqbn "${FQBN}" \
|
||||
--input-dir "${OUT_DIR}" \
|
||||
"${SKETCH_DIR}"
|
||||
|
||||
echo
|
||||
echo "== Done."
|
||||
@ -1,136 +0,0 @@
|
||||
/*
|
||||
* ESPRESSIF MIT License
|
||||
*
|
||||
* Copyright (c) 2018 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
|
||||
*
|
||||
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||
* it is free of charge, to any person obtaining a copy of this software and associated
|
||||
* documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
|
||||
* to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _AUDIO_HAL_H_
|
||||
#define _AUDIO_HAL_H_
|
||||
|
||||
#define AUDIO_HAL_VOL_DEFAULT 60
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Select media hal codec mode
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_CODEC_MODE_ENCODE = 1, /*!< select adc */
|
||||
AUDIO_HAL_CODEC_MODE_DECODE, /*!< select dac */
|
||||
AUDIO_HAL_CODEC_MODE_BOTH, /*!< select both adc and dac */
|
||||
AUDIO_HAL_CODEC_MODE_LINE_IN, /*!< set adc channel */
|
||||
} audio_hal_codec_mode_t;
|
||||
|
||||
/**
|
||||
* @brief Select adc channel for input mic signal
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_ADC_INPUT_LINE1 = 0x00, /*!< mic input to adc channel 1 */
|
||||
AUDIO_HAL_ADC_INPUT_LINE2, /*!< mic input to adc channel 2 */
|
||||
AUDIO_HAL_ADC_INPUT_ALL, /*!< mic input to both channels of adc */
|
||||
AUDIO_HAL_ADC_INPUT_DIFFERENCE, /*!< mic input to adc difference channel */
|
||||
} audio_hal_adc_input_t;
|
||||
|
||||
/**
|
||||
* @brief Select channel for dac output
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_DAC_OUTPUT_LINE1 = 0x00, /*!< dac output signal to channel 1 */
|
||||
AUDIO_HAL_DAC_OUTPUT_LINE2, /*!< dac output signal to channel 2 */
|
||||
AUDIO_HAL_DAC_OUTPUT_ALL, /*!< dac output signal to both channels */
|
||||
} audio_hal_dac_output_t;
|
||||
|
||||
/**
|
||||
* @brief Select operating mode i.e. start or stop for audio codec chip
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_CTRL_STOP = 0x00, /*!< set stop mode */
|
||||
AUDIO_HAL_CTRL_START = 0x01, /*!< set start mode */
|
||||
} audio_hal_ctrl_t;
|
||||
|
||||
/**
|
||||
* @brief Select I2S interface operating mode i.e. master or slave for audio codec chip
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_MODE_SLAVE = 0x00, /*!< set slave mode */
|
||||
AUDIO_HAL_MODE_MASTER = 0x01, /*!< set master mode */
|
||||
} audio_hal_iface_mode_t;
|
||||
|
||||
/**
|
||||
* @brief Select I2S interface samples per second
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_08K_SAMPLES, /*!< set to 8k samples per second */
|
||||
AUDIO_HAL_11K_SAMPLES, /*!< set to 11.025k samples per second */
|
||||
AUDIO_HAL_16K_SAMPLES, /*!< set to 16k samples in per second */
|
||||
AUDIO_HAL_22K_SAMPLES, /*!< set to 22.050k samples per second */
|
||||
AUDIO_HAL_24K_SAMPLES, /*!< set to 24k samples in per second */
|
||||
AUDIO_HAL_32K_SAMPLES, /*!< set to 32k samples in per second */
|
||||
AUDIO_HAL_44K_SAMPLES, /*!< set to 44.1k samples per second */
|
||||
AUDIO_HAL_48K_SAMPLES, /*!< set to 48k samples per second */
|
||||
} audio_hal_iface_samples_t;
|
||||
|
||||
/**
|
||||
* @brief Select I2S interface number of bits per sample
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_BIT_LENGTH_16BITS = 1, /*!< set 16 bits per sample */
|
||||
AUDIO_HAL_BIT_LENGTH_24BITS, /*!< set 24 bits per sample */
|
||||
AUDIO_HAL_BIT_LENGTH_32BITS, /*!< set 32 bits per sample */
|
||||
} audio_hal_iface_bits_t;
|
||||
|
||||
/**
|
||||
* @brief Select I2S interface format for audio codec chip
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_I2S_NORMAL = 0, /*!< set normal I2S format */
|
||||
AUDIO_HAL_I2S_LEFT, /*!< set all left format */
|
||||
AUDIO_HAL_I2S_RIGHT, /*!< set all right format */
|
||||
AUDIO_HAL_I2S_DSP, /*!< set dsp/pcm format */
|
||||
} audio_hal_iface_format_t;
|
||||
|
||||
/**
|
||||
* @brief I2s interface configuration for audio codec chip
|
||||
*/
|
||||
typedef struct {
|
||||
audio_hal_iface_mode_t mode; /*!< audio codec chip mode */
|
||||
audio_hal_iface_format_t fmt; /*!< I2S interface format */
|
||||
audio_hal_iface_samples_t samples; /*!< I2S interface samples per second */
|
||||
audio_hal_iface_bits_t bits; /*!< i2s interface number of bits per sample */
|
||||
} audio_hal_codec_i2s_iface_t;
|
||||
|
||||
/**
|
||||
* @brief Configure media hal for initialization of audio codec chip
|
||||
*/
|
||||
typedef struct {
|
||||
audio_hal_adc_input_t adc_input; /*!< set adc channel */
|
||||
audio_hal_dac_output_t dac_output; /*!< set dac channel */
|
||||
audio_hal_codec_mode_t codec_mode; /*!< select codec mode: adc, dac or both */
|
||||
audio_hal_codec_i2s_iface_t i2s_iface; /*!< set I2S interface configuration */
|
||||
} audio_hal_codec_config_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //__AUDIO_HAL_H__
|
||||
@ -1,549 +0,0 @@
|
||||
/*
|
||||
* ESPRESSIF MIT License
|
||||
*
|
||||
* Copyright (c) 2021 <ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD>
|
||||
*
|
||||
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||
* it is free of charge, to any person obtaining a copy of this software and associated
|
||||
* documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
|
||||
* to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef ESP32
|
||||
|
||||
#include <Wire.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "es7210.h"
|
||||
|
||||
|
||||
#define I2S_DSP_MODE_A 0
|
||||
#define MCLK_DIV_FRE 256
|
||||
|
||||
|
||||
#define ES7210_MCLK_SOURCE FROM_CLOCK_DOUBLE_PIN /* In master mode, 0 : MCLK from pad 1 : MCLK from clock doubler */
|
||||
#define FROM_PAD_PIN 0
|
||||
#define FROM_CLOCK_DOUBLE_PIN 1
|
||||
|
||||
|
||||
static TwoWire *es7210wire;
|
||||
static es7210_gain_value_t gain;
|
||||
|
||||
/*
|
||||
* Clock coefficient structer
|
||||
*/
|
||||
struct _coeff_div_es7210 {
|
||||
uint32_t mclk; /* mclk frequency */
|
||||
uint32_t lrck; /* lrck */
|
||||
uint8_t ss_ds;
|
||||
uint8_t adc_div; /* adcclk divider */
|
||||
uint8_t dll; /* dll_bypass */
|
||||
uint8_t doubler; /* doubler enable */
|
||||
uint8_t osr; /* adc osr */
|
||||
uint8_t mclk_src; /* select mclk source */
|
||||
uint32_t lrck_h; /* The high 4 bits of lrck */
|
||||
uint32_t lrck_l; /* The low 8 bits of lrck */
|
||||
};
|
||||
|
||||
static const char *TAG = "ES7210";
|
||||
|
||||
static es7210_input_mics_t mic_select = (es7210_input_mics_t)(ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 | ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4);
|
||||
|
||||
/* Codec hifi mclk clock divider coefficients
|
||||
* MEMBER REG
|
||||
* mclk: 0x03
|
||||
* lrck: standard
|
||||
* ss_ds: --
|
||||
* adc_div: 0x02
|
||||
* dll: 0x06
|
||||
* doubler: 0x02
|
||||
* osr: 0x07
|
||||
* mclk_src: 0x03
|
||||
* lrckh: 0x04
|
||||
* lrckl: 0x05
|
||||
*/
|
||||
static const struct _coeff_div_es7210 coeff_div[] = {
|
||||
//mclk lrck ss_ds adc_div dll doubler osr mclk_src lrckh lrckl
|
||||
/* 8k */
|
||||
{12288000, 8000 , 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00},
|
||||
{16384000, 8000 , 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00},
|
||||
{19200000, 8000 , 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60},
|
||||
{4096000, 8000 , 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
|
||||
/* 11.025k */
|
||||
{11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
|
||||
|
||||
/* 12k */
|
||||
{12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
|
||||
{19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40},
|
||||
|
||||
/* 16k */
|
||||
{4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80},
|
||||
{16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
|
||||
{12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00},
|
||||
|
||||
/* 22.05k */
|
||||
{11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
|
||||
/* 24k */
|
||||
{12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
{19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20},
|
||||
|
||||
/* 32k */
|
||||
{12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80},
|
||||
{16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
{19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58},
|
||||
|
||||
/* 44.1k */
|
||||
{11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
|
||||
/* 48k */
|
||||
{12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90},
|
||||
|
||||
/* 64k */
|
||||
{16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c},
|
||||
|
||||
/* 88.2k */
|
||||
{11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
|
||||
|
||||
/* 96k */
|
||||
{12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
|
||||
{19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8},
|
||||
};
|
||||
|
||||
static esp_err_t es7210_write_reg(uint8_t reg_addr, uint8_t data)
|
||||
{
|
||||
|
||||
es7210wire->beginTransmission(ES7210_ADDR);
|
||||
es7210wire->write(reg_addr);
|
||||
es7210wire->write(data);
|
||||
return es7210wire->endTransmission();
|
||||
|
||||
}
|
||||
|
||||
static esp_err_t es7210_update_reg_bit(uint8_t reg_addr, uint8_t update_bits, uint8_t data)
|
||||
{
|
||||
uint8_t regv;
|
||||
regv = es7210_read_reg(reg_addr);
|
||||
regv = (regv & (~update_bits)) | (update_bits & data);
|
||||
return es7210_write_reg(reg_addr, regv);
|
||||
}
|
||||
|
||||
static int get_coeff(uint32_t mclk, uint32_t lrck)
|
||||
{
|
||||
for (int i = 0; i < (sizeof(coeff_div) / sizeof(coeff_div[0])); i++) {
|
||||
if (coeff_div[i].lrck == lrck && coeff_div[i].mclk == mclk)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int8_t get_es7210_mclk_src(void)
|
||||
{
|
||||
return ES7210_MCLK_SOURCE;
|
||||
}
|
||||
|
||||
int es7210_read_reg(uint8_t reg_addr)
|
||||
{
|
||||
uint8_t data;
|
||||
es7210wire->beginTransmission(ES7210_ADDR);
|
||||
es7210wire->write(reg_addr);
|
||||
es7210wire->endTransmission(false);
|
||||
es7210wire->requestFrom(ES7210_ADDR, (size_t)1);
|
||||
data = es7210wire->read();
|
||||
return (int)data;
|
||||
}
|
||||
|
||||
esp_err_t es7210_config_sample(audio_hal_iface_samples_t sample)
|
||||
{
|
||||
uint8_t regv;
|
||||
int coeff;
|
||||
int sample_fre = 0;
|
||||
int mclk_fre = 0;
|
||||
esp_err_t ret = ESP_OK;
|
||||
switch (sample) {
|
||||
case AUDIO_HAL_08K_SAMPLES:
|
||||
sample_fre = 8000;
|
||||
break;
|
||||
case AUDIO_HAL_11K_SAMPLES:
|
||||
sample_fre = 11025;
|
||||
break;
|
||||
case AUDIO_HAL_16K_SAMPLES:
|
||||
sample_fre = 16000;
|
||||
break;
|
||||
case AUDIO_HAL_22K_SAMPLES:
|
||||
sample_fre = 22050;
|
||||
break;
|
||||
case AUDIO_HAL_24K_SAMPLES:
|
||||
sample_fre = 24000;
|
||||
break;
|
||||
case AUDIO_HAL_32K_SAMPLES:
|
||||
sample_fre = 32000;
|
||||
break;
|
||||
case AUDIO_HAL_44K_SAMPLES:
|
||||
sample_fre = 44100;
|
||||
break;
|
||||
case AUDIO_HAL_48K_SAMPLES:
|
||||
sample_fre = 48000;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unable to configure sample rate %dHz", sample_fre);
|
||||
break;
|
||||
}
|
||||
mclk_fre = sample_fre * MCLK_DIV_FRE;
|
||||
coeff = get_coeff(mclk_fre, sample_fre);
|
||||
if (coeff < 0) {
|
||||
ESP_LOGE(TAG, "Unable to configure sample rate %dHz with %dHz MCLK", sample_fre, mclk_fre);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
/* Set clock parammeters */
|
||||
if (coeff >= 0) {
|
||||
/* Set adc_div & doubler & dll */
|
||||
regv = es7210_read_reg(ES7210_MAINCLK_REG02) & 0x00;
|
||||
regv |= coeff_div[coeff].adc_div;
|
||||
regv |= coeff_div[coeff].doubler << 6;
|
||||
regv |= coeff_div[coeff].dll << 7;
|
||||
ret |= es7210_write_reg(ES7210_MAINCLK_REG02, regv);
|
||||
/* Set osr */
|
||||
regv = coeff_div[coeff].osr;
|
||||
ret |= es7210_write_reg(ES7210_OSR_REG07, regv);
|
||||
/* Set lrck */
|
||||
regv = coeff_div[coeff].lrck_h;
|
||||
ret |= es7210_write_reg(ES7210_LRCK_DIVH_REG04, regv);
|
||||
regv = coeff_div[coeff].lrck_l;
|
||||
ret |= es7210_write_reg(ES7210_LRCK_DIVL_REG05, regv);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_mic_select(es7210_input_mics_t mic)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
mic_select = mic;
|
||||
if (mic_select & (ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 | ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4)) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00);
|
||||
}
|
||||
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0xff);
|
||||
if (mic_select & ES7210_INPUT_MIC1) {
|
||||
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC1");
|
||||
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0x00);
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x10, 0x10);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC2) {
|
||||
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC2");
|
||||
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0x00);
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x10, 0x10);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC3) {
|
||||
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC3");
|
||||
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x15, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0x00);
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x10, 0x10);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC4) {
|
||||
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC4");
|
||||
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x15, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0x00);
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x10, 0x10);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Microphone selection error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
es7210wire = tw;
|
||||
|
||||
ret |= es7210_write_reg(ES7210_RESET_REG00, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_RESET_REG00, 0x41);
|
||||
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, 0x1f);
|
||||
ret |= es7210_write_reg(ES7210_TIME_CONTROL0_REG09, 0x30); /* Set chip state cycle */
|
||||
ret |= es7210_write_reg(ES7210_TIME_CONTROL1_REG0A, 0x30); /* Set power on state cycle */
|
||||
// ret |= es7210_write_reg(ES7210_ADC12_HPF2_REG23, 0x2a); /* Quick setup */
|
||||
// ret |= es7210_write_reg(ES7210_ADC12_HPF1_REG22, 0x0a);
|
||||
// ret |= es7210_write_reg(ES7210_ADC34_HPF2_REG20, 0x0a);
|
||||
// ret |= es7210_write_reg(ES7210_ADC34_HPF1_REG21, 0x2a);
|
||||
/* Set master/slave audio interface */
|
||||
audio_hal_codec_i2s_iface_t *i2s_cfg = & (codec_cfg->i2s_iface);
|
||||
switch (i2s_cfg->mode) {
|
||||
case AUDIO_HAL_MODE_MASTER: /* MASTER MODE */
|
||||
ESP_LOGI(TAG, "ES7210 in Master mode");
|
||||
// ret |= es7210_update_reg_bit(ES7210_MODE_CONFIG_REG08, 0x01, 0x01);
|
||||
ret |= es7210_write_reg(ES7210_MODE_CONFIG_REG08, 0x20);
|
||||
/* Select clock source for internal mclk */
|
||||
switch (get_es7210_mclk_src()) {
|
||||
case FROM_PAD_PIN:
|
||||
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x00);
|
||||
break;
|
||||
case FROM_CLOCK_DOUBLE_PIN:
|
||||
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x80);
|
||||
break;
|
||||
default:
|
||||
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x00);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case AUDIO_HAL_MODE_SLAVE: /* SLAVE MODE */
|
||||
ESP_LOGI(TAG, "ES7210 in Slave mode");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0xC3); /* Select power off analog, vdda = 3.3V, close vx20ff, VMID select 5KΩ start */
|
||||
ret |= es7210_write_reg(ES7210_MIC12_BIAS_REG41, 0x70); /* Select 2.87v */
|
||||
ret |= es7210_write_reg(ES7210_MIC34_BIAS_REG42, 0x70); /* Select 2.87v */
|
||||
ret |= es7210_write_reg(ES7210_OSR_REG07, 0x20);
|
||||
ret |= es7210_write_reg(ES7210_MAINCLK_REG02, 0xc1); /* Set the frequency division coefficient and use dll except clock doubler, and need to set 0xc1 to clear the state */
|
||||
ret |= es7210_config_sample(i2s_cfg->samples);
|
||||
ret |= es7210_mic_select(mic_select);
|
||||
ret |= es7210_adc_set_gain_all(GAIN_0DB);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_deinit()
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es7210_config_fmt(audio_hal_iface_format_t fmt)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
uint8_t adc_iface = 0;
|
||||
adc_iface = es7210_read_reg(ES7210_SDP_INTERFACE1_REG11);
|
||||
adc_iface &= 0xfc;
|
||||
switch (fmt) {
|
||||
case AUDIO_HAL_I2S_NORMAL:
|
||||
ESP_LOGD(TAG, "ES7210 in I2S Format");
|
||||
adc_iface |= 0x00;
|
||||
break;
|
||||
case AUDIO_HAL_I2S_LEFT:
|
||||
case AUDIO_HAL_I2S_RIGHT:
|
||||
ESP_LOGD(TAG, "ES7210 in LJ Format");
|
||||
adc_iface |= 0x01;
|
||||
break;
|
||||
case AUDIO_HAL_I2S_DSP:
|
||||
if (I2S_DSP_MODE_A) {
|
||||
ESP_LOGD(TAG, "ES7210 in DSP-A Format");
|
||||
adc_iface |= 0x03;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "ES7210 in DSP-B Format");
|
||||
adc_iface |= 0x13;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
adc_iface &= 0xfc;
|
||||
break;
|
||||
}
|
||||
ret |= es7210_write_reg(ES7210_SDP_INTERFACE1_REG11, adc_iface);
|
||||
/* Force ADC1/2 output to SDOUT1 and ADC3/4 output to SDOUT2 */
|
||||
ret |= es7210_write_reg(ES7210_SDP_INTERFACE2_REG12, 0x00);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_set_bits(audio_hal_iface_bits_t bits)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
uint8_t adc_iface = 0;
|
||||
adc_iface = es7210_read_reg(ES7210_SDP_INTERFACE1_REG11);
|
||||
adc_iface &= 0x1f;
|
||||
switch (bits) {
|
||||
case AUDIO_HAL_BIT_LENGTH_16BITS:
|
||||
adc_iface |= 0x60;
|
||||
break;
|
||||
case AUDIO_HAL_BIT_LENGTH_24BITS:
|
||||
adc_iface |= 0x00;
|
||||
break;
|
||||
case AUDIO_HAL_BIT_LENGTH_32BITS:
|
||||
adc_iface |= 0x80;
|
||||
break;
|
||||
default:
|
||||
adc_iface |= 0x60;
|
||||
break;
|
||||
}
|
||||
ret |= es7210_write_reg(ES7210_SDP_INTERFACE1_REG11, adc_iface);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_config_i2s(audio_hal_codec_mode_t mode, audio_hal_codec_i2s_iface_t *iface)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret |= es7210_set_bits(iface->bits);
|
||||
ret |= es7210_config_fmt(iface->fmt);
|
||||
ret |= es7210_config_sample(iface->samples);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_start(uint8_t clock_reg_value)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, clock_reg_value);
|
||||
ret |= es7210_write_reg(ES7210_POWER_DOWN_REG06, 0x00);
|
||||
// ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0x40);
|
||||
ret |= es7210_write_reg(ES7210_MIC1_POWER_REG47, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC2_POWER_REG48, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC3_POWER_REG49, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC4_POWER_REG4A, 0x00);
|
||||
ret |= es7210_mic_select(mic_select);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_stop(void)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret |= es7210_write_reg(ES7210_MIC1_POWER_REG47, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC2_POWER_REG48, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC3_POWER_REG49, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC4_POWER_REG4A, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B,0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0xff);
|
||||
// ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0xc0);
|
||||
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, 0x7f);
|
||||
ret |= es7210_write_reg(ES7210_POWER_DOWN_REG06, 0x07);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_ctrl_state(audio_hal_codec_mode_t mode, audio_hal_ctrl_t ctrl_state)
|
||||
{
|
||||
static uint8_t regv;
|
||||
esp_err_t ret = ESP_OK;
|
||||
// ESP_LOGW(TAG, "ES7210 only supports ADC mode");
|
||||
ret = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||
if ((ret != 0x7f) && (ret != 0xff)) {
|
||||
regv = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||
}
|
||||
if (ctrl_state == AUDIO_HAL_CTRL_START) {
|
||||
ESP_LOGI(TAG, "The ES7210_CLOCK_OFF_REG01 value before stop is %x",regv);
|
||||
ret |= es7210_start(regv);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "The codec is about to stop");
|
||||
regv = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||
ret |= es7210_stop();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_set_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t gain)
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
if (gain < GAIN_0DB) {
|
||||
gain = GAIN_0DB;
|
||||
}
|
||||
|
||||
if (gain > GAIN_37_5DB) {
|
||||
gain = GAIN_37_5DB;
|
||||
}
|
||||
|
||||
if (mic_mask & ES7210_INPUT_MIC1) {
|
||||
ret_val |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x0f, gain);
|
||||
}
|
||||
if (mic_mask & ES7210_INPUT_MIC2) {
|
||||
ret_val |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x0f, gain);
|
||||
}
|
||||
if (mic_mask & ES7210_INPUT_MIC3) {
|
||||
ret_val |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x0f, gain);
|
||||
}
|
||||
if (mic_mask & ES7210_INPUT_MIC4) {
|
||||
ret_val |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x0f, gain);
|
||||
}
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_set_gain_all(es7210_gain_value_t gain)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
uint32_t max_gain_vaule = 14;
|
||||
if (gain < 0) {
|
||||
gain = (es7210_gain_value_t) 0;
|
||||
} else if (gain > max_gain_vaule) {
|
||||
gain = (es7210_gain_value_t) max_gain_vaule;
|
||||
}
|
||||
ESP_LOGD(TAG, "SET: gain:%d", gain);
|
||||
if (mic_select & ES7210_INPUT_MIC1) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x0f, gain);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC2) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x0f, gain);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC3) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x0f, gain);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC4) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x0f, gain);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_get_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t *gain)
|
||||
{
|
||||
int regv = 0;
|
||||
uint8_t gain_value;
|
||||
if (mic_mask & ES7210_INPUT_MIC1) {
|
||||
regv = es7210_read_reg(ES7210_MIC1_GAIN_REG43);
|
||||
} else if (mic_mask & ES7210_INPUT_MIC2) {
|
||||
regv = es7210_read_reg(ES7210_MIC2_GAIN_REG44);
|
||||
} else if (mic_mask & ES7210_INPUT_MIC3) {
|
||||
regv = es7210_read_reg(ES7210_MIC3_GAIN_REG45);
|
||||
} else if (mic_mask & ES7210_INPUT_MIC4) {
|
||||
regv = es7210_read_reg(ES7210_MIC4_GAIN_REG46);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "No MIC selected");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (regv == ESP_FAIL) {
|
||||
return regv;
|
||||
}
|
||||
gain_value = (regv & 0x0f); /* Retain the last four bits for gain */
|
||||
*gain = (es7210_gain_value_t) gain_value;
|
||||
ESP_LOGI(TAG, "GET: gain_value:%d", gain_value);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_set_volume(int volume)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_LOGD(TAG, "ADC can adjust gain");
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_set_mute(bool enable)
|
||||
{
|
||||
ESP_LOGD(TAG, "ES7210 SetMute :%d", enable);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void es7210_read_all(void)
|
||||
{
|
||||
for (int i = 0; i <= 0x4E; i++) {
|
||||
uint8_t reg = es7210_read_reg(i);
|
||||
ets_printf("REG:%02x, %02x\n", reg, i);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -1,260 +0,0 @@
|
||||
/*
|
||||
* ESPRESSIF MIT License
|
||||
*
|
||||
* Copyright (c) 2021 <ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD>
|
||||
*
|
||||
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||
* it is free of charge, to any person obtaining a copy of this software and associated
|
||||
* documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
|
||||
* to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _ES7210_H
|
||||
#define _ES7210_H
|
||||
|
||||
#include "audio_hal.h"
|
||||
#include <Wire.h>
|
||||
|
||||
typedef enum {
|
||||
ES7210_AD1_AD0_00 = 0x40,
|
||||
ES7210_AD1_AD0_01 = 0x41,
|
||||
ES7210_AD1_AD0_10 = 0x42,
|
||||
ES7210_AD1_AD0_11 = 0x43,
|
||||
} es7210_address_t;
|
||||
|
||||
/* ES7210 address*/
|
||||
#define ES7210_ADDR ES7210_AD1_AD0_00
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define ES7210_RESET_REG00 0x00 /* Reset control */
|
||||
#define ES7210_CLOCK_OFF_REG01 0x01 /* Used to turn off the ADC clock */
|
||||
#define ES7210_MAINCLK_REG02 0x02 /* Set ADC clock frequency division */
|
||||
#define ES7210_MASTER_CLK_REG03 0x03 /* MCLK source $ SCLK division */
|
||||
#define ES7210_LRCK_DIVH_REG04 0x04 /* lrck_divh */
|
||||
#define ES7210_LRCK_DIVL_REG05 0x05 /* lrck_divl */
|
||||
#define ES7210_POWER_DOWN_REG06 0x06 /* power down */
|
||||
#define ES7210_OSR_REG07 0x07
|
||||
#define ES7210_MODE_CONFIG_REG08 0x08 /* Set master/slave & channels */
|
||||
#define ES7210_TIME_CONTROL0_REG09 0x09 /* Set Chip intial state period*/
|
||||
#define ES7210_TIME_CONTROL1_REG0A 0x0A /* Set Power up state period */
|
||||
#define ES7210_SDP_INTERFACE1_REG11 0x11 /* Set sample & fmt */
|
||||
#define ES7210_SDP_INTERFACE2_REG12 0x12 /* Pins state */
|
||||
#define ES7210_ADC_AUTOMUTE_REG13 0x13 /* Set mute */
|
||||
#define ES7210_ADC34_MUTERANGE_REG14 0x14 /* Set mute range */
|
||||
#define ES7210_ADC34_HPF2_REG20 0x20 /* HPF */
|
||||
#define ES7210_ADC34_HPF1_REG21 0x21
|
||||
#define ES7210_ADC12_HPF1_REG22 0x22
|
||||
#define ES7210_ADC12_HPF2_REG23 0x23
|
||||
#define ES7210_ANALOG_REG40 0x40 /* ANALOG Power */
|
||||
#define ES7210_MIC12_BIAS_REG41 0x41
|
||||
#define ES7210_MIC34_BIAS_REG42 0x42
|
||||
#define ES7210_MIC1_GAIN_REG43 0x43
|
||||
#define ES7210_MIC2_GAIN_REG44 0x44
|
||||
#define ES7210_MIC3_GAIN_REG45 0x45
|
||||
#define ES7210_MIC4_GAIN_REG46 0x46
|
||||
#define ES7210_MIC1_POWER_REG47 0x47
|
||||
#define ES7210_MIC2_POWER_REG48 0x48
|
||||
#define ES7210_MIC3_POWER_REG49 0x49
|
||||
#define ES7210_MIC4_POWER_REG4A 0x4A
|
||||
#define ES7210_MIC12_POWER_REG4B 0x4B /* MICBias & ADC & PGA Power */
|
||||
#define ES7210_MIC34_POWER_REG4C 0x4C
|
||||
|
||||
|
||||
|
||||
typedef enum {
|
||||
ES7210_INPUT_MIC1 = 0x01,
|
||||
ES7210_INPUT_MIC2 = 0x02,
|
||||
ES7210_INPUT_MIC3 = 0x04,
|
||||
ES7210_INPUT_MIC4 = 0x08
|
||||
} es7210_input_mics_t;
|
||||
|
||||
typedef enum gain_value{
|
||||
GAIN_0DB = 0,
|
||||
GAIN_3DB,
|
||||
GAIN_6DB,
|
||||
GAIN_9DB,
|
||||
GAIN_12DB,
|
||||
GAIN_15DB,
|
||||
GAIN_18DB,
|
||||
GAIN_21DB,
|
||||
GAIN_24DB,
|
||||
GAIN_27DB,
|
||||
GAIN_30DB,
|
||||
GAIN_33DB,
|
||||
GAIN_34_5DB,
|
||||
GAIN_36DB,
|
||||
GAIN_37_5DB,
|
||||
} es7210_gain_value_t;
|
||||
|
||||
/*
|
||||
* @brief Initialize ES7210 ADC chip
|
||||
*
|
||||
* @param[in] codec_cfg: configuration of ES7210
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize ES7210 ADC chip
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_deinit();
|
||||
|
||||
/**
|
||||
* @brief Configure ES7210 ADC mode and I2S interface
|
||||
*
|
||||
* @param[in] mode: codec mode
|
||||
* @param[in] iface: I2S config
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL Parameter error
|
||||
* - ESP_OK Success
|
||||
*/
|
||||
esp_err_t es7210_adc_config_i2s(audio_hal_codec_mode_t mode, audio_hal_codec_i2s_iface_t *iface);
|
||||
|
||||
/**
|
||||
* @brief Control ES7210 ADC chip
|
||||
*
|
||||
* @param[in] mode: codec mode
|
||||
* @param[in] ctrl_state: start or stop progress
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL Parameter error
|
||||
* - ESP_OK Success
|
||||
*/
|
||||
esp_err_t es7210_adc_ctrl_state(audio_hal_codec_mode_t mode, audio_hal_ctrl_t ctrl_state);
|
||||
|
||||
/**
|
||||
* @brief Set gain of given mask
|
||||
*
|
||||
* @param[in] mic_mask Mask of MIC channel
|
||||
*
|
||||
* @param[in] gain: gain
|
||||
*
|
||||
* gain : value
|
||||
* GAIN_0DB : 1
|
||||
* GAIN_3DB : 2
|
||||
* GAIN_6DB : 3
|
||||
* ·
|
||||
* ·
|
||||
* ·
|
||||
* GAIN_30DB : 10
|
||||
* GAIN_33DB : 11
|
||||
* GAIN_34_5DB : 12
|
||||
* GAIN_36DB : 13
|
||||
* GAIN_37_5DB : 14
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_set_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t gain);
|
||||
|
||||
/**
|
||||
* @brief Set gain (Note: the enabled microphone sets the same gain)
|
||||
*
|
||||
* @param[in] gain: gain
|
||||
*
|
||||
* gain : value
|
||||
* GAIN_0DB : 1
|
||||
* GAIN_3DB : 2
|
||||
* GAIN_6DB : 3
|
||||
* ·
|
||||
* ·
|
||||
* ·
|
||||
* GAIN_30DB : 10
|
||||
* GAIN_33DB : 11
|
||||
* GAIN_34_5DB : 12
|
||||
* GAIN_36DB : 13
|
||||
* GAIN_37_5DB : 14
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_set_gain_all(es7210_gain_value_t gain);
|
||||
|
||||
/**
|
||||
* @brief Get MIC gain
|
||||
*
|
||||
* @param mic_mask Selected MIC
|
||||
* @param gain Pointer to `es7210_gain_value_t`
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_get_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t *gain);
|
||||
|
||||
/**
|
||||
* @brief Set volume
|
||||
*
|
||||
* @param[in] volume: volume
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
*/
|
||||
esp_err_t es7210_adc_set_volume(int volume);
|
||||
|
||||
/**
|
||||
* @brief Set ES7210 ADC mute status
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL
|
||||
* - ESP_OK
|
||||
*/
|
||||
esp_err_t es7210_set_mute(bool enable);
|
||||
|
||||
/**
|
||||
* @brief Select ES7210 mic
|
||||
*
|
||||
* @param[in] mic: mics
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL
|
||||
* - ESP_OK
|
||||
*/
|
||||
esp_err_t es7210_mic_select(es7210_input_mics_t mic);
|
||||
|
||||
/**
|
||||
* @brief Read regs of ES7210
|
||||
*
|
||||
* @param[in] reg_addr: reg_addr
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL
|
||||
* - ESP_OK
|
||||
*/
|
||||
int es7210_read_reg(uint8_t reg_addr);
|
||||
|
||||
/**
|
||||
* @brief Read all regs of ES7210
|
||||
*/
|
||||
void es7210_read_all(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _ES7210_H_ */
|
||||
@ -1,443 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "es8311.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_check.h"
|
||||
#include "es8311_reg.h"
|
||||
#include "esp32-hal-i2c.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
unsigned int port;
|
||||
uint16_t dev_addr;
|
||||
} es8311_dev_t;
|
||||
|
||||
/*
|
||||
* Clock coefficient structure
|
||||
*/
|
||||
struct _coeff_div {
|
||||
uint32_t mclk; /* mclk frequency */
|
||||
uint32_t rate; /* sample rate */
|
||||
uint8_t pre_div; /* the pre divider with range from 1 to 8 */
|
||||
uint8_t pre_multi; /* the pre multiplier with 0: 1x, 1: 2x, 2: 4x, 3: 8x selection */
|
||||
uint8_t adc_div; /* adcclk divider */
|
||||
uint8_t dac_div; /* dacclk divider */
|
||||
uint8_t fs_mode; /* double speed or single speed, =0, ss, =1, ds */
|
||||
uint8_t lrck_h; /* adclrck divider and daclrck divider */
|
||||
uint8_t lrck_l;
|
||||
uint8_t bclk_div; /* sclk divider */
|
||||
uint8_t adc_osr; /* adc osr */
|
||||
uint8_t dac_osr; /* dac osr */
|
||||
};
|
||||
|
||||
/* codec hifi mclk clock divider coefficients */
|
||||
static const struct _coeff_div coeff_div[] = {
|
||||
/*!<mclk rate pre_div mult adc_div dac_div fs_mode lrch lrcl bckdiv osr */
|
||||
/* 8k */
|
||||
{12288000, 8000, 0x06, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 8000, 0x03, 0x01, 0x03, 0x03, 0x00, 0x05, 0xff, 0x18, 0x10, 0x10},
|
||||
{16384000, 8000, 0x08, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{8192000, 8000, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 8000, 0x03, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{4096000, 8000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 8000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2048000, 8000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 8000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1024000, 8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 11.025k */
|
||||
{11289600, 11025, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{5644800, 11025, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2822400, 11025, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1411200, 11025, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 12k */
|
||||
{12288000, 12000, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 12000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 12000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 12000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 16k */
|
||||
{12288000, 16000, 0x03, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 16000, 0x03, 0x01, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x10},
|
||||
{16384000, 16000, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{8192000, 16000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 16000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{4096000, 16000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 16000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2048000, 16000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 16000, 0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1024000, 16000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 22.05k */
|
||||
{11289600, 22050, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{5644800, 22050, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2822400, 22050, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1411200, 22050, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{705600, 22050, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 24k */
|
||||
{12288000, 24000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 24000, 0x03, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 24000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 24000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 24000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 32k */
|
||||
{12288000, 32000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 32000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x10},
|
||||
{16384000, 32000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{8192000, 32000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 32000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{4096000, 32000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 32000, 0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2048000, 32000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 32000, 0x03, 0x03, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
|
||||
{1024000, 32000, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 44.1k */
|
||||
{11289600, 44100, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{5644800, 44100, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2822400, 44100, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1411200, 44100, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 48k */
|
||||
{12288000, 48000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 48000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 48000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 48000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 48000, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 64k */
|
||||
{12288000, 64000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 64000, 0x03, 0x02, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
|
||||
{16384000, 64000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{8192000, 64000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 64000, 0x01, 0x02, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
|
||||
{4096000, 64000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 64000, 0x01, 0x03, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
|
||||
{2048000, 64000, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 64000, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0xbf, 0x03, 0x18, 0x18},
|
||||
{1024000, 64000, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
|
||||
|
||||
/* 88.2k */
|
||||
{11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{5644800, 88200, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2822400, 88200, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1411200, 88200, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
|
||||
|
||||
/* 96k */
|
||||
{12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 96000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 96000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 96000, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 96000, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
|
||||
};
|
||||
|
||||
static const char *TAG = "ES8311";
|
||||
|
||||
static inline esp_err_t es8311_write_reg(es8311_handle_t dev, uint8_t reg_addr, uint8_t data)
|
||||
{
|
||||
es8311_dev_t *es = (es8311_dev_t *) dev;
|
||||
const uint8_t write_buf[2] = {reg_addr, data};
|
||||
return i2cWrite(es->port, es->dev_addr, write_buf, sizeof(write_buf), 1000);
|
||||
}
|
||||
|
||||
static inline esp_err_t es8311_read_reg(es8311_handle_t dev, uint8_t reg_addr, uint8_t *reg_value)
|
||||
{
|
||||
es8311_dev_t *es = (es8311_dev_t *) dev;
|
||||
size_t readCount = 0;
|
||||
return i2cWriteReadNonStop(es->port, es->dev_addr, ®_addr, 1, reg_value, 1, 1000, &readCount);
|
||||
}
|
||||
|
||||
/*
|
||||
* look for the coefficient in coeff_div[] table
|
||||
*/
|
||||
static int get_coeff(uint32_t mclk, uint32_t rate)
|
||||
{
|
||||
for (int i = 0; i < (sizeof(coeff_div) / sizeof(coeff_div[0])); i++) {
|
||||
if (coeff_div[i].rate == rate && coeff_div[i].mclk == mclk) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_err_t es8311_sample_frequency_config(es8311_handle_t dev, int mclk_frequency, int sample_frequency)
|
||||
{
|
||||
uint8_t regv;
|
||||
|
||||
/* Get clock coefficients from coefficient table */
|
||||
int coeff = get_coeff(mclk_frequency, sample_frequency);
|
||||
|
||||
if (coeff < 0) {
|
||||
ESP_LOGE(TAG, "Unable to configure sample rate %dHz with %dHz MCLK", sample_frequency, mclk_frequency);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const struct _coeff_div *const selected_coeff = &coeff_div[coeff];
|
||||
|
||||
/* register 0x02 */
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_CLK_MANAGER_REG02, ®v), TAG, "I2C read/write error");
|
||||
regv &= 0x07;
|
||||
regv |= (selected_coeff->pre_div - 1) << 5;
|
||||
regv |= selected_coeff->pre_multi << 3;
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG02, regv), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x03 */
|
||||
const uint8_t reg03 = (selected_coeff->fs_mode << 6) | selected_coeff->adc_osr;
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG03, reg03), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x04 */
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG04, selected_coeff->dac_osr), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x05 */
|
||||
const uint8_t reg05 = ((selected_coeff->adc_div - 1) << 4) | (selected_coeff->dac_div - 1);
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG05, reg05), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x06 */
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_CLK_MANAGER_REG06, ®v), TAG, "I2C read/write error");
|
||||
regv &= 0xE0;
|
||||
|
||||
if (selected_coeff->bclk_div < 19) {
|
||||
regv |= (selected_coeff->bclk_div - 1) << 0;
|
||||
} else {
|
||||
regv |= (selected_coeff->bclk_div) << 0;
|
||||
}
|
||||
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG06, regv), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x07 */
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_CLK_MANAGER_REG07, ®v), TAG, "I2C read/write error");
|
||||
regv &= 0xC0;
|
||||
regv |= selected_coeff->lrck_h << 0;
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG07, regv), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x08 */
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG08, selected_coeff->lrck_l), TAG, "I2C read/write error");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t es8311_clock_config(es8311_handle_t dev, const es8311_clock_config_t *const clk_cfg, es8311_resolution_t res)
|
||||
{
|
||||
uint8_t reg06;
|
||||
uint8_t reg01 = 0x3F; // Enable all clocks
|
||||
int mclk_hz;
|
||||
|
||||
/* Select clock source for internal MCLK and determine its frequency */
|
||||
if (clk_cfg->mclk_from_mclk_pin) {
|
||||
mclk_hz = clk_cfg->mclk_frequency;
|
||||
} else {
|
||||
mclk_hz = clk_cfg->sample_frequency * (int)res * 2;
|
||||
reg01 |= BIT(7); // Select BCLK (a.k.a. SCK) pin
|
||||
}
|
||||
|
||||
if (clk_cfg->mclk_inverted) {
|
||||
reg01 |= BIT(6); // Invert MCLK pin
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG01, reg01), TAG, "I2C read/write error");
|
||||
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_CLK_MANAGER_REG06, ®06), TAG, "I2C read/write error");
|
||||
if (clk_cfg->sclk_inverted) {
|
||||
reg06 |= BIT(5);
|
||||
} else {
|
||||
reg06 &= ~BIT(5);
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG06, reg06), TAG, "I2C read/write error");
|
||||
|
||||
/* Configure clock dividers */
|
||||
return es8311_sample_frequency_config(dev, mclk_hz, clk_cfg->sample_frequency);
|
||||
}
|
||||
|
||||
static esp_err_t es8311_resolution_config(const es8311_resolution_t res, uint8_t *reg)
|
||||
{
|
||||
switch (res) {
|
||||
case ES8311_RESOLUTION_16:
|
||||
*reg |= (3 << 2);
|
||||
break;
|
||||
case ES8311_RESOLUTION_18:
|
||||
*reg |= (2 << 2);
|
||||
break;
|
||||
case ES8311_RESOLUTION_20:
|
||||
*reg |= (1 << 2);
|
||||
break;
|
||||
case ES8311_RESOLUTION_24:
|
||||
*reg |= (0 << 2);
|
||||
break;
|
||||
case ES8311_RESOLUTION_32:
|
||||
*reg |= (4 << 2);
|
||||
break;
|
||||
default:
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t es8311_fmt_config(es8311_handle_t dev, const es8311_resolution_t res_in, const es8311_resolution_t res_out)
|
||||
{
|
||||
uint8_t reg09 = 0; // SDP In
|
||||
uint8_t reg0a = 0; // SDP Out
|
||||
|
||||
ESP_LOGI(TAG, "ES8311 in Slave mode and I2S format");
|
||||
uint8_t reg00;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_RESET_REG00, ®00), TAG, "I2C read/write error");
|
||||
reg00 &= 0xBF;
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_RESET_REG00, reg00), TAG, "I2C read/write error"); // Slave serial port - default
|
||||
|
||||
/* Setup SDP In and Out resolution */
|
||||
es8311_resolution_config(res_in, ®09);
|
||||
es8311_resolution_config(res_out, ®0a);
|
||||
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SDPIN_REG09, reg09), TAG, "I2C read/write error");
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SDPOUT_REG0A, reg0a), TAG, "I2C read/write error");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es8311_microphone_config(es8311_handle_t dev, bool digital_mic)
|
||||
{
|
||||
uint8_t reg14 = 0x1A; // enable analog MIC and max PGA gain
|
||||
|
||||
/* PDM digital microphone enable or disable */
|
||||
if (digital_mic) {
|
||||
reg14 |= BIT(6);
|
||||
}
|
||||
es8311_write_reg(dev, ES8311_ADC_REG17, 0xC8); // Set ADC gain @todo move this to ADC config section
|
||||
|
||||
return es8311_write_reg(dev, ES8311_SYSTEM_REG14, reg14);
|
||||
}
|
||||
|
||||
esp_err_t es8311_init(es8311_handle_t dev, const es8311_clock_config_t *const clk_cfg, const es8311_resolution_t res_in, const es8311_resolution_t res_out)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(
|
||||
(clk_cfg->sample_frequency >= 8000) && (clk_cfg->sample_frequency <= 96000),
|
||||
ESP_ERR_INVALID_ARG, TAG, "ES8311 init needs frequency in interval [8000; 96000] Hz"
|
||||
);
|
||||
if (!clk_cfg->mclk_from_mclk_pin) {
|
||||
ESP_RETURN_ON_FALSE(res_out == res_in, ESP_ERR_INVALID_ARG, TAG, "Resolution IN/OUT must be equal if MCLK is taken from SCK pin");
|
||||
}
|
||||
|
||||
|
||||
/* Reset ES8311 to its default */
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_RESET_REG00, 0x1F), TAG, "I2C read/write error");
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_RESET_REG00, 0x00), TAG, "I2C read/write error");
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_RESET_REG00, 0x80), TAG, "I2C read/write error"); // Power-on command
|
||||
|
||||
/* Setup clock: source, polarity and clock dividers */
|
||||
ESP_RETURN_ON_ERROR(es8311_clock_config(dev, clk_cfg, res_out), TAG, "");
|
||||
|
||||
/* Setup audio format (fmt): master/slave, resolution, I2S */
|
||||
ESP_RETURN_ON_ERROR(es8311_fmt_config(dev, res_in, res_out), TAG, "");
|
||||
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SYSTEM_REG0D, 0x01), TAG, "I2C read/write error"); // Power up analog circuitry - NOT default
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SYSTEM_REG0E, 0x02), TAG, "I2C read/write error"); // Enable analog PGA, enable ADC modulator - NOT default
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SYSTEM_REG12, 0x00), TAG, "I2C read/write error"); // power-up DAC - NOT default
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SYSTEM_REG13, 0x10), TAG, "I2C read/write error"); // Enable output to HP drive - NOT default
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_ADC_REG1C, 0x6A), TAG, "I2C read/write error"); // ADC Equalizer bypass, cancel DC offset in digital domain
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_DAC_REG37, 0x08), TAG, "I2C read/write error"); // Bypass DAC equalizer - NOT default
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void es8311_delete(es8311_handle_t dev)
|
||||
{
|
||||
free(dev);
|
||||
}
|
||||
|
||||
esp_err_t es8311_voice_volume_set(es8311_handle_t dev, int volume, int *volume_set)
|
||||
{
|
||||
if (volume < 0) {
|
||||
volume = 0;
|
||||
} else if (volume > 100) {
|
||||
volume = 100;
|
||||
}
|
||||
|
||||
int reg32;
|
||||
if (volume == 0) {
|
||||
reg32 = 0;
|
||||
} else {
|
||||
reg32 = ((volume) * 256 / 100) - 1;
|
||||
}
|
||||
|
||||
// provide user with real volume set
|
||||
if (volume_set != NULL) {
|
||||
*volume_set = volume;
|
||||
}
|
||||
return es8311_write_reg(dev, ES8311_DAC_REG32, reg32);
|
||||
}
|
||||
|
||||
esp_err_t es8311_voice_volume_get(es8311_handle_t dev, int *volume)
|
||||
{
|
||||
uint8_t reg32;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_DAC_REG32, ®32), TAG, "I2C read/write error");
|
||||
|
||||
if (reg32 == 0) {
|
||||
*volume = 0;
|
||||
} else {
|
||||
*volume = ((reg32 * 100) / 256) + 1;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es8311_voice_mute(es8311_handle_t dev, bool mute)
|
||||
{
|
||||
uint8_t reg31;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_DAC_REG31, ®31), TAG, "I2C read/write error");
|
||||
|
||||
if (mute) {
|
||||
reg31 |= BIT(6) | BIT(5);
|
||||
} else {
|
||||
reg31 &= ~(BIT(6) | BIT(5));
|
||||
}
|
||||
|
||||
return es8311_write_reg(dev, ES8311_DAC_REG31, reg31);
|
||||
}
|
||||
|
||||
esp_err_t es8311_microphone_gain_set(es8311_handle_t dev, es8311_mic_gain_t gain_db)
|
||||
{
|
||||
return es8311_write_reg(dev, ES8311_ADC_REG16, gain_db); // ADC gain scale up
|
||||
}
|
||||
|
||||
esp_err_t es8311_voice_fade(es8311_handle_t dev, const es8311_fade_t fade)
|
||||
{
|
||||
uint8_t reg37;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_DAC_REG37, ®37), TAG, "I2C read/write error");
|
||||
reg37 &= 0x0F;
|
||||
reg37 |= (fade << 4);
|
||||
return es8311_write_reg(dev, ES8311_DAC_REG37, reg37);
|
||||
}
|
||||
|
||||
esp_err_t es8311_microphone_fade(es8311_handle_t dev, const es8311_fade_t fade)
|
||||
{
|
||||
uint8_t reg15;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_ADC_REG15, ®15), TAG, "I2C read/write error");
|
||||
reg15 &= 0x0F;
|
||||
reg15 |= (fade << 4);
|
||||
return es8311_write_reg(dev, ES8311_ADC_REG15, reg15);
|
||||
}
|
||||
|
||||
void es8311_register_dump(es8311_handle_t dev)
|
||||
{
|
||||
for (int reg = 0; reg < 0x4A; reg++) {
|
||||
uint8_t value;
|
||||
ESP_ERROR_CHECK(es8311_read_reg(dev, reg, &value));
|
||||
printf("REG:%02x: %02x", reg, value);
|
||||
}
|
||||
}
|
||||
|
||||
es8311_handle_t es8311_create(const unsigned int port, const uint16_t dev_addr)
|
||||
{
|
||||
es8311_dev_t *sensor = (es8311_dev_t *) calloc(1, sizeof(es8311_dev_t));
|
||||
sensor->port = port;
|
||||
sensor->dev_addr = dev_addr;
|
||||
return (es8311_handle_t) sensor;
|
||||
}
|
||||
@ -1,227 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @brief ES8311 driver
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_types.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
/* ES8311 address: CE pin low - 0x18, CE pin high - 0x19 */
|
||||
#define ES8311_ADDRRES_0 0x18u // Leaving this here for backward compatibility
|
||||
#define ES8311_ADDRESS_0 0x18u
|
||||
#define ES8311_ADDRESS_1 0x19u
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void *es8311_handle_t;
|
||||
|
||||
typedef enum {
|
||||
ES8311_MIC_GAIN_MIN = -1,
|
||||
ES8311_MIC_GAIN_0DB,
|
||||
ES8311_MIC_GAIN_6DB,
|
||||
ES8311_MIC_GAIN_12DB,
|
||||
ES8311_MIC_GAIN_18DB,
|
||||
ES8311_MIC_GAIN_24DB,
|
||||
ES8311_MIC_GAIN_30DB,
|
||||
ES8311_MIC_GAIN_36DB,
|
||||
ES8311_MIC_GAIN_42DB,
|
||||
ES8311_MIC_GAIN_MAX
|
||||
} es8311_mic_gain_t;
|
||||
|
||||
typedef enum {
|
||||
ES8311_FADE_OFF = 0,
|
||||
ES8311_FADE_4LRCK, // 4LRCK means ramp 0.25dB/4LRCK
|
||||
ES8311_FADE_8LRCK,
|
||||
ES8311_FADE_16LRCK,
|
||||
ES8311_FADE_32LRCK,
|
||||
ES8311_FADE_64LRCK,
|
||||
ES8311_FADE_128LRCK,
|
||||
ES8311_FADE_256LRCK,
|
||||
ES8311_FADE_512LRCK,
|
||||
ES8311_FADE_1024LRCK,
|
||||
ES8311_FADE_2048LRCK,
|
||||
ES8311_FADE_4096LRCK,
|
||||
ES8311_FADE_8192LRCK,
|
||||
ES8311_FADE_16384LRCK,
|
||||
ES8311_FADE_32768LRCK,
|
||||
ES8311_FADE_65536LRCK
|
||||
} es8311_fade_t;
|
||||
|
||||
typedef enum es8311_resolution_t {
|
||||
ES8311_RESOLUTION_16 = 16,
|
||||
ES8311_RESOLUTION_18 = 18,
|
||||
ES8311_RESOLUTION_20 = 20,
|
||||
ES8311_RESOLUTION_24 = 24,
|
||||
ES8311_RESOLUTION_32 = 32
|
||||
} es8311_resolution_t;
|
||||
|
||||
typedef struct es8311_clock_config_t {
|
||||
bool mclk_inverted;
|
||||
bool sclk_inverted;
|
||||
bool mclk_from_mclk_pin; // true: from MCLK pin (pin no. 2), false: from SCLK pin (pin no. 6)
|
||||
int mclk_frequency; // This parameter is ignored if MCLK is taken from SCLK pin
|
||||
int sample_frequency; // in Hz
|
||||
} es8311_clock_config_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize ES8311
|
||||
*
|
||||
* There are two ways of providing Master Clock (MCLK) signal to ES8311 in Slave Mode:
|
||||
* 1. From MCLK pin:
|
||||
* For flexible scenarios. A clock signal from I2S master is routed to MCLK pin.
|
||||
* Its frequency must be defined in clk_cfg->mclk_frequency parameter.
|
||||
* 2. From SCLK pin:
|
||||
* For simpler scenarios. ES8311 takes its clock from SCK pin. MCLK pin does not have to be connected.
|
||||
* In this case, res_in must equal res_out; clk_cfg->mclk_frequency parameter is ignored
|
||||
* and MCLK is calculated as MCLK = clk_cfg->sample_frequency * res_out * 2.
|
||||
* Not all sampling frequencies are supported in this mode.
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] clk_cfg Clock configuration
|
||||
* @param[in] res_in Input serial port resolution
|
||||
* @param[in] res_out Output serial port resolution
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - ESP_ERR_INVALID_ARG Sample frequency or resolution invalid
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_init(es8311_handle_t dev, const es8311_clock_config_t *const clk_cfg, const es8311_resolution_t res_in,
|
||||
const es8311_resolution_t res_out);
|
||||
|
||||
/**
|
||||
* @brief Set output volume
|
||||
*
|
||||
* Volume paramter out of <0, 100> interval will be truncated.
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] volume Set volume (0 ~ 100)
|
||||
* @param[out] volume_set Volume that was set. Same as volume, unless volume is outside of <0, 100> interval.
|
||||
* This parameter can be set to NULL, if user does not need this information.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_voice_volume_set(es8311_handle_t dev, int volume, int *volume_set);
|
||||
|
||||
/**
|
||||
* @brief Get output volume
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[out] volume get volume (0 ~ 100)
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_voice_volume_get(es8311_handle_t dev, int *volume);
|
||||
|
||||
/**
|
||||
* @brief Print out ES8311 register content
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
*/
|
||||
void es8311_register_dump(es8311_handle_t dev);
|
||||
|
||||
/**
|
||||
* @brief Mute ES8311 output
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] enable true: mute, false: don't mute
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_voice_mute(es8311_handle_t dev, bool enable);
|
||||
|
||||
/**
|
||||
* @brief Set Microphone gain
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] gain_db Microphone gain
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_microphone_gain_set(es8311_handle_t dev, es8311_mic_gain_t gain_db);
|
||||
|
||||
/**
|
||||
* @brief Configure microphone
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] digital_mic Set to true for digital microphone
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_microphone_config(es8311_handle_t dev, bool digital_mic);
|
||||
|
||||
/**
|
||||
* @brief Configure sampling frequency
|
||||
*
|
||||
* @note This function is called by es8311_init().
|
||||
* Call this function explicitly only if you want to change sample frequency during runtime.
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] mclk_frequency MCLK frequency in [Hz] (MCLK or SCLK pin, depending on bit register01[7])
|
||||
* @param[in] sample_frequency Required sample frequency in [Hz], e.g. 44100, 22050...
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - ESP_ERR_INVALID_ARG cannot set clock dividers for given MCLK and sampling frequency
|
||||
* - Else I2C read/write error
|
||||
*/
|
||||
esp_err_t es8311_sample_frequency_config(es8311_handle_t dev, int mclk_frequency, int sample_frequency);
|
||||
|
||||
/**
|
||||
* @brief Configure fade in/out for ADC: voice
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] fade Fade ramp rate
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else I2C read/write error
|
||||
*/
|
||||
esp_err_t es8311_voice_fade(es8311_handle_t dev, const es8311_fade_t fade);
|
||||
|
||||
/**
|
||||
* @brief Configure fade in/out for DAC: microphone
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] fade Fade ramp rate
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else I2C read/write error
|
||||
*/
|
||||
esp_err_t es8311_microphone_fade(es8311_handle_t dev, const es8311_fade_t fade);
|
||||
|
||||
/**
|
||||
* @brief Create ES8311 object and return its handle
|
||||
*
|
||||
* @param[in] port I2C port number
|
||||
* @param[in] dev_addr I2C device address of ES8311
|
||||
*
|
||||
* @return
|
||||
* - NULL Fail
|
||||
* - Others Success
|
||||
*/
|
||||
es8311_handle_t es8311_create(const unsigned int port, const uint16_t dev_addr);
|
||||
|
||||
/**
|
||||
* @brief Delete ES8311 object
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
*/
|
||||
void es8311_delete(es8311_handle_t dev);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -1,76 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* ES8311_REGISTER NAME_REG_REGISTER ADDRESS
|
||||
*/
|
||||
#define ES8311_RESET_REG00 0x00 /*reset digital,csm,clock manager etc.*/
|
||||
|
||||
/*
|
||||
* Clock Scheme Register definition
|
||||
*/
|
||||
#define ES8311_CLK_MANAGER_REG01 0x01 /* select clk src for mclk, enable clock for codec */
|
||||
#define ES8311_CLK_MANAGER_REG02 0x02 /* clk divider and clk multiplier */
|
||||
#define ES8311_CLK_MANAGER_REG03 0x03 /* adc fsmode and osr */
|
||||
#define ES8311_CLK_MANAGER_REG04 0x04 /* dac osr */
|
||||
#define ES8311_CLK_MANAGER_REG05 0x05 /* clk divier for adc and dac */
|
||||
#define ES8311_CLK_MANAGER_REG06 0x06 /* bclk inverter and divider */
|
||||
#define ES8311_CLK_MANAGER_REG07 0x07 /* tri-state, lrck divider */
|
||||
#define ES8311_CLK_MANAGER_REG08 0x08 /* lrck divider */
|
||||
/*
|
||||
* SDP
|
||||
*/
|
||||
#define ES8311_SDPIN_REG09 0x09 /* dac serial digital port */
|
||||
#define ES8311_SDPOUT_REG0A 0x0A /* adc serial digital port */
|
||||
/*
|
||||
* SYSTEM
|
||||
*/
|
||||
#define ES8311_SYSTEM_REG0B 0x0B /* system */
|
||||
#define ES8311_SYSTEM_REG0C 0x0C /* system */
|
||||
#define ES8311_SYSTEM_REG0D 0x0D /* system, power up/down */
|
||||
#define ES8311_SYSTEM_REG0E 0x0E /* system, power up/down */
|
||||
#define ES8311_SYSTEM_REG0F 0x0F /* system, low power */
|
||||
#define ES8311_SYSTEM_REG10 0x10 /* system */
|
||||
#define ES8311_SYSTEM_REG11 0x11 /* system */
|
||||
#define ES8311_SYSTEM_REG12 0x12 /* system, Enable DAC */
|
||||
#define ES8311_SYSTEM_REG13 0x13 /* system */
|
||||
#define ES8311_SYSTEM_REG14 0x14 /* system, select DMIC, select analog pga gain */
|
||||
/*
|
||||
* ADC
|
||||
*/
|
||||
#define ES8311_ADC_REG15 0x15 /* ADC, adc ramp rate, dmic sense */
|
||||
#define ES8311_ADC_REG16 0x16 /* ADC */
|
||||
#define ES8311_ADC_REG17 0x17 /* ADC, volume */
|
||||
#define ES8311_ADC_REG18 0x18 /* ADC, alc enable and winsize */
|
||||
#define ES8311_ADC_REG19 0x19 /* ADC, alc maxlevel */
|
||||
#define ES8311_ADC_REG1A 0x1A /* ADC, alc automute */
|
||||
#define ES8311_ADC_REG1B 0x1B /* ADC, alc automute, adc hpf s1 */
|
||||
#define ES8311_ADC_REG1C 0x1C /* ADC, equalizer, hpf s2 */
|
||||
/*
|
||||
* DAC
|
||||
*/
|
||||
#define ES8311_DAC_REG31 0x31 /* DAC, mute */
|
||||
#define ES8311_DAC_REG32 0x32 /* DAC, volume */
|
||||
#define ES8311_DAC_REG33 0x33 /* DAC, offset */
|
||||
#define ES8311_DAC_REG34 0x34 /* DAC, drc enable, drc winsize */
|
||||
#define ES8311_DAC_REG35 0x35 /* DAC, drc maxlevel, minilevel */
|
||||
#define ES8311_DAC_REG37 0x37 /* DAC, ramprate */
|
||||
/*
|
||||
*GPIO
|
||||
*/
|
||||
#define ES8311_GPIO_REG44 0x44 /* GPIO, dac2adc for test */
|
||||
#define ES8311_GP_REG45 0x45 /* GP CONTROL */
|
||||
/*
|
||||
* CHIP
|
||||
*/
|
||||
#define ES8311_CHD1_REGFD 0xFD /* CHIP ID1 */
|
||||
#define ES8311_CHD2_REGFE 0xFE /* CHIP ID2 */
|
||||
#define ES8311_CHVER_REGFF 0xFF /* VERSION */
|
||||
#define ES8311_CHD1_REGFD 0xFD /* CHIP ID1 */
|
||||
|
||||
#define ES8311_MAX_REGISTER 0xFF
|
||||
@ -1,272 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <math.h>
|
||||
#include "ESP_I2S.h"
|
||||
#include "esp_check.h"
|
||||
#include "Arduino_GFX_Library.h"
|
||||
#include "pin_config.h"
|
||||
#include "TouchDrvCSTXXX.hpp"
|
||||
#include "SensorQMI8658.hpp"
|
||||
#include "es8311.h"
|
||||
#include "es7210.h"
|
||||
|
||||
static const int DISP_W = 480;
|
||||
static const int DISP_H = 480;
|
||||
static const int SAFE = 20;
|
||||
static const uint32_t SAMPLE_RATE = 16000;
|
||||
static const size_t REC_SECONDS = 2;
|
||||
static const size_t AUDIO_BYTES = SAMPLE_RATE * REC_SECONDS * 2 * 2;
|
||||
static const int I2C_NUM = 0;
|
||||
|
||||
I2SClass i2s;
|
||||
TouchDrvCST92xx touch;
|
||||
SensorQMI8658 qmi;
|
||||
IMUdata acc;
|
||||
|
||||
Arduino_DataBus *bus = new Arduino_ESP32QSPI(LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(bus, LCD_RESET, 0, DISP_W, DISP_H, 0, 0, 0, 0);
|
||||
|
||||
uint8_t *audioBuf = nullptr;
|
||||
size_t audioLen = 0;
|
||||
bool touchIrq = false;
|
||||
String statusLine = "Ready";
|
||||
String tiltLine = "Tilt: FLAT";
|
||||
String audioLine = "Audio: empty";
|
||||
uint32_t lastTiltMs = 0;
|
||||
bool micReady = false;
|
||||
|
||||
void IRAM_ATTR onTouchInt() { touchIrq = true; }
|
||||
|
||||
esp_err_t codecInit() {
|
||||
es8311_handle_t esHandle = es8311_create(I2C_NUM, ES8311_ADDRRES_0);
|
||||
ESP_RETURN_ON_FALSE(esHandle, ESP_FAIL, "AV", "es8311 create failed");
|
||||
const es8311_clock_config_t clk = {
|
||||
.mclk_inverted = false,
|
||||
.sclk_inverted = false,
|
||||
.mclk_from_mclk_pin = true,
|
||||
.mclk_frequency = SAMPLE_RATE * 256,
|
||||
.sample_frequency = SAMPLE_RATE,
|
||||
};
|
||||
ESP_ERROR_CHECK(es8311_init(esHandle, &clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
|
||||
ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(esHandle, clk.mclk_frequency, clk.sample_frequency), "AV", "clock cfg failed");
|
||||
ESP_RETURN_ON_ERROR(es8311_microphone_config(esHandle, false), "AV", "mic cfg failed");
|
||||
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(esHandle, 90, NULL), "AV", "volume failed");
|
||||
ESP_RETURN_ON_ERROR(es8311_microphone_gain_set(esHandle, (es8311_mic_gain_t)6), "AV", "mic gain failed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t micInit() {
|
||||
audio_hal_codec_config_t cfg = {
|
||||
.adc_input = AUDIO_HAL_ADC_INPUT_ALL,
|
||||
.codec_mode = AUDIO_HAL_CODEC_MODE_ENCODE,
|
||||
.i2s_iface = {
|
||||
.mode = AUDIO_HAL_MODE_SLAVE,
|
||||
.fmt = AUDIO_HAL_I2S_NORMAL,
|
||||
.samples = AUDIO_HAL_16K_SAMPLES,
|
||||
.bits = AUDIO_HAL_BIT_LENGTH_16BITS,
|
||||
},
|
||||
};
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret |= es7210_adc_init(&Wire, &cfg);
|
||||
ret |= es7210_adc_config_i2s(cfg.codec_mode, &cfg.i2s_iface);
|
||||
ret |= es7210_adc_set_gain(
|
||||
(es7210_input_mics_t)(ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2),
|
||||
(es7210_gain_value_t)GAIN_24DB);
|
||||
ret |= es7210_adc_set_gain(
|
||||
(es7210_input_mics_t)(ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4),
|
||||
(es7210_gain_value_t)GAIN_24DB);
|
||||
ret |= es7210_adc_ctrl_state(cfg.codec_mode, AUDIO_HAL_CTRL_START);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void drawStaticUi() {
|
||||
gfx->fillScreen(RGB565_BLACK);
|
||||
gfx->setTextSize(2);
|
||||
gfx->setTextColor(RGB565_WHITE);
|
||||
gfx->setCursor(SAFE + 16, SAFE + 10);
|
||||
gfx->println("Simple AV + Tilt Test");
|
||||
|
||||
gfx->drawRect(24, 76, 132, 64, RGB565_GREEN);
|
||||
gfx->setCursor(44, 100);
|
||||
gfx->println("RECORD");
|
||||
|
||||
gfx->drawRect(174, 76, 132, 64, RGB565_YELLOW);
|
||||
gfx->setCursor(212, 100);
|
||||
gfx->println("PLAY");
|
||||
|
||||
gfx->drawRect(324, 76, 132, 64, RGB565_CYAN);
|
||||
gfx->setCursor(360, 100);
|
||||
gfx->println("BEEP");
|
||||
}
|
||||
|
||||
void drawDynamic() {
|
||||
gfx->fillRect(20, 166, 440, 140, RGB565_BLACK);
|
||||
gfx->setTextSize(2);
|
||||
gfx->setTextColor(RGB565_WHITE);
|
||||
gfx->setCursor(28, 178);
|
||||
gfx->println(statusLine);
|
||||
gfx->setTextColor(RGB565_CYAN);
|
||||
gfx->setCursor(28, 212);
|
||||
gfx->println(tiltLine);
|
||||
gfx->setTextColor(RGB565_ORANGE);
|
||||
gfx->setCursor(28, 246);
|
||||
gfx->println(audioLine);
|
||||
gfx->drawRect(28, 278, 300, 20, RGB565_DARKGREY);
|
||||
}
|
||||
|
||||
void updateTilt() {
|
||||
if (!qmi.getDataReady()) return;
|
||||
if (!qmi.getAccelerometer(acc.x, acc.y, acc.z)) return;
|
||||
String next = "Tilt: FLAT";
|
||||
if (acc.x > 0.8f) next = "Tilt: RIGHT";
|
||||
else if (acc.x < -0.8f) next = "Tilt: LEFT";
|
||||
else if (acc.y > 0.8f) next = "Tilt: UP";
|
||||
else if (acc.y < -0.8f) next = "Tilt: DOWN";
|
||||
if (next != tiltLine) {
|
||||
tiltLine = next;
|
||||
drawDynamic();
|
||||
}
|
||||
}
|
||||
|
||||
void playBeep() {
|
||||
static int16_t tone[16000];
|
||||
for (int i = 0; i < 8000; ++i) {
|
||||
float s = sinf(2.0f * 3.1415926f * 440.0f * ((float)i / SAMPLE_RATE));
|
||||
int16_t v = (int16_t)(s * 6000.0f);
|
||||
tone[i * 2] = v;
|
||||
tone[i * 2 + 1] = v;
|
||||
}
|
||||
statusLine = "Playing test beep...";
|
||||
drawDynamic();
|
||||
i2s.write((uint8_t *)tone, 8000 * 2 * sizeof(int16_t));
|
||||
statusLine = "Beep done";
|
||||
drawDynamic();
|
||||
}
|
||||
|
||||
void recordAudio() {
|
||||
if (!audioBuf || !micReady) {
|
||||
statusLine = "No audio buffer";
|
||||
drawDynamic();
|
||||
return;
|
||||
}
|
||||
statusLine = "Recording 2 sec...";
|
||||
drawDynamic();
|
||||
size_t total = 0;
|
||||
uint16_t peakAll = 0;
|
||||
while (total < AUDIO_BYTES) {
|
||||
size_t chunk = min((size_t)4096, AUDIO_BYTES - total);
|
||||
size_t got = i2s.readBytes((char *)(audioBuf + total), chunk);
|
||||
if (!got) break;
|
||||
total += got;
|
||||
|
||||
int16_t *s = (int16_t *)(audioBuf + total - got);
|
||||
size_t n = got / sizeof(int16_t);
|
||||
uint16_t peak = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint16_t a = (uint16_t)abs((int)s[i]);
|
||||
if (a > peak) peak = a;
|
||||
}
|
||||
if (peak > peakAll) peakAll = peak;
|
||||
|
||||
int bar = map((int)peak, 0, 20000, 0, 296);
|
||||
if (bar < 0) bar = 0;
|
||||
if (bar > 296) bar = 296;
|
||||
gfx->fillRect(30, 280, 296, 16, RGB565_BLACK);
|
||||
uint16_t c = (peak > 12000) ? RGB565_RED : ((peak > 6000) ? RGB565_YELLOW : RGB565_GREEN);
|
||||
gfx->fillRect(30, 280, bar, 16, c);
|
||||
}
|
||||
audioLen = total;
|
||||
audioLine = (total > 0) ? "Audio: recorded" : "Audio: empty";
|
||||
if (total > 0) {
|
||||
statusLine = String("Record done, peak=") + String((int)peakAll);
|
||||
} else {
|
||||
statusLine = "Record failed";
|
||||
}
|
||||
drawDynamic();
|
||||
}
|
||||
|
||||
void playAudio() {
|
||||
if (!audioLen) {
|
||||
statusLine = "No recording. Try BEEP.";
|
||||
drawDynamic();
|
||||
return;
|
||||
}
|
||||
statusLine = "Playing recording...";
|
||||
drawDynamic();
|
||||
size_t wrote = i2s.write(audioBuf, audioLen);
|
||||
statusLine = (wrote > 0) ? "Play done" : "Play failed";
|
||||
drawDynamic();
|
||||
}
|
||||
|
||||
void handleTouch() {
|
||||
if (!touchIrq) return;
|
||||
touchIrq = false;
|
||||
int16_t x[1];
|
||||
int16_t y[1];
|
||||
uint8_t touched = touch.getPoint(x, y, 1);
|
||||
if (!touched) return;
|
||||
|
||||
int px = x[0];
|
||||
int py = y[0];
|
||||
if (py < 76 || py > 140) return;
|
||||
if (px >= 24 && px <= 156) recordAudio();
|
||||
else if (px >= 174 && px <= 306) playAudio();
|
||||
else if (px >= 324 && px <= 456) playBeep();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
|
||||
pinMode(PA, OUTPUT);
|
||||
digitalWrite(PA, HIGH);
|
||||
|
||||
if (!gfx->begin()) Serial.println("gfx begin failed");
|
||||
bus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(200);
|
||||
|
||||
pinMode(TP_RST, OUTPUT);
|
||||
digitalWrite(TP_RST, LOW);
|
||||
delay(30);
|
||||
digitalWrite(TP_RST, HIGH);
|
||||
delay(50);
|
||||
touch.setPins(TP_RST, TP_INT);
|
||||
touch.begin(Wire, 0x5A, IIC_SDA, IIC_SCL);
|
||||
touch.setMaxCoordinates(480, 480);
|
||||
touch.setSwapXY(true);
|
||||
touch.setMirrorXY(true, false);
|
||||
attachInterrupt(TP_INT, onTouchInt, FALLING);
|
||||
|
||||
if (qmi.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {
|
||||
qmi.configAccelerometer(SensorQMI8658::ACC_RANGE_4G, SensorQMI8658::ACC_ODR_1000Hz, SensorQMI8658::LPF_MODE_0);
|
||||
qmi.enableAccelerometer();
|
||||
} else {
|
||||
tiltLine = "Tilt: IMU not found";
|
||||
}
|
||||
|
||||
i2s.setPins(9, 45, 8, 10, 42);
|
||||
if (!i2s.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO, I2S_STD_SLOT_BOTH)) {
|
||||
statusLine = "I2S init failed";
|
||||
} else if (codecInit() != ESP_OK) {
|
||||
statusLine = "ES8311 init failed";
|
||||
} else if (micInit() != ESP_OK) {
|
||||
statusLine = "ES7210 init failed";
|
||||
} else {
|
||||
micReady = true;
|
||||
}
|
||||
|
||||
audioBuf = (uint8_t *)ps_malloc(AUDIO_BYTES);
|
||||
if (!audioBuf) statusLine = "No PSRAM buffer";
|
||||
|
||||
drawStaticUi();
|
||||
drawDynamic();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
handleTouch();
|
||||
if (millis() - lastTiltMs > 120) {
|
||||
lastTiltMs = millis();
|
||||
updateTilt();
|
||||
}
|
||||
delay(5);
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1,17 +1,8 @@
|
||||
TELEGRAM_BOT_TOKEN=replace_me
|
||||
OPENAI_API_KEY=replace_me
|
||||
ALLOWED_TELEGRAM_USERNAME=AidarKC
|
||||
ALLOWED_TELEGRAM_PLAYERS=malvviiina:Милана,zodiaktechnika32:Сергей,oidasyda:Иван,blackbyrd1:Ворон,dimasol1:Дима
|
||||
ALLOWED_TELEGRAM_CHANNEL_USERNAME=shine_writing
|
||||
BOT_USERNAME=aidar_su_bot
|
||||
OPENAI_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
|
||||
TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS=300
|
||||
OPENAI_TRANSCRIBE_TIMEOUT_SECONDS=900
|
||||
OPENAI_TTS_MODEL=gpt-4o-mini-tts
|
||||
OPENAI_TTS_VOICE=alloy
|
||||
OPENAI_TTS_RESPONSE_FORMAT=opus
|
||||
OPENAI_TTS_TIMEOUT_SECONDS=180
|
||||
OPENAI_TTS_CHUNK_CHARS=3500
|
||||
CODEX_BIN=/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl
|
||||
CODEX_WORKDIR=/home/ai/work/SHiNE/SHiNE-server-sha256
|
||||
CODEX_TIMEOUT_SECONDS=900
|
||||
|
||||
@ -9,41 +9,12 @@
|
||||
- История диалога хранится в JSONL-файле, путь передаётся в промпте.
|
||||
- Сообщение может быть текстом или результатом распознавания голосового.
|
||||
- Ответ пойдёт пользователю в Telegram как обычное текстовое сообщение.
|
||||
- Единственная рабочая реализация сервиса — Python-скрипт `py_bot_service.py`; старая Java-реализация удалена как нерабочая и не должна восстанавливаться без отдельного решения Айдара.
|
||||
- В репозитории также есть отдельный Solana/Anchor-модуль `shine-solana/shine/`; он логически связан с SHiNE, но не должен автоматически подключаться к основному серверному deploy без отдельной команды.
|
||||
- Перед изменениями внутри `shine-solana/shine/` читать локальные инструкции `shine-solana/shine/AGENTS.md`; в git не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
|
||||
|
||||
## Авторитет команд и история
|
||||
- Основной пользователь и источник команд — Айдар: `@AidarKC` / `@aidarkc`.
|
||||
- Дополнительно разрешены игроки из whitelist (`ALLOWED_TELEGRAM_PLAYERS`), каждый со своей отдельной историей и рабочей папкой `Players/<username>/`.
|
||||
- Игроки работают в режиме вопросов/анализа/подготовки материалов: в промпте явно задано правило не менять код проекта и писать материалы только в своей папке.
|
||||
- Для неизвестных пользователей в личном чате сервис отвечает вежливым отказом.
|
||||
- В Telegram-канале/группе `@shine_writing` сервис выполняет сообщения только от Айдара, а ответы отправляет в тот же чат.
|
||||
- Если Telegram сообщает о миграции обычной группы в supergroup, сервис должен запомнить новый `chat_id` и отправлять ответы уже туда.
|
||||
- На события подключения/отключения пользователей (join/leave) сервис не отвечает и ничего не отправляет.
|
||||
|
||||
## Очередь и состояние
|
||||
- Входящие задачи записываются в файловую очередь и обрабатываются строго по одной, чтобы не смешивать изменения в проекте.
|
||||
- Сервис ведёт состояние активной задачи и текущего файла истории, а после рестарта продолжает незавершённую обработку с учётом сохранённого состояния.
|
||||
- Истории диалогов хранятся в JSONL по каждому разрешённому username отдельно: `data/history/<username>/`.
|
||||
- Архив истории после `/new`: `data/history/<username>/archive/`.
|
||||
- Для просмотра истории игрока открывать файлы в его папке истории по username.
|
||||
- Истории диалогов хранятся в JSONL; после команды `/new` старая история архивируется, а новая начинается отдельно.
|
||||
- Дедупликация входящих Telegram update нужна, чтобы одно сообщение не попало в обработку повторно.
|
||||
- Если Codex молчит во время активной задачи 2 минуты подряд, сервис отправляет аварийный статус с общим временем работы задачи; при дальнейшем молчании повторяет статус каждые 2 минуты.
|
||||
- После успешной обработки задачи из личного чата Айдара сервис должен отправить публичный итоговый отчёт в группу `@shine_writing`: первым сообщением исходный запрос, вторым сообщением-ответом итоговый ответ Codex. Промежуточные статусы в группу не дублировать.
|
||||
- Для приватных voice/audio-запросов в публичном отчёте первым сообщением отправлять исходный Telegram voice/audio-файл с подписью, где указан распознанный текст. В пользовательском тексте отчёта не показывать Telegram `file_id`.
|
||||
- Озвучивание финальных ответов настраивается персонально для каждого Telegram-пользователя командами `/voice_on`, `/voice_off`, `/voice_status`.
|
||||
- Если озвучивание включено, после полного текстового финального ответа сервис дополнительно отправляет voice-файл с синтезированной речью через OpenAI TTS. Промежуточные статусы и публичный отчёт в `@shine_writing` не озвучивать.
|
||||
|
||||
## Планы и отложенные фичи
|
||||
- Планы проекта по отложенным фичам хранятся в `Dev_Docs/Future_Features/`.
|
||||
- Внутри есть три горизонта:
|
||||
- `near/` - ближайшие планы, обычно сегодня/завтра;
|
||||
- `medium/` - среднесрочные планы, обычно недели или 1-2 месяца;
|
||||
- `far/` - дальнее будущее без понятного срока.
|
||||
- Если пользователь спрашивает, какие есть планы или что можно продолжить, нужно смотреть эти три папки и отвечать кратким списком по горизонтам.
|
||||
- Файлы из `Dev_Docs/Future_Features/` не начинать реализовывать без явной команды пользователя.
|
||||
- После реализации фичи, требующей ручной проверки, нужно добавить отдельный файл в `Dev_Docs/Pending_Features/`.
|
||||
|
||||
## Локальный запуск и systemd
|
||||
- Основной запуск сервиса выполняется Python-скриптом `py_bot_service.py` из папки `SHiNE-agent-bot-coder/`.
|
||||
@ -51,7 +22,6 @@
|
||||
- Для проверки Codex без Telegram можно использовать self-test режим сервиса.
|
||||
- Для постоянного локального запуска используется user-level systemd service `shine-agent-bot-coder`; скрипты установки лежат в `SHiNE-agent-bot-coder/scripts/systemd/`.
|
||||
- Если меняется логика сервиса, после изменений нужно проверить запуск локально и при необходимости перезапустить user systemd service.
|
||||
- Команда Telegram `/restart_service` (и алиас `/restart`) доступна только Айдару.
|
||||
|
||||
## Правила ответа
|
||||
- Пиши содержательно и коротко.
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
# AGENTS
|
||||
|
||||
## Назначение
|
||||
- Это автоматически читаемые инструкции Codex для папки `SHiNE-agent-bot-coder/`.
|
||||
- `SHiNE-agent-bot-coder` — локальный Telegram-бот-сервис агента-кодера для работы с проектом SHiNE.
|
||||
- Если пользователь говорит «агент MD», «агент с MD» или похожим образом про файл инструкций Codex, считать, что имеется в виду `AGENTS.md`.
|
||||
|
||||
## Связанные инструкции
|
||||
- Подробные служебные правила Telegram-обработчика лежат в `AGENT.md`.
|
||||
- `AGENT.md` используется самим сервисом как файл инструкций, который передаётся в промпт обработчика входящих Telegram-сообщений.
|
||||
- При изменении логики сервиса сначала читать `AGENT.md`, затем код `py_bot_service.py`.
|
||||
|
||||
## Планы и задачи
|
||||
- Отложенные задачи проекта лежат в `../Dev_Docs/Future_Features/`.
|
||||
- Точка входа по планам: `../Dev_Docs/Future_Features/README.md`.
|
||||
- Горизонты планов:
|
||||
- `near/` - ближайшие планы;
|
||||
- `medium/` - среднесрочные планы;
|
||||
- `far/` - дальнее будущее.
|
||||
- Если пользователь спрашивает, какие есть планы или что можно продолжить, кратко перечислять задачи по этим горизонтам.
|
||||
- Не начинать реализацию задач из `Future_Features` без явной команды пользователя.
|
||||
|
||||
## Проверка после изменений
|
||||
- Если меняется логика Telegram-бота, проверить локальный запуск или self-test, когда это уместно.
|
||||
- Если меняется только документация или инструкции, достаточно проверить, что ссылки на документы актуальны.
|
||||
@ -1,2 +0,0 @@
|
||||
@AGENTS.md
|
||||
@AGENT.md
|
||||
@ -1,26 +0,0 @@
|
||||
# Промпты для режима игроков (на согласование)
|
||||
|
||||
## 1) Базовый служебный промпт (добавка к задаче игрока)
|
||||
|
||||
```text
|
||||
Режим игрока (обязательно):
|
||||
- Пользователь: <Имя> (@<username>).
|
||||
- Рабочая папка игрока: <project>/Players/<username>
|
||||
- Код проекта не изменять.
|
||||
- Можно отвечать на вопросы по проекту, предлагать идеи и готовить ТЗ.
|
||||
- Если нужны правки кода, описывать предложение текстом и сохранять материалы только в папке игрока.
|
||||
```
|
||||
|
||||
## 2) Приветственное сообщение игроку (один раз)
|
||||
|
||||
```text
|
||||
Привет, <Имя>.
|
||||
Можно задавать вопросы по проекту, просить анализ, идеи и подготовку готового ТЗ.
|
||||
Команда /new начинает новую сессию и архивирует текущую историю.
|
||||
```
|
||||
|
||||
## 3) Отказ неизвестному пользователю
|
||||
|
||||
```text
|
||||
Извините, доступ к этому агенту пока не выдан. Обратитесь к Айдару.
|
||||
```
|
||||
@ -2,43 +2,27 @@
|
||||
|
||||
Локальный Telegram-бот-сервис для пользователя `ai`:
|
||||
- принимает сообщения от `@AidarKC`;
|
||||
- поддерживает whitelist игроков (`ALLOWED_TELEGRAM_PLAYERS`) с отдельными историями;
|
||||
- ведёт историю диалога в `JSONL`;
|
||||
- ставит задачи в файловую очередь;
|
||||
- обрабатывает задачи строго последовательно;
|
||||
- поддерживает текстовые и голосовые сообщения (voice/audio через OpenAI transcription);
|
||||
- вызывает Codex CLI и отправляет ответ в Telegram;
|
||||
- умеет персонально для каждого пользователя озвучивать финальный ответ через OpenAI TTS;
|
||||
- при рестарте восстанавливает незавершённые задачи;
|
||||
- отправляет аварийный статус только если Codex молчит 2 минуты подряд во время активной задачи;
|
||||
- принимает сообщения из канала/группы `@shine_writing`, выполняет команды только от `@AidarKC`;
|
||||
- учитывает миграцию обычной Telegram-группы в supergroup и перенаправляет ответы на новый `chat_id`.
|
||||
|
||||
Рабочая реализация сервиса — только `py_bot_service.py`. Старая Java-реализация удалена, потому что не заработала и больше не используется.
|
||||
- при рестарте восстанавливает незавершённые задачи.
|
||||
|
||||
## Структура
|
||||
- `.env` — локальные секреты и параметры запуска (не коммитится);
|
||||
- `data/queue.jsonl` — очередь задач;
|
||||
- `data/state.json` — текущее состояние (active job + текущий history-файл);
|
||||
- `data/py_queue.jsonl` — очередь Python-сервиса;
|
||||
- `data/py_state.json` — текущее состояние Python-сервиса;
|
||||
- `data/py_processed_updates.log` — дедуп входящих update;
|
||||
- `data/history/<username>/*.jsonl` — активные истории по пользователям;
|
||||
- `data/history/<username>/archive/*.jsonl` — архивы после `/new`.
|
||||
- `data/history/*.jsonl` — активные истории;
|
||||
- `data/history/archive/*.jsonl` — архив историй после `/new`.
|
||||
|
||||
## Локальный запуск
|
||||
1. Скопировать пример:
|
||||
- `cp .env.example .env`
|
||||
2. Заполнить секреты в `.env`.
|
||||
- `TELEGRAM_BOT_TOKEN` — токен рабочего Telegram-бота.
|
||||
- `ALLOWED_TELEGRAM_USERNAME` — пользователь, чьи сообщения выполняются как команды.
|
||||
- `ALLOWED_TELEGRAM_PLAYERS` — whitelist игроков в формате `username:Имя,username2:Имя2`.
|
||||
- `ALLOWED_TELEGRAM_CHANNEL_USERNAME` — канал, из которого принимаются `channel_post`; обычные group/supergroup-сообщения обрабатываются как `message`.
|
||||
- `TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS` — тайм-аут скачивания voice/audio из Telegram, по умолчанию 300 секунд.
|
||||
- `OPENAI_TRANSCRIBE_TIMEOUT_SECONDS` — тайм-аут распознавания voice/audio в OpenAI, по умолчанию 900 секунд.
|
||||
- `OPENAI_TTS_MODEL` — модель синтеза речи, по умолчанию `gpt-4o-mini-tts`.
|
||||
- `OPENAI_TTS_VOICE` — голос синтеза речи, по умолчанию `alloy`.
|
||||
- `OPENAI_TTS_RESPONSE_FORMAT` — аудиоформат для Telegram voice, по умолчанию `opus`.
|
||||
- `OPENAI_TTS_TIMEOUT_SECONDS` — тайм-аут генерации одного фрагмента речи, по умолчанию 180 секунд.
|
||||
- `OPENAI_TTS_CHUNK_CHARS` — максимальный размер одного фрагмента озвучки, по умолчанию 3500 символов.
|
||||
3. Запуск:
|
||||
- `python3 SHiNE-agent-bot-coder/py_bot_service.py`
|
||||
|
||||
@ -58,17 +42,3 @@ python3 SHiNE-agent-bot-coder/py_bot_service.py --selftest-codex "Ответь
|
||||
Проверка:
|
||||
- `systemctl --user status shine-agent-bot-coder --no-pager`
|
||||
- `journalctl --user -u shine-agent-bot-coder -f`
|
||||
|
||||
Перезапуск после изменений:
|
||||
- `systemctl --user restart shine-agent-bot-coder`
|
||||
|
||||
## Telegram-команды
|
||||
- `/status` — активная задача и размер очереди.
|
||||
- `/queue` — список задач в очереди.
|
||||
- `/stop` — остановить текущую задачу.
|
||||
- `/cancel <id|all>` — удалить задачу по id/префиксу или очистить очередь.
|
||||
- `/new` — архивировать текущую историю и начать новый диалог.
|
||||
- `/voice_on` — включить озвучивание финальных ответов для текущего пользователя.
|
||||
- `/voice_off` — выключить озвучивание финальных ответов для текущего пользователя.
|
||||
- `/voice_status` — показать состояние озвучивания для текущего пользователя.
|
||||
- `/restart_service` — перезапустить сервис (только для Айдара); systemd должен поднять процесс заново.
|
||||
|
||||
50
SHiNE-agent-bot-coder/build.gradle
Normal file
50
SHiNE-agent-bot-coder/build.gradle
Normal file
@ -0,0 +1,50 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
}
|
||||
|
||||
group = 'shine.agent'
|
||||
version = '1.0.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.telegram:telegrambots:6.9.7.1'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.16'
|
||||
runtimeOnly 'org.slf4j:slf4j-simple:2.0.16'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'org.apache.httpcomponents:httpcore:4.4.16'
|
||||
implementation 'commons-codec:commons-codec:1.17.0'
|
||||
|
||||
testImplementation platform('org.junit:junit-bom:5.10.2')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'shine.agent.botcoder.BotCoderApplication'
|
||||
}
|
||||
|
||||
tasks.named('jar') {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
archiveBaseName.set('shine-agent-bot-coder')
|
||||
archiveClassifier.set('')
|
||||
archiveVersion.set('')
|
||||
mergeServiceFiles()
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,76 @@
|
||||
package shine.agent.botcoder;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.telegram.telegrambots.meta.TelegramBotsApi;
|
||||
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;
|
||||
import shine.agent.botcoder.codex.CodexClient;
|
||||
import shine.agent.botcoder.config.AppConfig;
|
||||
import shine.agent.botcoder.history.HistoryManager;
|
||||
import shine.agent.botcoder.openai.OpenAiTranscriber;
|
||||
import shine.agent.botcoder.queue.QueueStore;
|
||||
import shine.agent.botcoder.state.RuntimeStateStore;
|
||||
import shine.agent.botcoder.state.SingleInstanceLock;
|
||||
import shine.agent.botcoder.telegram.ProcessedUpdatesStore;
|
||||
import shine.agent.botcoder.telegram.ShineAgentBot;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public class BotCoderApplication {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BotCoderApplication.class);
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Path serviceRoot = Path.of("").toAbsolutePath().normalize();
|
||||
AppConfig config = AppConfig.load(serviceRoot);
|
||||
Files.createDirectories(config.dataDir());
|
||||
SingleInstanceLock appLock = SingleInstanceLock.tryAcquire(config.dataDir().resolve("app.lock"));
|
||||
if (appLock == null) {
|
||||
log.error("SHiNE-agent-bot-coder уже запущен: lock занят {}", config.dataDir().resolve("app.lock"));
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeStateStore stateStore = new RuntimeStateStore(config.dataDir().resolve("state.json"));
|
||||
QueueStore queueStore = new QueueStore(config.dataDir().resolve("queue.jsonl"), stateStore);
|
||||
HistoryManager historyManager = new HistoryManager(
|
||||
config.dataDir().resolve("history"),
|
||||
config.dataDir().resolve("history").resolve("archive"),
|
||||
stateStore
|
||||
);
|
||||
|
||||
List<String> recovered = queueStore.recoverActiveJobs();
|
||||
if (!recovered.isEmpty()) {
|
||||
historyManager.appendSystemEvent("active_jobs_recovered", java.util.Map.of("jobIds", recovered));
|
||||
}
|
||||
|
||||
OpenAiTranscriber transcriber = new OpenAiTranscriber(config.openAiApiKey(), config.openAiTranscribeModel());
|
||||
CodexClient codexClient = new CodexClient(config.codexBin(), config.codexWorkDir(), config.codexTimeoutSeconds());
|
||||
ProcessedUpdatesStore processedUpdatesStore = new ProcessedUpdatesStore(
|
||||
config.dataDir().resolve("processed_updates.log"),
|
||||
5000
|
||||
);
|
||||
|
||||
ShineAgentBot bot = new ShineAgentBot(config, queueStore, historyManager, transcriber, codexClient, processedUpdatesStore);
|
||||
bot.startWorkers();
|
||||
|
||||
TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class);
|
||||
botsApi.registerBot(bot);
|
||||
|
||||
log.info("SHiNE-agent-bot-coder запущен. allowed user: @{}", config.allowedTelegramUsername());
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
bot.shutdown();
|
||||
try {
|
||||
appLock.close();
|
||||
} catch (Exception e) {
|
||||
log.warn("Не удалось закрыть lock-файл", e);
|
||||
}
|
||||
latch.countDown();
|
||||
}, "shine-agent-bot-shutdown"));
|
||||
latch.await();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,225 @@
|
||||
package shine.agent.botcoder.codex;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CodexClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CodexClient.class);
|
||||
|
||||
private final Path codexBin;
|
||||
private final Path codexWorkDir;
|
||||
private final int timeoutSeconds;
|
||||
private final AtomicReference<Process> activeProcess = new AtomicReference<>();
|
||||
|
||||
public CodexClient(Path codexBin, Path codexWorkDir, int timeoutSeconds) {
|
||||
this.codexBin = codexBin;
|
||||
this.codexWorkDir = codexWorkDir;
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
}
|
||||
|
||||
public String executePrompt(String prompt, CodexStatusListener statusListener) throws IOException, InterruptedException {
|
||||
Path lastMessageFile = Files.createTempFile("shine-codex-last-message-", ".txt");
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(codexBin.toString());
|
||||
command.add("exec");
|
||||
command.add("--dangerously-bypass-approvals-and-sandbox");
|
||||
command.add("--json");
|
||||
command.add("-C");
|
||||
command.add(codexWorkDir.toString());
|
||||
command.add("-o");
|
||||
command.add(lastMessageFile.toString());
|
||||
command.add(prompt);
|
||||
log.info("Запуск codex exec, bin={}, workdir={}", codexBin, codexWorkDir);
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(command);
|
||||
builder.redirectErrorStream(true);
|
||||
Process process = builder.start();
|
||||
activeProcess.set(process);
|
||||
if (statusListener != null) {
|
||||
statusListener.onStatus("Codex запущен");
|
||||
}
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
Thread outputThread = new Thread(() -> readOutput(process, output, statusListener));
|
||||
outputThread.setDaemon(true);
|
||||
outputThread.start();
|
||||
|
||||
boolean finished;
|
||||
try {
|
||||
finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException interrupted) {
|
||||
process.destroyForcibly();
|
||||
joinOutputThread(outputThread);
|
||||
activeProcess.compareAndSet(process, null);
|
||||
Thread.currentThread().interrupt();
|
||||
throw interrupted;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!finished) {
|
||||
process.destroyForcibly();
|
||||
joinOutputThread(outputThread);
|
||||
log.error("Codex timeout after {}s", timeoutSeconds);
|
||||
throw new IOException("Codex timeout after " + timeoutSeconds + "s");
|
||||
}
|
||||
|
||||
joinOutputThread(outputThread);
|
||||
int exitCode = process.exitValue();
|
||||
String lastMessage = "";
|
||||
if (Files.exists(lastMessageFile)) {
|
||||
lastMessage = Files.readString(lastMessageFile, StandardCharsets.UTF_8).trim();
|
||||
}
|
||||
|
||||
if (exitCode != 0) {
|
||||
log.error("Codex exit code={}, outputTail={}", exitCode, tail(output.toString(), 500));
|
||||
throw new IOException("Codex exited with code " + exitCode + ". Output: " + tail(output.toString(), 1800));
|
||||
}
|
||||
|
||||
if (!lastMessage.isBlank()) {
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
String fallback = extractFallbackMessage(output.toString());
|
||||
if (fallback.isBlank()) {
|
||||
throw new IOException("Codex returned empty response");
|
||||
}
|
||||
return fallback;
|
||||
} finally {
|
||||
activeProcess.compareAndSet(process, null);
|
||||
try {
|
||||
Files.deleteIfExists(lastMessageFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean stopActiveProcess() {
|
||||
Process process = activeProcess.getAndSet(null);
|
||||
if (process == null) {
|
||||
return false;
|
||||
}
|
||||
process.destroy();
|
||||
try {
|
||||
if (!process.waitFor(2, TimeUnit.SECONDS)) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
process.destroyForcibly();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void readOutput(Process process, StringBuilder output, CodexStatusListener statusListener) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.append(line).append('\n');
|
||||
String status = normalizeStatusLine(line);
|
||||
if (status != null && statusListener != null) {
|
||||
statusListener.onStatus(status);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeStatusLine(String line) {
|
||||
String trimmed = line == null ? "" : line.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"thread.started\"")) {
|
||||
return "Codex: инициализировал сессию";
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"turn.started\"")) {
|
||||
return "Codex: начал обработку запроса";
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"item.completed\"") && trimmed.contains("\"type\":\"agent_message\"")) {
|
||||
return "Codex: формирует финальный ответ";
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"turn.completed\"")) {
|
||||
return "Codex: завершил шаг";
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"error\"")) {
|
||||
return "Codex: ошибка выполнения";
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"reasoning\"")) {
|
||||
return "Codex: анализирует задачу";
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"function_call\"")) {
|
||||
return "Codex: вызывает инструмент";
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"function_call_output\"")) {
|
||||
return "Codex: получил результат инструмента";
|
||||
}
|
||||
if (trimmed.contains("\"type\":\"message\"") && trimmed.contains("\"role\":\"assistant\"")) {
|
||||
return "Codex: формирует ответ";
|
||||
}
|
||||
if (trimmed.startsWith("mcp")) {
|
||||
return "Codex: инициализирует MCP";
|
||||
}
|
||||
if (trimmed.startsWith("tokens used")) {
|
||||
return "Codex: завершает обработку";
|
||||
}
|
||||
if (trimmed.startsWith("ERROR:")) {
|
||||
return "Codex: ошибка выполнения";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void joinOutputThread(Thread outputThread) throws InterruptedException {
|
||||
try {
|
||||
outputThread.join(Duration.ofSeconds(2).toMillis());
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw interrupted;
|
||||
}
|
||||
}
|
||||
|
||||
private String extractFallbackMessage(String rawOutput) {
|
||||
String[] lines = rawOutput.split("\\R");
|
||||
for (int i = lines.length - 1; i >= 0; i--) {
|
||||
String line = lines[i].trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("tokens used")) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("OpenAI Codex")) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("workdir:") || line.startsWith("model:") || line.startsWith("provider:")) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("approval:") || line.startsWith("sandbox:") || line.startsWith("reasoning")) {
|
||||
continue;
|
||||
}
|
||||
if (line.equals("user") || line.equals("exec") || line.equals("--------")) {
|
||||
continue;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String tail(String value, int maxLen) {
|
||||
if (value.length() <= maxLen) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(value.length() - maxLen);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package shine.agent.botcoder.codex;
|
||||
|
||||
public interface CodexStatusListener {
|
||||
void onStatus(String message);
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
package shine.agent.botcoder.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
public record AppConfig(
|
||||
String telegramBotToken,
|
||||
String botUsername,
|
||||
String allowedTelegramUsername,
|
||||
String openAiApiKey,
|
||||
String openAiTranscribeModel,
|
||||
Path codexBin,
|
||||
Path codexWorkDir,
|
||||
int codexTimeoutSeconds,
|
||||
int maxRetries,
|
||||
Path dataDir,
|
||||
Path agentInstructionsFile
|
||||
) {
|
||||
|
||||
public static AppConfig load(Path serviceRoot) throws IOException {
|
||||
Map<String, String> env = EnvLoader.load(serviceRoot.resolve(".env"));
|
||||
String telegramBotToken = required(env, "TELEGRAM_BOT_TOKEN");
|
||||
String openAiApiKey = required(env, "OPENAI_API_KEY");
|
||||
|
||||
String botUsername = env.getOrDefault("BOT_USERNAME", "aidar_su_bot");
|
||||
String allowed = normalizeUsername(env.getOrDefault("ALLOWED_TELEGRAM_USERNAME", "AidarKC"));
|
||||
String transcribeModel = env.getOrDefault("OPENAI_TRANSCRIBE_MODEL", "gpt-4o-mini-transcribe");
|
||||
|
||||
Path codexBin = Path.of(env.getOrDefault(
|
||||
"CODEX_BIN",
|
||||
"/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl"
|
||||
));
|
||||
Path codexWorkDir = Path.of(env.getOrDefault(
|
||||
"CODEX_WORKDIR",
|
||||
"/home/ai/work/SHiNE/SHiNE-server-sha256"
|
||||
));
|
||||
|
||||
int timeout = parseInt(env.getOrDefault("CODEX_TIMEOUT_SECONDS", "900"), 900);
|
||||
int retries = parseInt(env.getOrDefault("MAX_RETRIES", "3"), 3);
|
||||
if (retries < 1) {
|
||||
retries = 1;
|
||||
}
|
||||
Path dataDir = serviceRoot.resolve(env.getOrDefault("DATA_DIR", "./data")).normalize();
|
||||
Path agentInstructions = serviceRoot.resolve("AGENT.md").normalize();
|
||||
|
||||
return new AppConfig(
|
||||
telegramBotToken,
|
||||
botUsername,
|
||||
allowed,
|
||||
openAiApiKey,
|
||||
transcribeModel,
|
||||
codexBin,
|
||||
codexWorkDir,
|
||||
timeout,
|
||||
retries,
|
||||
dataDir,
|
||||
agentInstructions
|
||||
);
|
||||
}
|
||||
|
||||
public static String normalizeUsername(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if (normalized.startsWith("@")) {
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
private static String required(Map<String, String> env, String key) {
|
||||
String value = env.get(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Не задан обязательный параметр: " + key);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static int parseInt(String value, int fallback) {
|
||||
try {
|
||||
return Integer.parseInt(value.trim());
|
||||
} catch (Exception ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package shine.agent.botcoder.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class EnvLoader {
|
||||
|
||||
private EnvLoader() {
|
||||
}
|
||||
|
||||
public static Map<String, String> load(Path envFile) throws IOException {
|
||||
Map<String, String> values = new HashMap<>();
|
||||
if (Files.exists(envFile)) {
|
||||
for (String rawLine : Files.readAllLines(envFile)) {
|
||||
String line = rawLine.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
int idx = line.indexOf('=');
|
||||
if (idx <= 0) {
|
||||
continue;
|
||||
}
|
||||
String key = line.substring(0, idx).trim();
|
||||
String value = line.substring(idx + 1).trim();
|
||||
values.put(key, stripQuotes(value));
|
||||
}
|
||||
}
|
||||
System.getenv().forEach(values::put);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static String stripQuotes(String value) {
|
||||
if (value.length() >= 2) {
|
||||
char first = value.charAt(0);
|
||||
char last = value.charAt(value.length() - 1);
|
||||
if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) {
|
||||
return value.substring(1, value.length() - 1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
package shine.agent.botcoder.history;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import shine.agent.botcoder.state.RuntimeState;
|
||||
import shine.agent.botcoder.state.RuntimeStateStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class HistoryManager {
|
||||
|
||||
private static final DateTimeFormatter FILE_TS = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss");
|
||||
private final Path historyDir;
|
||||
private final Path archiveDir;
|
||||
private final RuntimeStateStore stateStore;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public HistoryManager(Path historyDir, Path archiveDir, RuntimeStateStore stateStore) throws IOException {
|
||||
this.historyDir = historyDir;
|
||||
this.archiveDir = archiveDir;
|
||||
this.stateStore = stateStore;
|
||||
this.mapper = new ObjectMapper();
|
||||
Files.createDirectories(historyDir);
|
||||
Files.createDirectories(archiveDir);
|
||||
ensureCurrentFile();
|
||||
}
|
||||
|
||||
public synchronized Path currentHistoryFile() throws IOException {
|
||||
return ensureCurrentFile();
|
||||
}
|
||||
|
||||
public synchronized Path rotateHistory(String reason, String requestedBy) throws IOException {
|
||||
Path current = ensureCurrentFile();
|
||||
Path archived = archiveDir.resolve(current.getFileName().toString());
|
||||
Files.move(current, archived, StandardCopyOption.REPLACE_EXISTING);
|
||||
Path next = newHistoryFile();
|
||||
appendSystemEvent(
|
||||
"history_rotated",
|
||||
Map.of(
|
||||
"reason", reason,
|
||||
"requestedBy", requestedBy,
|
||||
"archivedFile", archived.toString()
|
||||
)
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
public synchronized void appendIncomingText(long chatId, int messageId, String username, String text) throws IOException {
|
||||
Map<String, Object> payload = basePayload("incoming_text");
|
||||
payload.put("chatId", chatId);
|
||||
payload.put("messageId", messageId);
|
||||
payload.put("username", username);
|
||||
payload.put("text", text);
|
||||
append(payload);
|
||||
}
|
||||
|
||||
public synchronized void appendIncomingVoice(long chatId, int messageId, String username, String fileId) throws IOException {
|
||||
Map<String, Object> payload = basePayload("incoming_voice");
|
||||
payload.put("chatId", chatId);
|
||||
payload.put("messageId", messageId);
|
||||
payload.put("username", username);
|
||||
payload.put("telegramFileId", fileId);
|
||||
append(payload);
|
||||
}
|
||||
|
||||
public synchronized void appendTranscription(String jobId, String text) throws IOException {
|
||||
Map<String, Object> payload = basePayload("voice_transcription");
|
||||
payload.put("jobId", jobId);
|
||||
payload.put("text", text);
|
||||
append(payload);
|
||||
}
|
||||
|
||||
public synchronized void appendCodexRequest(String jobId, String prompt) throws IOException {
|
||||
Map<String, Object> payload = basePayload("codex_request");
|
||||
payload.put("jobId", jobId);
|
||||
payload.put("prompt", prompt);
|
||||
append(payload);
|
||||
}
|
||||
|
||||
public synchronized void appendCodexResponse(String jobId, String response) throws IOException {
|
||||
Map<String, Object> payload = basePayload("codex_response");
|
||||
payload.put("jobId", jobId);
|
||||
payload.put("response", response);
|
||||
append(payload);
|
||||
}
|
||||
|
||||
public synchronized void appendOutgoingMessage(String jobId, long chatId, String text) throws IOException {
|
||||
Map<String, Object> payload = basePayload("outgoing_message");
|
||||
payload.put("jobId", jobId);
|
||||
payload.put("chatId", chatId);
|
||||
payload.put("text", text);
|
||||
append(payload);
|
||||
}
|
||||
|
||||
public synchronized void appendJobError(String jobId, String error, boolean willRetry, int attempts, int maxRetries) throws IOException {
|
||||
Map<String, Object> payload = basePayload("job_error");
|
||||
payload.put("jobId", jobId);
|
||||
payload.put("error", error);
|
||||
payload.put("willRetry", willRetry);
|
||||
payload.put("attempts", attempts);
|
||||
payload.put("maxRetries", maxRetries);
|
||||
append(payload);
|
||||
}
|
||||
|
||||
public synchronized void appendSystemEvent(String event, Map<String, Object> fields) throws IOException {
|
||||
Map<String, Object> payload = basePayload(event);
|
||||
payload.putAll(fields);
|
||||
append(payload);
|
||||
}
|
||||
|
||||
private Map<String, Object> basePayload(String type) {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("timestamp", Instant.now().toString());
|
||||
payload.put("type", type);
|
||||
return payload;
|
||||
}
|
||||
|
||||
private void append(Map<String, Object> payload) throws IOException {
|
||||
Path current = ensureCurrentFile();
|
||||
Files.writeString(
|
||||
current,
|
||||
mapper.writeValueAsString(payload) + System.lineSeparator(),
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND
|
||||
);
|
||||
}
|
||||
|
||||
private Path ensureCurrentFile() throws IOException {
|
||||
RuntimeState snapshot = stateStore.snapshot();
|
||||
if (snapshot.currentHistoryFile != null && !snapshot.currentHistoryFile.isBlank()) {
|
||||
Path configured = Path.of(snapshot.currentHistoryFile);
|
||||
if (!configured.isAbsolute()) {
|
||||
configured = historyDir.resolve(snapshot.currentHistoryFile).normalize();
|
||||
}
|
||||
Files.createDirectories(configured.getParent());
|
||||
if (!Files.exists(configured)) {
|
||||
Files.createFile(configured);
|
||||
}
|
||||
return configured;
|
||||
}
|
||||
return newHistoryFile();
|
||||
}
|
||||
|
||||
private Path newHistoryFile() throws IOException {
|
||||
String name = FILE_TS.format(LocalDateTime.now()) + "_" + UUID.randomUUID().toString().substring(0, 8) + ".jsonl";
|
||||
Path file = historyDir.resolve(name);
|
||||
Files.createFile(file);
|
||||
stateStore.setCurrentHistoryFile(file.toString());
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package shine.agent.botcoder.openai;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
|
||||
public class OpenAiTranscriber {
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final ObjectMapper mapper;
|
||||
private final String apiKey;
|
||||
private final String model;
|
||||
|
||||
public OpenAiTranscriber(String apiKey, String model) {
|
||||
this.apiKey = apiKey;
|
||||
this.model = model;
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
this.mapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
public String transcribe(byte[] audioBytes, String fileName) throws IOException, InterruptedException {
|
||||
String boundary = "----shine-boundary-" + UUID.randomUUID();
|
||||
byte[] body = buildMultipartBody(boundary, audioBytes, fileName);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("https://api.openai.com/v1/audio/transcriptions"))
|
||||
.timeout(Duration.ofSeconds(120))
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() >= 300) {
|
||||
throw new IOException("OpenAI transcription error HTTP " + response.statusCode() + ": " + response.body());
|
||||
}
|
||||
|
||||
JsonNode root = mapper.readTree(response.body());
|
||||
JsonNode text = root.get("text");
|
||||
if (text == null || text.asText().isBlank()) {
|
||||
throw new IOException("OpenAI transcription returned empty text");
|
||||
}
|
||||
return text.asText().trim();
|
||||
}
|
||||
|
||||
private byte[] buildMultipartBody(String boundary, byte[] audioBytes, String fileName) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
String lineEnd = "\r\n";
|
||||
String prefix = "--" + boundary + lineEnd;
|
||||
|
||||
out.write(prefix.getBytes(StandardCharsets.UTF_8));
|
||||
out.write(("Content-Disposition: form-data; name=\"model\"" + lineEnd + lineEnd).getBytes(StandardCharsets.UTF_8));
|
||||
out.write(model.getBytes(StandardCharsets.UTF_8));
|
||||
out.write(lineEnd.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
out.write(prefix.getBytes(StandardCharsets.UTF_8));
|
||||
out.write(("Content-Disposition: form-data; name=\"language\"" + lineEnd + lineEnd).getBytes(StandardCharsets.UTF_8));
|
||||
out.write("ru".getBytes(StandardCharsets.UTF_8));
|
||||
out.write(lineEnd.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
out.write(prefix.getBytes(StandardCharsets.UTF_8));
|
||||
out.write(("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"" + lineEnd).getBytes(StandardCharsets.UTF_8));
|
||||
out.write(("Content-Type: audio/ogg" + lineEnd + lineEnd).getBytes(StandardCharsets.UTF_8));
|
||||
out.write(audioBytes);
|
||||
out.write(lineEnd.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
out.write(("--" + boundary + "--" + lineEnd).getBytes(StandardCharsets.UTF_8));
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package shine.agent.botcoder.queue;
|
||||
|
||||
public record FailureResult(boolean willRetry, int attempts, int maxRetries) {
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package shine.agent.botcoder.queue;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class QueueJob {
|
||||
public String id;
|
||||
public QueueStatus status;
|
||||
public String type;
|
||||
public long chatId;
|
||||
public int messageId;
|
||||
public String username;
|
||||
public String text;
|
||||
public String telegramFileId;
|
||||
public String historyFile;
|
||||
public String createdAt;
|
||||
public String updatedAt;
|
||||
public String activeSince;
|
||||
public int attempts;
|
||||
public String retryReason;
|
||||
public String lastError;
|
||||
|
||||
public static QueueJob textJob(long chatId, int messageId, String username, String text, String historyFile) {
|
||||
QueueJob job = baseJob(chatId, messageId, username, historyFile);
|
||||
job.type = "text";
|
||||
job.text = text;
|
||||
return job;
|
||||
}
|
||||
|
||||
public static QueueJob voiceJob(long chatId, int messageId, String username, String fileId, String historyFile) {
|
||||
QueueJob job = baseJob(chatId, messageId, username, historyFile);
|
||||
job.type = "voice";
|
||||
job.telegramFileId = fileId;
|
||||
return job;
|
||||
}
|
||||
|
||||
private static QueueJob baseJob(long chatId, int messageId, String username, String historyFile) {
|
||||
QueueJob job = new QueueJob();
|
||||
String now = Instant.now().toString();
|
||||
job.id = UUID.randomUUID().toString();
|
||||
job.status = QueueStatus.PENDING;
|
||||
job.chatId = chatId;
|
||||
job.messageId = messageId;
|
||||
job.username = username;
|
||||
job.historyFile = historyFile;
|
||||
job.createdAt = now;
|
||||
job.updatedAt = now;
|
||||
job.attempts = 0;
|
||||
return job;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package shine.agent.botcoder.queue;
|
||||
|
||||
public enum QueueStatus {
|
||||
PENDING,
|
||||
ACTIVE
|
||||
}
|
||||
@ -0,0 +1,203 @@
|
||||
package shine.agent.botcoder.queue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import shine.agent.botcoder.state.RuntimeStateStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class QueueStore {
|
||||
|
||||
private final Path queueFile;
|
||||
private final RuntimeStateStore stateStore;
|
||||
private final ObjectMapper mapper;
|
||||
private final List<QueueJob> jobs;
|
||||
|
||||
public QueueStore(Path queueFile, RuntimeStateStore stateStore) throws IOException {
|
||||
this.queueFile = queueFile;
|
||||
this.stateStore = stateStore;
|
||||
this.mapper = new ObjectMapper();
|
||||
Path parent = queueFile.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
this.jobs = loadQueue();
|
||||
persistQueue();
|
||||
}
|
||||
|
||||
public synchronized void enqueue(QueueJob job) throws IOException {
|
||||
jobs.add(job);
|
||||
persistQueue();
|
||||
}
|
||||
|
||||
public synchronized List<String> recoverActiveJobs() throws IOException {
|
||||
List<String> recovered = new ArrayList<>();
|
||||
String now = Instant.now().toString();
|
||||
for (QueueJob job : jobs) {
|
||||
if (job.status == QueueStatus.ACTIVE) {
|
||||
job.status = QueueStatus.PENDING;
|
||||
job.retryReason = "service_restart_recovery";
|
||||
job.updatedAt = now;
|
||||
recovered.add(job.id);
|
||||
}
|
||||
}
|
||||
stateStore.setActiveJobId(null);
|
||||
persistQueue();
|
||||
return recovered;
|
||||
}
|
||||
|
||||
public synchronized Optional<QueueJob> activateNext() throws IOException {
|
||||
for (QueueJob job : jobs) {
|
||||
if (job.status == QueueStatus.PENDING) {
|
||||
job.status = QueueStatus.ACTIVE;
|
||||
job.activeSince = Instant.now().toString();
|
||||
job.updatedAt = job.activeSince;
|
||||
stateStore.setActiveJobId(job.id);
|
||||
persistQueue();
|
||||
return Optional.of(job);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public synchronized void markDone(String jobId) throws IOException {
|
||||
Iterator<QueueJob> iterator = jobs.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
QueueJob job = iterator.next();
|
||||
if (job.id.equals(jobId)) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
stateStore.setActiveJobId(null);
|
||||
persistQueue();
|
||||
}
|
||||
|
||||
public synchronized Optional<QueueJob> getActiveJob() {
|
||||
return jobs.stream().filter(j -> j.status == QueueStatus.ACTIVE).findFirst();
|
||||
}
|
||||
|
||||
public synchronized int pendingCount() {
|
||||
int count = 0;
|
||||
for (QueueJob job : jobs) {
|
||||
if (job.status == QueueStatus.PENDING) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public synchronized int totalCount() {
|
||||
return jobs.size();
|
||||
}
|
||||
|
||||
public synchronized List<QueueJob> snapshot() {
|
||||
return new ArrayList<>(jobs);
|
||||
}
|
||||
|
||||
public synchronized boolean cancelActiveJob(String reason) throws IOException {
|
||||
for (Iterator<QueueJob> iterator = jobs.iterator(); iterator.hasNext(); ) {
|
||||
QueueJob job = iterator.next();
|
||||
if (job.status == QueueStatus.ACTIVE) {
|
||||
iterator.remove();
|
||||
stateStore.setActiveJobId(null);
|
||||
persistQueue();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public synchronized int cancelAll(String reason) throws IOException {
|
||||
int size = jobs.size();
|
||||
if (size == 0) {
|
||||
return 0;
|
||||
}
|
||||
jobs.clear();
|
||||
stateStore.setActiveJobId(null);
|
||||
persistQueue();
|
||||
return size;
|
||||
}
|
||||
|
||||
public synchronized boolean cancelByIdPrefix(String idPrefix) throws IOException {
|
||||
if (idPrefix == null || idPrefix.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = idPrefix.trim().toLowerCase();
|
||||
for (Iterator<QueueJob> iterator = jobs.iterator(); iterator.hasNext(); ) {
|
||||
QueueJob job = iterator.next();
|
||||
if (job.id != null && job.id.toLowerCase().startsWith(normalized)) {
|
||||
if (job.status == QueueStatus.ACTIVE) {
|
||||
stateStore.setActiveJobId(null);
|
||||
}
|
||||
iterator.remove();
|
||||
persistQueue();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public synchronized FailureResult markFailed(String jobId, String error, int maxRetries) throws IOException {
|
||||
for (Iterator<QueueJob> it = jobs.iterator(); it.hasNext(); ) {
|
||||
QueueJob job = it.next();
|
||||
if (!job.id.equals(jobId)) {
|
||||
continue;
|
||||
}
|
||||
job.attempts = job.attempts + 1;
|
||||
job.lastError = error;
|
||||
job.updatedAt = Instant.now().toString();
|
||||
stateStore.setActiveJobId(null);
|
||||
|
||||
if (job.attempts < maxRetries) {
|
||||
job.status = QueueStatus.PENDING;
|
||||
job.retryReason = error;
|
||||
persistQueue();
|
||||
return new FailureResult(true, job.attempts, maxRetries);
|
||||
}
|
||||
|
||||
it.remove();
|
||||
persistQueue();
|
||||
return new FailureResult(false, job.attempts, maxRetries);
|
||||
}
|
||||
|
||||
stateStore.setActiveJobId(null);
|
||||
persistQueue();
|
||||
return new FailureResult(false, maxRetries, maxRetries);
|
||||
}
|
||||
|
||||
private List<QueueJob> loadQueue() throws IOException {
|
||||
List<QueueJob> loaded = new ArrayList<>();
|
||||
if (!Files.exists(queueFile)) {
|
||||
return loaded;
|
||||
}
|
||||
for (String line : Files.readAllLines(queueFile, StandardCharsets.UTF_8)) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
loaded.add(mapper.readValue(trimmed, QueueJob.class));
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private void persistQueue() throws IOException {
|
||||
Files.writeString(queueFile, "", StandardCharsets.UTF_8);
|
||||
for (QueueJob job : jobs) {
|
||||
Files.writeString(
|
||||
queueFile,
|
||||
mapper.writeValueAsString(job) + System.lineSeparator(),
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.APPEND
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package shine.agent.botcoder.state;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class RuntimeState {
|
||||
public String activeJobId;
|
||||
public String currentHistoryFile;
|
||||
public String updatedAt;
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package shine.agent.botcoder.state;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
|
||||
public class RuntimeStateStore {
|
||||
|
||||
private final Path stateFile;
|
||||
private final ObjectMapper mapper;
|
||||
private RuntimeState state;
|
||||
|
||||
public RuntimeStateStore(Path stateFile) throws IOException {
|
||||
this.stateFile = stateFile;
|
||||
this.mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||
Path parent = stateFile.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
this.state = loadOrCreate();
|
||||
persist();
|
||||
}
|
||||
|
||||
public synchronized RuntimeState snapshot() {
|
||||
RuntimeState copy = new RuntimeState();
|
||||
copy.activeJobId = state.activeJobId;
|
||||
copy.currentHistoryFile = state.currentHistoryFile;
|
||||
copy.updatedAt = state.updatedAt;
|
||||
return copy;
|
||||
}
|
||||
|
||||
public synchronized void setActiveJobId(String activeJobId) throws IOException {
|
||||
state.activeJobId = activeJobId;
|
||||
state.updatedAt = Instant.now().toString();
|
||||
persist();
|
||||
}
|
||||
|
||||
public synchronized void setCurrentHistoryFile(String historyFile) throws IOException {
|
||||
state.currentHistoryFile = historyFile;
|
||||
state.updatedAt = Instant.now().toString();
|
||||
persist();
|
||||
}
|
||||
|
||||
private RuntimeState loadOrCreate() throws IOException {
|
||||
if (!Files.exists(stateFile)) {
|
||||
RuntimeState created = new RuntimeState();
|
||||
created.updatedAt = Instant.now().toString();
|
||||
return created;
|
||||
}
|
||||
String raw = Files.readString(stateFile, StandardCharsets.UTF_8).trim();
|
||||
if (raw.isEmpty()) {
|
||||
RuntimeState created = new RuntimeState();
|
||||
created.updatedAt = Instant.now().toString();
|
||||
return created;
|
||||
}
|
||||
RuntimeState loaded = mapper.readValue(raw, RuntimeState.class);
|
||||
if (loaded.updatedAt == null) {
|
||||
loaded.updatedAt = Instant.now().toString();
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private void persist() throws IOException {
|
||||
Files.writeString(
|
||||
stateFile,
|
||||
mapper.writeValueAsString(state),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package shine.agent.botcoder.state;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
public final class SingleInstanceLock implements Closeable {
|
||||
|
||||
private final FileChannel channel;
|
||||
private final FileLock lock;
|
||||
private final Path path;
|
||||
|
||||
private SingleInstanceLock(FileChannel channel, FileLock lock, Path path) {
|
||||
this.channel = channel;
|
||||
this.lock = lock;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public static SingleInstanceLock tryAcquire(Path lockFile) throws IOException {
|
||||
FileChannel channel = FileChannel.open(
|
||||
lockFile,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE
|
||||
);
|
||||
FileLock lock = channel.tryLock();
|
||||
if (lock == null) {
|
||||
channel.close();
|
||||
return null;
|
||||
}
|
||||
return new SingleInstanceLock(channel, lock, lockFile);
|
||||
}
|
||||
|
||||
public Path path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
if (lock != null && lock.isValid()) {
|
||||
lock.release();
|
||||
}
|
||||
} finally {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package shine.agent.botcoder.telegram;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class ProcessedUpdatesStore {
|
||||
|
||||
private final Path file;
|
||||
private final LinkedHashSet<String> ids = new LinkedHashSet<>();
|
||||
private final int maxEntries;
|
||||
|
||||
public ProcessedUpdatesStore(Path file, int maxEntries) throws IOException {
|
||||
this.file = file;
|
||||
this.maxEntries = Math.max(100, maxEntries);
|
||||
Path parent = file.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
if (Files.exists(file)) {
|
||||
List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
|
||||
for (String line : lines) {
|
||||
String id = line.trim();
|
||||
if (!id.isEmpty()) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
trimIfNeeded();
|
||||
persistAll();
|
||||
}
|
||||
|
||||
public synchronized boolean isDuplicateAndMark(String id) throws IOException {
|
||||
if (id == null || id.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = id.trim();
|
||||
if (ids.contains(normalized)) {
|
||||
return true;
|
||||
}
|
||||
ids.add(normalized);
|
||||
trimIfNeeded();
|
||||
Files.writeString(file, normalized + System.lineSeparator(), StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void trimIfNeeded() throws IOException {
|
||||
if (ids.size() <= maxEntries) {
|
||||
return;
|
||||
}
|
||||
int toRemove = ids.size() - maxEntries;
|
||||
int removed = 0;
|
||||
Iterator<String> it = ids.iterator();
|
||||
while (it.hasNext() && removed < toRemove) {
|
||||
it.next();
|
||||
it.remove();
|
||||
removed++;
|
||||
}
|
||||
persistAll();
|
||||
}
|
||||
|
||||
private void persistAll() throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String id : ids) {
|
||||
sb.append(id).append(System.lineSeparator());
|
||||
}
|
||||
Files.writeString(file, sb.toString(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,589 @@
|
||||
package shine.agent.botcoder.telegram;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
|
||||
import org.telegram.telegrambots.meta.api.methods.GetFile;
|
||||
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
|
||||
import org.telegram.telegrambots.meta.api.objects.Message;
|
||||
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||
import org.telegram.telegrambots.meta.api.objects.User;
|
||||
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
|
||||
import shine.agent.botcoder.codex.CodexStatusListener;
|
||||
import shine.agent.botcoder.codex.CodexClient;
|
||||
import shine.agent.botcoder.config.AppConfig;
|
||||
import shine.agent.botcoder.history.HistoryManager;
|
||||
import shine.agent.botcoder.openai.OpenAiTranscriber;
|
||||
import shine.agent.botcoder.queue.FailureResult;
|
||||
import shine.agent.botcoder.queue.QueueJob;
|
||||
import shine.agent.botcoder.queue.QueueStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class ShineAgentBot extends TelegramLongPollingBot {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ShineAgentBot.class);
|
||||
|
||||
private final AppConfig config;
|
||||
private final QueueStore queueStore;
|
||||
private final HistoryManager historyManager;
|
||||
private final OpenAiTranscriber transcriber;
|
||||
private final CodexClient codexClient;
|
||||
private final ExecutorService worker;
|
||||
private final ExecutorService notifier;
|
||||
private final AtomicBoolean running;
|
||||
private final HttpClient httpClient;
|
||||
private final ProcessedUpdatesStore processedUpdatesStore;
|
||||
private final AtomicReference<QueueJob> activeJobRef = new AtomicReference<>();
|
||||
private final AtomicLong activeJobStartedAt = new AtomicLong(0L);
|
||||
private final ScheduledExecutorService heartbeatScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "shine-agent-heartbeat");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
public ShineAgentBot(
|
||||
AppConfig config,
|
||||
QueueStore queueStore,
|
||||
HistoryManager historyManager,
|
||||
OpenAiTranscriber transcriber,
|
||||
CodexClient codexClient,
|
||||
ProcessedUpdatesStore processedUpdatesStore
|
||||
) {
|
||||
this.config = config;
|
||||
this.queueStore = queueStore;
|
||||
this.historyManager = historyManager;
|
||||
this.transcriber = transcriber;
|
||||
this.codexClient = codexClient;
|
||||
this.processedUpdatesStore = processedUpdatesStore;
|
||||
this.worker = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread thread = new Thread(r, "shine-agent-bot-worker");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
this.notifier = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread thread = new Thread(r, "shine-agent-bot-notifier");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
this.running = new AtomicBoolean(true);
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void startWorkers() {
|
||||
worker.submit(this::processLoop);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
running.set(false);
|
||||
codexClient.stopActiveProcess();
|
||||
worker.shutdown();
|
||||
notifier.shutdown();
|
||||
heartbeatScheduler.shutdownNow();
|
||||
try {
|
||||
if (!worker.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
worker.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
worker.shutdownNow();
|
||||
}
|
||||
try {
|
||||
if (!notifier.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
notifier.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
notifier.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBotUsername() {
|
||||
return config.botUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBotToken() {
|
||||
return config.telegramBotToken();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdateReceived(Update update) {
|
||||
if (update == null || !update.hasMessage()) {
|
||||
return;
|
||||
}
|
||||
Message message = update.getMessage();
|
||||
try {
|
||||
String updateId = message.getChatId() + ":" + message.getMessageId();
|
||||
if (processedUpdatesStore.isDuplicateAndMark(updateId)) {
|
||||
log.info("Дубликат update пропущен: {}", updateId);
|
||||
return;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Не удалось проверить дубликат update", e);
|
||||
}
|
||||
|
||||
User from = message.getFrom();
|
||||
if (from == null) {
|
||||
return;
|
||||
}
|
||||
String username = AppConfig.normalizeUsername(from.getUserName());
|
||||
if (!username.equals(config.allowedTelegramUsername())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (message.hasText() && "/new".equalsIgnoreCase(message.getText().trim())) {
|
||||
handleNewCommand(message, username);
|
||||
return;
|
||||
}
|
||||
if (message.hasText() && handleControlCommand(message)) {
|
||||
return;
|
||||
}
|
||||
if (message.hasText() && !message.getText().isBlank()) {
|
||||
enqueueText(message, username);
|
||||
return;
|
||||
}
|
||||
if (message.hasVoice()) {
|
||||
enqueueVoice(message, username);
|
||||
return;
|
||||
}
|
||||
if (message.hasAudio()) {
|
||||
enqueueAudio(message, username);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка обработки update", e);
|
||||
safeSendText(message.getChatId(), "Ошибка обработки входящего сообщения: " + shortError(e), message.getMessageId());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNewCommand(Message message, String username) throws IOException {
|
||||
historyManager.appendSystemEvent(
|
||||
"command_new_received",
|
||||
Map.of(
|
||||
"chatId", message.getChatId(),
|
||||
"messageId", message.getMessageId(),
|
||||
"username", username
|
||||
)
|
||||
);
|
||||
var archived = historyManager.rotateHistory("command_new", username);
|
||||
String response = "История очищена. Новый диалог начат.\nАрхив: " + archived.getFileName();
|
||||
safeSendText(message.getChatId(), response, message.getMessageId());
|
||||
}
|
||||
|
||||
private void enqueueText(Message message, String username) throws IOException {
|
||||
QueueJob job = QueueJob.textJob(
|
||||
message.getChatId(),
|
||||
message.getMessageId(),
|
||||
username,
|
||||
message.getText(),
|
||||
historyManager.currentHistoryFile().toString()
|
||||
);
|
||||
historyManager.appendIncomingText(message.getChatId(), message.getMessageId(), username, message.getText());
|
||||
queueStore.enqueue(job);
|
||||
safeSendText(message.getChatId(), "Принял в очередь: " + shortId(job.id), message.getMessageId());
|
||||
}
|
||||
|
||||
private void enqueueVoice(Message message, String username) throws IOException {
|
||||
String fileId = message.getVoice().getFileId();
|
||||
QueueJob job = QueueJob.voiceJob(
|
||||
message.getChatId(),
|
||||
message.getMessageId(),
|
||||
username,
|
||||
fileId,
|
||||
historyManager.currentHistoryFile().toString()
|
||||
);
|
||||
historyManager.appendIncomingVoice(message.getChatId(), message.getMessageId(), username, fileId);
|
||||
queueStore.enqueue(job);
|
||||
safeSendText(message.getChatId(), "Голосовое принято в очередь: " + shortId(job.id), message.getMessageId());
|
||||
}
|
||||
|
||||
private void enqueueAudio(Message message, String username) throws IOException {
|
||||
String fileId = message.getAudio().getFileId();
|
||||
QueueJob job = QueueJob.voiceJob(
|
||||
message.getChatId(),
|
||||
message.getMessageId(),
|
||||
username,
|
||||
fileId,
|
||||
historyManager.currentHistoryFile().toString()
|
||||
);
|
||||
historyManager.appendIncomingVoice(message.getChatId(), message.getMessageId(), username, fileId);
|
||||
queueStore.enqueue(job);
|
||||
safeSendText(message.getChatId(), "Аудио принято в очередь: " + shortId(job.id), message.getMessageId());
|
||||
}
|
||||
|
||||
private void processLoop() {
|
||||
while (running.get()) {
|
||||
try {
|
||||
Optional<QueueJob> next = queueStore.activateNext();
|
||||
if (next.isEmpty()) {
|
||||
Thread.sleep(500);
|
||||
continue;
|
||||
}
|
||||
processJob(next.get());
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка worker loop", e);
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processJob(QueueJob job) {
|
||||
ScheduledFuture<?> heartbeat = null;
|
||||
try {
|
||||
log.info("Начало обработки jobId={}, type={}, chatId={}, attempts={}", job.id, job.type, job.chatId, job.attempts);
|
||||
activeJobRef.set(job);
|
||||
activeJobStartedAt.set(System.currentTimeMillis());
|
||||
safeSendText(job.chatId, "Задача " + shortId(job.id) + " взята в работу.", job.messageId);
|
||||
String userText = resolveUserText(job);
|
||||
String prompt = buildPrompt(job, userText);
|
||||
historyManager.appendCodexRequest(job.id, prompt);
|
||||
|
||||
log.info("Вызов Codex для jobId={}", job.id);
|
||||
heartbeat = heartbeatScheduler.scheduleAtFixedRate(
|
||||
() -> notifier.submit(() ->
|
||||
safeSendText(job.chatId, "Статус " + shortId(job.id) + ": в работе " + elapsedSeconds() + "с", job.messageId)
|
||||
),
|
||||
30, 30, TimeUnit.SECONDS
|
||||
);
|
||||
String answer;
|
||||
answer = codexClient.executePrompt(prompt, buildStatusListener(job));
|
||||
log.info("Codex завершился для jobId={}, длина ответа={}", job.id, answer.length());
|
||||
safeSendText(job.chatId, "Codex завершил обработку, отправляю результат.", job.messageId);
|
||||
sendLongMessage(job.chatId, answer, job.messageId);
|
||||
historyManager.appendCodexResponse(job.id, answer);
|
||||
historyManager.appendOutgoingMessage(job.id, job.chatId, answer);
|
||||
queueStore.markDone(job.id);
|
||||
log.info("Задача завершена jobId={}", job.id);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
handleInterruptedJob(job, e);
|
||||
} catch (Exception e) {
|
||||
handleFailedJob(job, e);
|
||||
} finally {
|
||||
if (heartbeat != null) {
|
||||
heartbeat.cancel(true);
|
||||
}
|
||||
activeJobRef.set(null);
|
||||
activeJobStartedAt.set(0L);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInterruptedJob(QueueJob job, InterruptedException e) {
|
||||
if (!running.get()) {
|
||||
log.info("Обработка jobId={} прервана из-за остановки сервиса", job.id);
|
||||
try {
|
||||
historyManager.appendSystemEvent("job_interrupted_on_shutdown", Map.of(
|
||||
"jobId", job.id,
|
||||
"reason", shortError(e)
|
||||
));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
handleFailedJob(job, e);
|
||||
}
|
||||
|
||||
private void handleFailedJob(QueueJob job, Exception e) {
|
||||
String error = shortError(e);
|
||||
log.error("Ошибка обработки jobId={}: {}", job.id, error, e);
|
||||
try {
|
||||
if (!isJobStillInQueue(job.id)) {
|
||||
log.info("Задача {} уже удалена из очереди, ошибка не ретраится", job.id);
|
||||
return;
|
||||
}
|
||||
FailureResult failure = queueStore.markFailed(job.id, error, config.maxRetries());
|
||||
historyManager.appendJobError(job.id, error, failure.willRetry(), failure.attempts(), failure.maxRetries());
|
||||
String message = failure.willRetry()
|
||||
? "Ошибка выполнения задачи " + shortId(job.id) + ", повтор: " + failure.attempts() + "/" + failure.maxRetries()
|
||||
: "Ошибка выполнения задачи " + shortId(job.id) + ". Лимит попыток исчерпан.";
|
||||
safeSendText(job.chatId, message, job.messageId);
|
||||
} catch (Exception inner) {
|
||||
log.error("Не удалось зафиксировать ошибку задачи {}", job.id, inner);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJobStillInQueue(String jobId) {
|
||||
for (QueueJob item : queueStore.snapshot()) {
|
||||
if (jobId.equals(item.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private CodexStatusListener buildStatusListener(QueueJob job) {
|
||||
AtomicReference<String> lastStatus = new AtomicReference<>("");
|
||||
AtomicLong lastSentAt = new AtomicLong(0L);
|
||||
return status -> {
|
||||
long now = System.currentTimeMillis();
|
||||
String prev = lastStatus.get();
|
||||
boolean changed = !status.equals(prev);
|
||||
boolean heartbeatDue = now - lastSentAt.get() > 30_000;
|
||||
if (changed || heartbeatDue) {
|
||||
String text = changed
|
||||
? "Статус " + shortId(job.id) + ": " + status
|
||||
: "Статус " + shortId(job.id) + ": в работе " + elapsedSeconds() + "с";
|
||||
notifier.submit(() -> safeSendText(job.chatId, text, job.messageId));
|
||||
lastStatus.set(status);
|
||||
lastSentAt.set(now);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String resolveUserText(QueueJob job) throws IOException, InterruptedException, TelegramApiException {
|
||||
if (!"voice".equals(job.type)) {
|
||||
return job.text;
|
||||
}
|
||||
byte[] audio = downloadTelegramFile(job.telegramFileId);
|
||||
String transcription = transcriber.transcribe(audio, job.id + ".ogg");
|
||||
historyManager.appendTranscription(job.id, transcription);
|
||||
safeSendText(job.chatId, "Распознано:\n" + transcription, job.messageId);
|
||||
return transcription;
|
||||
}
|
||||
|
||||
private byte[] downloadTelegramFile(String fileId) throws IOException, InterruptedException, TelegramApiException {
|
||||
GetFile getFile = new GetFile(fileId);
|
||||
org.telegram.telegrambots.meta.api.objects.File tgFile = execute(getFile);
|
||||
String fileUrl = tgFile.getFileUrl(getBotToken());
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(fileUrl))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||
if (response.statusCode() >= 300) {
|
||||
throw new IOException("Telegram file download failed HTTP " + response.statusCode());
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
private String buildPrompt(QueueJob job, String text) {
|
||||
String retryBlock = "";
|
||||
if (job.retryReason != null && !job.retryReason.isBlank()) {
|
||||
retryBlock = "\n\nПометка retry: " + job.retryReason;
|
||||
}
|
||||
|
||||
return """
|
||||
Пришло сообщение в Telegram.
|
||||
Тип: %s
|
||||
Username отправителя: @%s
|
||||
Текст для обработки:
|
||||
%s
|
||||
|
||||
История диалога (JSONL): %s
|
||||
Инструкции агента: %s
|
||||
Работай в рабочем проекте аккуратно и верни только текст ответа пользователю.%s
|
||||
""".formatted(
|
||||
job.type,
|
||||
job.username,
|
||||
text,
|
||||
job.historyFile,
|
||||
config.agentInstructionsFile(),
|
||||
retryBlock
|
||||
);
|
||||
}
|
||||
|
||||
private boolean handleControlCommand(Message message) throws IOException {
|
||||
String text = message.getText().trim();
|
||||
String lower = text.toLowerCase();
|
||||
|
||||
if ("/start".equals(lower) || "/help".equals(lower)) {
|
||||
safeSendText(message.getChatId(), helpText(), message.getMessageId());
|
||||
return true;
|
||||
}
|
||||
if ("/status".equals(lower)) {
|
||||
safeSendText(message.getChatId(), buildStatusText(), message.getMessageId());
|
||||
return true;
|
||||
}
|
||||
if ("/queue".equals(lower)) {
|
||||
safeSendText(message.getChatId(), buildQueueText(), message.getMessageId());
|
||||
return true;
|
||||
}
|
||||
if ("/stop".equals(lower)) {
|
||||
boolean stopped = codexClient.stopActiveProcess();
|
||||
if (stopped) {
|
||||
queueStore.cancelActiveJob("stopped_by_user");
|
||||
historyManager.appendSystemEvent("job_stopped_by_user", Map.of(
|
||||
"timestamp", Instant.now().toString()
|
||||
));
|
||||
safeSendText(message.getChatId(), "Текущая задача остановлена и удалена из очереди.", message.getMessageId());
|
||||
} else {
|
||||
safeSendText(message.getChatId(), "Сейчас нет активной задачи.", message.getMessageId());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (lower.startsWith("/cancel")) {
|
||||
String[] parts = text.split("\\s+", 2);
|
||||
if (parts.length < 2) {
|
||||
safeSendText(message.getChatId(), "Использование: /cancel <id|all>", message.getMessageId());
|
||||
return true;
|
||||
}
|
||||
String arg = parts[1].trim();
|
||||
if ("all".equalsIgnoreCase(arg)) {
|
||||
codexClient.stopActiveProcess();
|
||||
int cancelled = queueStore.cancelAll("cancel_all_by_user");
|
||||
safeSendText(message.getChatId(), "Удалено задач из очереди: " + cancelled, message.getMessageId());
|
||||
return true;
|
||||
}
|
||||
Optional<QueueJob> active = queueStore.getActiveJob();
|
||||
if (active.isPresent() && active.get().id != null
|
||||
&& active.get().id.toLowerCase().startsWith(arg.toLowerCase())) {
|
||||
codexClient.stopActiveProcess();
|
||||
}
|
||||
boolean cancelled = queueStore.cancelByIdPrefix(arg);
|
||||
safeSendText(message.getChatId(),
|
||||
cancelled ? "Задача удалена: " + arg : "Задача не найдена: " + arg,
|
||||
message.getMessageId());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String buildStatusText() {
|
||||
Optional<QueueJob> active = queueStore.getActiveJob();
|
||||
int pending = queueStore.pendingCount();
|
||||
if (active.isEmpty()) {
|
||||
return "Статус: активной задачи нет.\nВ очереди pending: " + pending;
|
||||
}
|
||||
QueueJob job = active.get();
|
||||
return "Статус: активная задача " + shortId(job.id) +
|
||||
"\nТип: " + job.type +
|
||||
"\nПопытка: " + (job.attempts + 1) + "/" + config.maxRetries() +
|
||||
"\nВыполняется: " + elapsedSeconds() + "с" +
|
||||
"\nPending: " + pending;
|
||||
}
|
||||
|
||||
private String buildQueueText() {
|
||||
List<QueueJob> jobs = queueStore.snapshot();
|
||||
if (jobs.isEmpty()) {
|
||||
return "Очередь пуста.";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Очередь: ").append(jobs.size()).append('\n');
|
||||
int limit = Math.min(jobs.size(), 10);
|
||||
for (int i = 0; i < limit; i++) {
|
||||
QueueJob job = jobs.get(i);
|
||||
sb.append(i + 1).append(") ")
|
||||
.append(shortId(job.id))
|
||||
.append(" [").append(job.status).append("] ")
|
||||
.append(job.type)
|
||||
.append(" attempts=").append(job.attempts)
|
||||
.append('\n');
|
||||
}
|
||||
if (jobs.size() > limit) {
|
||||
sb.append("...и ещё ").append(jobs.size() - limit).append(" задач");
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
private String helpText() {
|
||||
return """
|
||||
Доступные команды:
|
||||
/status — активная задача и размер очереди
|
||||
/queue — список задач в очереди
|
||||
/stop — остановить текущую задачу
|
||||
/cancel <id|all> — удалить задачу по id (префикс) или все
|
||||
/new — архивировать историю и начать новую
|
||||
/help — эта справка
|
||||
""";
|
||||
}
|
||||
|
||||
private void safeSendText(long chatId, String text, Integer replyToMessageId) {
|
||||
try {
|
||||
SendMessage message = new SendMessage();
|
||||
message.setChatId(String.valueOf(chatId));
|
||||
message.setText(trimForTelegram(text));
|
||||
if (replyToMessageId != null) {
|
||||
message.setReplyToMessageId(replyToMessageId);
|
||||
}
|
||||
execute(message);
|
||||
} catch (Exception e) {
|
||||
log.error("Не удалось отправить сообщение в Telegram", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendLongMessage(long chatId, String text, Integer replyToMessageId) {
|
||||
String normalized = text == null ? "" : text.strip();
|
||||
if (normalized.isEmpty()) {
|
||||
safeSendText(chatId, "(пустой ответ)", replyToMessageId);
|
||||
return;
|
||||
}
|
||||
int max = 3500;
|
||||
int start = 0;
|
||||
while (start < normalized.length()) {
|
||||
int end = Math.min(start + max, normalized.length());
|
||||
String part = normalized.substring(start, end);
|
||||
safeSendText(chatId, part, replyToMessageId);
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
private String trimForTelegram(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String text = value.strip();
|
||||
int max = 3900;
|
||||
if (text.length() <= max) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, max) + "\n...[обрезано]";
|
||||
}
|
||||
|
||||
private String shortId(String id) {
|
||||
if (id == null || id.length() < 8) {
|
||||
return id;
|
||||
}
|
||||
return id.substring(0, 8);
|
||||
}
|
||||
|
||||
private long elapsedSeconds() {
|
||||
long started = activeJobStartedAt.get();
|
||||
if (started <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (System.currentTimeMillis() - started) / 1000L;
|
||||
}
|
||||
|
||||
private String shortError(Throwable e) {
|
||||
String message = e.getMessage();
|
||||
if (message == null || message.isBlank()) {
|
||||
return e.getClass().getSimpleName();
|
||||
}
|
||||
String normalized = message.replace('\n', ' ').replace('\r', ' ').trim();
|
||||
if (normalized.length() > 600) {
|
||||
return normalized.substring(0, 600);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,2 @@
|
||||
client.version=1.2.96
|
||||
server.version=1.2.90
|
||||
client.version=1.2.84
|
||||
server.version=1.2.78
|
||||
|
||||
@ -6,7 +6,6 @@ REMOTE_HOST="${REMOTE_HOST:-player@45.136.124.227}"
|
||||
REMOTE_UI_DIR="${REMOTE_UI_DIR:-/home/player/SHiNE/shine-ui}"
|
||||
EXPECTED_CADDY_UI_ROOT="${EXPECTED_CADDY_UI_ROOT:-/home/player/SHiNE/shine-ui}"
|
||||
ALLOW_CADDY_MISMATCH="${ALLOW_CADDY_MISMATCH:-0}"
|
||||
EXPECTED_CADDY_SITE="${EXPECTED_CADDY_SITE:-shineup.me}"
|
||||
BUILD_VERSION="$(date -u +%Y%m%d%H%M%S)"
|
||||
VERSION_FILE="VERSION.properties"
|
||||
export BUILD_VERSION
|
||||
@ -60,68 +59,9 @@ CADDY_CONFIG_PATH="$(ssh "$REMOTE_HOST" "set -e; \
|
||||
cfg=\$(printf '%s' \"\$exec_line\" | sed -n 's/.*--config \\([^ ]*\\).*/\\1/p' | head -n 1); \
|
||||
if [ -z \"\$cfg\" ]; then cfg=/etc/caddy/Caddyfile; fi; \
|
||||
printf '%s' \"\$cfg\"")"
|
||||
ROOT_CHECK_OUTPUT="$(ssh "$REMOTE_HOST" "set -euo pipefail; \
|
||||
cfg='$CADDY_CONFIG_PATH'; \
|
||||
site='$EXPECTED_CADDY_SITE'; \
|
||||
sudo awk -v site=\"\$site\" '
|
||||
BEGIN { in_site=0; depth=0; root_line=\"\"; root_lineno=0; have_site=0; }
|
||||
{
|
||||
line=\$0;
|
||||
trimmed=line;
|
||||
sub(/^[[:space:]]+/, \"\", trimmed);
|
||||
sub(/[[:space:]]+$/, \"\", trimmed);
|
||||
|
||||
if (!in_site) {
|
||||
if (trimmed ~ \"^\" site \"[[:space:]]*\\\\{\") {
|
||||
in_site=1;
|
||||
have_site=1;
|
||||
depth=1;
|
||||
next;
|
||||
}
|
||||
} else {
|
||||
if (trimmed ~ /^root[[:space:]]+\\*[[:space:]]+/ && root_line == \"\") {
|
||||
root_line=line;
|
||||
root_lineno=NR;
|
||||
}
|
||||
opens=gsub(/\\{/, \"{\", line);
|
||||
closes=gsub(/\\}/, \"}\", line);
|
||||
depth += (opens - closes);
|
||||
if (depth <= 0) {
|
||||
in_site=0;
|
||||
}
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (!have_site) {
|
||||
print \"SITE_NOT_FOUND\";
|
||||
exit 0;
|
||||
}
|
||||
if (root_line == \"\") {
|
||||
print \"ROOT_NOT_FOUND\";
|
||||
exit 0;
|
||||
}
|
||||
print root_lineno \":\" root_line;
|
||||
}
|
||||
' \"\$cfg\"")"
|
||||
|
||||
if [[ "$ROOT_CHECK_OUTPUT" == "SITE_NOT_FOUND" ]]; then
|
||||
echo "ERROR: Caddy site block not found: $EXPECTED_CADDY_SITE" >&2
|
||||
echo "Caddy config: $CADDY_CONFIG_PATH" >&2
|
||||
if [[ "$ALLOW_CADDY_MISMATCH" != "1" ]]; then
|
||||
echo "Set ALLOW_CADDY_MISMATCH=1 to bypass this check." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "WARN: proceeding due to ALLOW_CADDY_MISMATCH=1"
|
||||
elif [[ "$ROOT_CHECK_OUTPUT" == "ROOT_NOT_FOUND" ]]; then
|
||||
echo "ERROR: root directive not found inside site block: $EXPECTED_CADDY_SITE" >&2
|
||||
echo "Caddy config: $CADDY_CONFIG_PATH" >&2
|
||||
if [[ "$ALLOW_CADDY_MISMATCH" != "1" ]]; then
|
||||
echo "Set ALLOW_CADDY_MISMATCH=1 to bypass this check." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "WARN: proceeding due to ALLOW_CADDY_MISMATCH=1"
|
||||
elif [[ "$ROOT_CHECK_OUTPUT" != *"$EXPECTED_CADDY_UI_ROOT"* ]]; then
|
||||
echo "ERROR: Caddy root mismatch for site $EXPECTED_CADDY_SITE. Found: $ROOT_CHECK_OUTPUT" >&2
|
||||
CADDY_ROOT_LINE="$(ssh "$REMOTE_HOST" "sudo grep -n 'root \\* ' '$CADDY_CONFIG_PATH' | head -n 1 || true")"
|
||||
if [[ -n "$CADDY_ROOT_LINE" && "$CADDY_ROOT_LINE" != *"$EXPECTED_CADDY_UI_ROOT"* ]]; then
|
||||
echo "ERROR: Caddy root mismatch. Found: $CADDY_ROOT_LINE" >&2
|
||||
echo "Caddy config: $CADDY_CONFIG_PATH" >&2
|
||||
echo "Expected path fragment: $EXPECTED_CADDY_UI_ROOT" >&2
|
||||
if [[ "$ALLOW_CADDY_MISMATCH" != "1" ]]; then
|
||||
|
||||
36
doc_todelite/DOC/libs/shine-main Описание базовых классов.md
Normal file
36
doc_todelite/DOC/libs/shine-main Описание базовых классов.md
Normal file
@ -0,0 +1,36 @@
|
||||
краткая «памятка себе» по базовым классам и как они связаны.
|
||||
|
||||
server.logic.InboundMessageProcessor (устаревший путь)
|
||||
|
||||
Роль: маршрутизатор бинарного протокола: берёт входящие байты, читает первые 4 байта как op, находит MessageHandler и отдаёт ему сообщение.
|
||||
Что возвращает: байтовый ответ хэндлера; при ошибках — 4 байта со статусом (BAD_REQUEST/INTERNAL_ERROR).
|
||||
Важно: сейчас фактически не используется (карта HANDLERS пустая/закомментирована) — это «след» старого бинарного протокола, который вы заменили на JSON-WS.
|
||||
|
||||
server.ws.BlockchainTmpRecoveryOnStartup
|
||||
|
||||
Роль: «автослесарь» при старте: чинит последствия падения во время записи блокчейн-файла.
|
||||
Логика: ищет *.tmp_bch в data/, сравнивает размеры tmp, main .bch и state.fileSizeBytes из БД.
|
||||
Решения:
|
||||
если stateSize == mainSize → tmp мусор, удаляем;
|
||||
если stateSize == tmpSize → tmp актуален, атомарно заменяем main;
|
||||
если не сходится / подозрительно (нет state, но есть main+tmp и т.п.) → CRITICAL + стоп сервера.
|
||||
Итог: гарантирует, что на запуске не будет «тихо битого» блокчейна.
|
||||
|
||||
server.ws.BlockchainWsEndpoint
|
||||
|
||||
Роль: WS-эндпоинт Jetty, который принимает и бинарные, и текстовые сообщения.
|
||||
Connect: сохраняет Session, кладёт её в ConnectionContext.
|
||||
Binary: асинхронно вызывает InboundMessageProcessor.process(msg) и отправляет байтовый ответ. (Это тот самый устаревший путь.)
|
||||
Text (JSON): асинхронно вызывает JsonInboundProcessor.processJson(message, connectionContext) и отправляет строку JSON.
|
||||
Close: удаляет соединение из ActiveConnectionsRegistry, чистит ConnectionContext.
|
||||
Смысл: один входной узел WS, где JSON — основной протокол, binary — “наследие”.
|
||||
|
||||
server.ws.WsServer
|
||||
|
||||
Роль: точка входа сервера.
|
||||
|
||||
Порядок запуска:
|
||||
BlockchainTmpRecoveryOnStartup.runRecoveryOrThrow() — если не смог починить/сопоставить → сервер не стартует;
|
||||
читает порт из AppConfig (server.port), иначе 7070;
|
||||
поднимает Jetty, конфигурирует WS-контейнер, маппит /ws → BlockchainWsEndpoint, ставит idleTimeout.
|
||||
Итог: «бутстрап»: сначала безопасность файлов, потом сеть.
|
||||
286
doc_todelite/DOC/Описание протокола.md
Normal file
286
doc_todelite/DOC/Описание протокола.md
Normal file
@ -0,0 +1,286 @@
|
||||
JSON WebSocket протокол сервера: список существующих запросов (op)
|
||||
|
||||
Общий формат любого запроса (client → server):
|
||||
op - имя операции (строка)
|
||||
requestId - идентификатор запроса для связывания с ответом (строка)
|
||||
payload - объект с параметрами конкретной операции (object)
|
||||
|
||||
Общий формат любого ответа (server → client):
|
||||
op - имя операции (строка, совпадает с запросом)
|
||||
requestId - идентификатор запроса (строка, совпадает с запросом)
|
||||
status - статус результата (200 = успех, другое = ошибка)
|
||||
payload - объект с полями ответа (object; при ошибке содержит code/message)
|
||||
|
||||
Группа: Авторизация и сессии
|
||||
|
||||
Эта группа управляет входом пользователя, созданием/обновлением сессий и безопасным завершением активных подключений.
|
||||
|
||||
----- AuthChallenge
|
||||
Одноразовый шаг 1: по логину выдаёт nonce (authNonce), который затем подписывается на шаге 2.
|
||||
|
||||
Запрос:
|
||||
op - "AuthChallenge"
|
||||
requestId - id запроса
|
||||
login - логин пользователя, для которого начинается авторизация
|
||||
|
||||
Ответ (успех):
|
||||
op - "AuthChallenge"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
authNonce - одноразовый nonce, Base64Url(32 bytes) без padding
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "AuthChallenge"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- CreateAuthSession
|
||||
Шаг 2: проверяет подпись владения ключом, создаёт новую active_session и возвращает sessionId/sessionPwd.
|
||||
|
||||
Запрос:
|
||||
op - "CreateAuthSession"
|
||||
requestId - id запроса
|
||||
storagePwd - ключ/пароль хранилища клиента, base64(32 bytes)
|
||||
timeMs - время на клиенте в мс (для защиты от повторов и проверки рассинхрона)
|
||||
signatureB64 - подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64)
|
||||
clientInfo - короткая строка о клиенте/устройстве (до 50 символов), опционально
|
||||
|
||||
Ответ (успех):
|
||||
op - "CreateAuthSession"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
sessionId - идентификатор сессии, Base64Url(32 bytes)
|
||||
sessionPwd - секрет сессии, Base64Url(32 bytes)
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "CreateAuthSession"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- RefreshSession
|
||||
Повторный вход без подписи: проверяет sessionId+sessionPwd, обновляет метаданные сессии и возвращает storagePwd.
|
||||
|
||||
Запрос:
|
||||
op - "RefreshSession"
|
||||
requestId - id запроса
|
||||
sessionId - идентификатор ранее выданной сессии (base64/url-safe строка)
|
||||
sessionPwd - секрет ранее выданной сессии (base64/url-safe строка)
|
||||
clientInfo - короткая строка о клиенте/устройстве (до 50 символов), опционально
|
||||
|
||||
Ответ (успех):
|
||||
op - "RefreshSession"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
storagePwd - пароль хранилища, сохранённый в сессии (base64(32 bytes))
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "RefreshSession"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- ListSessions
|
||||
Возвращает список всех активных сессий текущего пользователя (для управления устройствами/входами).
|
||||
|
||||
Запрос:
|
||||
op - "ListSessions"
|
||||
requestId - id запроса
|
||||
timeMs - время на клиенте в мс (нужно только если статус AUTH_IN_PROGRESS)
|
||||
signatureB64 - подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (нужно только если статус AUTH_IN_PROGRESS)
|
||||
|
||||
Ответ (успех):
|
||||
op - "ListSessions"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
sessions - массив активных сессий пользователя
|
||||
|
||||
Поля элемента sessions[i]:
|
||||
sessionId - идентификатор сессии, Base64Url(32 bytes)
|
||||
clientInfoFromClient - что прислал клиент в clientInfo
|
||||
clientInfoFromRequest - что собрал сервер из окружения (UA/платформа и т.п.)
|
||||
geo - строка "Country, City" или "unknown"
|
||||
lastAuthirificatedAtMs - время последней успешной авторизации/refresh (мс)
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "ListSessions"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- CloseActiveSession
|
||||
Закрывает одну активную сессию пользователя (указанную или текущую), при необходимости подтверждая владение ключом подписью.
|
||||
|
||||
Запрос:
|
||||
op - "CloseActiveSession"
|
||||
requestId - id запроса
|
||||
sessionId - идентификатор сессии для закрытия (если пусто, закрывается текущая)
|
||||
timeMs - время на клиенте в мс (нужно только если статус AUTH_IN_PROGRESS)
|
||||
signatureB64 - подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (нужно только если статус AUTH_IN_PROGRESS)
|
||||
|
||||
Ответ (успех):
|
||||
op - "CloseActiveSession"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "CloseActiveSession"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
Группа: Блокчейн (загрузка блоков)
|
||||
|
||||
Эта группа отвечает за приём и валидацию блоков (цепочка, линии, подпись/хэш) и атомарную запись в БД+файл.
|
||||
|
||||
----- AddBlock
|
||||
Добавляет следующий блок в конкретный blockchainName, строго проверяя номера, prev-хэши, линии, подпись и лимит размера.
|
||||
|
||||
Запрос:
|
||||
op - "AddBlock"
|
||||
requestId - id запроса
|
||||
blockchainName - имя цепочки (по нему вычисляется login)
|
||||
globalNumber - глобальный номер добавляемого блока (должен быть serverLastGlobalNumber+1)
|
||||
prevGlobalHash - HEX(64) предыдущего глобального хэша (или "" только там, где это допускается правилами)
|
||||
blockBytesB64 - полный блок (raw+signature+hash) в Base64
|
||||
|
||||
Ответ (успех):
|
||||
op - "AddBlock"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
reasonCode - null при успехе
|
||||
serverLastGlobalNumber - последний глобальный номер, который сервер считает актуальным после обработки
|
||||
serverLastGlobalHash - последний глобальный хэш (HEX(64)) после обработки
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "AddBlock"
|
||||
requestId - id запроса
|
||||
status - код ошибки (например 400/404/413/500)
|
||||
reasonCode - строка причины (например bad_block_base64, bad_global_number, limit_exceeded и т.п.)
|
||||
serverLastGlobalNumber - текущий серверный lastGlobalNumber
|
||||
serverLastGlobalHash - текущий серверный lastGlobalHash (HEX(64), если известен)
|
||||
|
||||
Группа: Параметры пользователя (UserParams)
|
||||
|
||||
Эта группа хранит и отдаёт подписанные клиентом параметры (ключ-значение) для синхронизации и состояния.
|
||||
|
||||
----- UpsertUserParam
|
||||
Добавляет или обновляет параметр пользователя, проверяя подпись Ed25519 и применяя запись только если time_ms новее.
|
||||
|
||||
Запрос:
|
||||
op - "UpsertUserParam"
|
||||
requestId - id запроса
|
||||
login - логин пользователя
|
||||
param - имя параметра
|
||||
time_ms - метка времени значения в мс
|
||||
value - значение параметра
|
||||
device_key - публичный ключ устройства, base64(32 bytes)
|
||||
signature - подпись Ed25519 от строки USER_PARAMETER_PREFIX + login + param + time_ms + value
|
||||
|
||||
Ответ (успех):
|
||||
op - "UpsertUserParam"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "UpsertUserParam"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- GetUserParam
|
||||
Возвращает один сохранённый параметр пользователя.
|
||||
|
||||
Запрос:
|
||||
op - "GetUserParam"
|
||||
requestId - id запроса
|
||||
login - логин пользователя
|
||||
param - имя параметра
|
||||
|
||||
Ответ (успех):
|
||||
op - "GetUserParam"
|
||||
requestId - id запроса
|
||||
status - 200
|
||||
login
|
||||
param
|
||||
time_ms
|
||||
value
|
||||
device_key
|
||||
signature
|
||||
|
||||
Ответ (не найдено):
|
||||
op - "GetUserParam"
|
||||
requestId
|
||||
status - 404
|
||||
|
||||
Ответ (ошибка):
|
||||
op
|
||||
requestId
|
||||
status
|
||||
code
|
||||
message
|
||||
|
||||
----- ListUserParams
|
||||
Возвращает все сохранённые параметры пользователя.
|
||||
|
||||
Запрос:
|
||||
op - "ListUserParams"
|
||||
requestId
|
||||
login
|
||||
|
||||
Ответ (успех):
|
||||
op
|
||||
requestId
|
||||
status - 200
|
||||
login
|
||||
params - массив параметров
|
||||
|
||||
Поля params[i]:
|
||||
login
|
||||
param
|
||||
time_ms
|
||||
value
|
||||
device_key
|
||||
signature
|
||||
|
||||
Ответ (ошибка):
|
||||
op
|
||||
requestId
|
||||
status
|
||||
code
|
||||
message
|
||||
|
||||
Группа: Тестовые/временные операции
|
||||
|
||||
Эта группа предназначена для отладки и первичного наполнения БД.
|
||||
|
||||
----- AddUser
|
||||
Тестовая регистрация локального пользователя.
|
||||
|
||||
Запрос:
|
||||
op - "AddUser"
|
||||
requestId
|
||||
login
|
||||
blockchainName
|
||||
solanaKey
|
||||
deviceKey
|
||||
bchLimit
|
||||
|
||||
Ответ (успех):
|
||||
op
|
||||
requestId
|
||||
status - 200
|
||||
|
||||
Ответ (ошибка):
|
||||
op
|
||||
requestId
|
||||
status
|
||||
code
|
||||
message
|
||||
165
doc_todelite/DOC/Формат Блокцейнов.md
Normal file
165
doc_todelite/DOC/Формат Блокцейнов.md
Normal file
@ -0,0 +1,165 @@
|
||||
нет это неправильно откатись к1) Общий формат записи блока (BchBlockEntry)
|
||||
|
||||
Блок хранится как FULL = RAW + TAIL, где RAW участвует в хэшировании/подписи, а TAIL хранит подпись и итоговый хэш.
|
||||
|
||||
RAW (BigEndian)
|
||||
|
||||
recordSize [4] int32 — размер RAW в байтах (включая поля RAW-заголовка и bodyBytes), без signature64 и hash32.
|
||||
|
||||
recordNumber [4] int32 — глобальный порядковый номер блока (сквозной по всему блокчейну).
|
||||
|
||||
timestamp [8] int64 — Unix seconds (время создания блока).
|
||||
|
||||
lineIndex [2] int16 — номер линии (канонические линии см. LineIndex).
|
||||
|
||||
lineNumber [4] int32 — порядковый номер внутри выбранной линии.
|
||||
|
||||
bodyBytes [N] bytes — тело блока, начинается с [type][version] (и дальше подформат конкретного body).
|
||||
|
||||
TAIL (не входит в recordSize)
|
||||
|
||||
signature64 [64] bytes — подпись Ed25519 над hash32.
|
||||
|
||||
hash32 [32] bytes — SHA-256 от preimage (см. ниже).
|
||||
|
||||
2) Как считается хэш и что подписываем (BchCryptoVerifier)
|
||||
|
||||
preimage =
|
||||
|
||||
"SHiNE" (ASCII)
|
||||
|
||||
loginLen[1] + loginBytes[loginLen] (UTF-8, 1..255)
|
||||
|
||||
prevGlobalHash32[32]
|
||||
|
||||
prevLineHash32[32]
|
||||
|
||||
rawBytes[recordSize]
|
||||
|
||||
hash32 = SHA-256(preimage)
|
||||
Далее верификация:
|
||||
|
||||
hash32 должен совпасть с hash32, записанным в блоке.
|
||||
|
||||
signature64 проверяется как Ed25519 подпись над hash32 публичным ключом пользователя.
|
||||
|
||||
3) Типы body и разновидности (по 1 предложению на тип)
|
||||
|
||||
HeaderBody (type=0) — генезис/идентификация блокчейна: фиксирует владельца (login) и тег формата.
|
||||
|
||||
TextBody (type=1) — текстовое сообщение: либо новое, либо ответ (reply), либо репост (repost) со ссылкой на целевой блок.
|
||||
|
||||
ReactionBody (type=2) — реакция на конкретный блок (в MVP — лайк) по ссылке на блок.
|
||||
|
||||
ConnectionBody (type=3) — событие связи с другим пользователем (friend/contact/follow) или отмена этой связи.
|
||||
|
||||
UserParamBody (type=4) — изменение/заявление одного параметра профиля в формате key/value.
|
||||
|
||||
4) Общий формат bodyBytes (для всех body)
|
||||
|
||||
type [2] int16 — код типа тела (0..4).
|
||||
|
||||
version [2] int16 — версия формата конкретного типа (сейчас везде 1).
|
||||
|
||||
subType [2] uint16 — подтип внутри типа (для Header всегда 0; для Text — NEW/REPLY/REPOST; для Reaction — LIKE; для Connection — set/unset + вид; для UserParam — TEXT_TEXT).
|
||||
|
||||
payload [N] bytes — поля конкретного body (строго по формату; “мусор” в конце запрещён).
|
||||
|
||||
5) Формат каждого типа body (по 1 строке на поле)
|
||||
5.1 HeaderBody (type=0, ver=1, lineIndex=0)
|
||||
|
||||
type [2] — 0.
|
||||
|
||||
version [2] — 1.
|
||||
|
||||
subType [2] — 0 (compat).
|
||||
|
||||
tag [5] — ASCII "SHiNE".
|
||||
|
||||
loginLen [1] — длина login в UTF-8 (1..255).
|
||||
|
||||
login [N] — login UTF-8 (^[A-Za-z0-9_]+$).
|
||||
|
||||
5.2 TextBody (type=1, ver=1, lineIndex=1)
|
||||
|
||||
type [2] — 1.
|
||||
|
||||
version [2] — 1.
|
||||
|
||||
subType [2] — 1=NEW, 2=REPLY, 3=REPOST.
|
||||
|
||||
textLenBytes [2] — длина текста в байтах UTF-8 (1..65535).
|
||||
|
||||
text [N] — текст UTF-8 (валидный, не blank).
|
||||
|
||||
toBlockchainNameLen [1] — (только для REPLY/REPOST) длина имени блокчейна цели (1..255).
|
||||
|
||||
toBlockchainName [N] — (только для REPLY/REPOST) UTF-8 имя блокчейна цели.
|
||||
|
||||
toBlockGlobalNumber [4] — (только для REPLY/REPOST) globalNumber целевого блока.
|
||||
|
||||
toBlockHash32 [32] — (только для REPLY/REPOST) raw-хэш целевого блока.
|
||||
|
||||
5.3 ReactionBody (type=2, ver=1, lineIndex=2)
|
||||
|
||||
type [2] — 2.
|
||||
|
||||
version [2] — 1.
|
||||
|
||||
subType [2] — 1=LIKE (зарезервировано под будущие реакции).
|
||||
|
||||
toBlockchainNameLen [1] — длина имени блокчейна цели (1..255).
|
||||
|
||||
toBlockchainName [N] — UTF-8 имя блокчейна цели.
|
||||
|
||||
toBlockGlobalNumber [4] — globalNumber целевого блока.
|
||||
|
||||
toBlockHash32 [32] — raw-хэш целевого блока.
|
||||
|
||||
5.4 ConnectionBody (type=3, ver=1, lineIndex=3)
|
||||
|
||||
type [2] — 3.
|
||||
|
||||
version [2] — 1.
|
||||
|
||||
subType [2] — 10/20/30 (FRIEND/CONTACT/FOLLOW) или 11/21/31 (UNFRIEND/UNCONTACT/UNFOLLOW).
|
||||
|
||||
toLoginLen [1] — длина login цели (1..255).
|
||||
|
||||
toLogin [N] — UTF-8 login цели (^[A-Za-z0-9_]+$).
|
||||
|
||||
toBlockchainNameLen [1] — длина имени блокчейна цели (1..255).
|
||||
|
||||
toBlockchainName [N] — UTF-8 имя блокчейна цели (снимок/якорь).
|
||||
|
||||
toBlockGlobalNumber [4] — lastKnown globalNumber у цели (снимок/якорь).
|
||||
|
||||
toBlockHash32 [32] — lastKnown hash у цели (снимок/якорь).
|
||||
|
||||
5.5 UserParamBody (type=4, ver=1, lineIndex=4)
|
||||
|
||||
type [2] — 4.
|
||||
|
||||
version [2] — 1.
|
||||
|
||||
subType [2] — 1=TEXT_TEXT.
|
||||
|
||||
keyLenBytes [2] — длина ключа в байтах UTF-8 (1..65535).
|
||||
|
||||
keyUtf8 [N] — ключ параметра UTF-8 (валидный, не blank).
|
||||
|
||||
valueLenBytes [2] — длина значения в байтах UTF-8 (1..65535).
|
||||
|
||||
valueUtf8 [M] — значение UTF-8 (валидное, не blank).
|
||||
|
||||
6) Канонические линии (LineIndex)
|
||||
|
||||
0 HEADER — генезис/идентификация.
|
||||
|
||||
1 TEXT — сообщения.
|
||||
|
||||
2 REACTION — реакции.
|
||||
|
||||
3 CONNECTION — связи.
|
||||
|
||||
4 USER_PARAM — параметры профиля.
|
||||
@ -0,0 +1,2 @@
|
||||
НАПИШИ ВНАЧАЛЕ ФОРМАТ ОБЩЕГО ЗАГЛАВИЯ.
|
||||
А ПОТОМ ФОРМАТ ПО КАЖДОМУ ТИПУ (И В НЁМ СУБТИПУ БЛОКОВ) ДЛЯ ЧЕГО НАДО, ЧТО ХРАНИТЬСЯ, КАКИЕ ПРАВИЛА И ОСОБЕННОСТИ ЗАПОЛНЕНИЯ
|
||||
@ -0,0 +1,39 @@
|
||||
Net_AuthChallenge_Request — запрос вызова авторизации
|
||||
Net_CreateAuthSession_Request — создание авторизационной сессии
|
||||
Net_CloseAuthSession_Request — закрытие (завершение) сессии
|
||||
Net_ListSessions_Request — получение списка активных сессий
|
||||
Net_RefreshSession_Request — обновление / продление сессии
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Net_AuthChallenge_Request — запрос вызова авторизации
|
||||
Net_CreateAuthSession_Request — создание авторизационной сессии
|
||||
|
||||
|
||||
закрытие сессии
|
||||
Net_CloseAuthSession_Request — закрытие (завершение) сессии
|
||||
елси уже авторифицирован то:
|
||||
Net_CloseAuthSession_Request с параметром номер сесии или без параметра если прям эту сесиию
|
||||
если не авторифицирован то:
|
||||
Net_AuthChallenge_Request — запрос вызова авторизации
|
||||
Net_CloseAuthSession_Request с параметром номер сесии для закрытия и время и цп для подтвержедния
|
||||
|
||||
|
||||
получение списка сессий
|
||||
|
||||
Net_AuthChallenge_Request — запрос вызова авторизации
|
||||
Net_ListSessions_Request — получение списка активных сессий
|
||||
|
||||
|
||||
Net_RefreshSession_Request — обновление / продление сессии
|
||||
|
||||
|
||||
|
||||
план что сделать
|
||||
|
||||
получить список сесссий
|
||||
и удалить сессию
|
||||
|
||||
при новом подключении или при активной сесии
|
||||
@ -0,0 +1,286 @@
|
||||
JSON WebSocket протокол сервера: список существующих запросов (op)
|
||||
|
||||
Общий формат любого запроса (client → server):
|
||||
op - имя операции (строка)
|
||||
requestId - идентификатор запроса для связывания с ответом (строка)
|
||||
payload - объект с параметрами конкретной операции (object)
|
||||
|
||||
Общий формат любого ответа (server → client):
|
||||
op - имя операции (строка, совпадает с запросом)
|
||||
requestId - идентификатор запроса (строка, совпадает с запросом)
|
||||
status - статус результата (200 = успех, другое = ошибка)
|
||||
payload - объект с полями ответа (object; при ошибке содержит code/message)
|
||||
|
||||
Группа: Авторизация и сессии
|
||||
|
||||
Эта группа управляет входом пользователя, созданием/обновлением сессий и безопасным завершением активных подключений.
|
||||
|
||||
----- AuthChallenge
|
||||
Одноразовый шаг 1: по логину выдаёт nonce (authNonce), который затем подписывается на шаге 2.
|
||||
|
||||
Запрос:
|
||||
op - "AuthChallenge"
|
||||
requestId - id запроса
|
||||
login - логин пользователя, для которого начинается авторизация
|
||||
|
||||
Ответ (успех):
|
||||
op - "AuthChallenge"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
authNonce - одноразовый nonce, Base64Url(32 bytes) без padding
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "AuthChallenge"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- CreateAuthSession
|
||||
Шаг 2: проверяет подпись владения ключом, создаёт новую active_session и возвращает sessionId/sessionPwd.
|
||||
|
||||
Запрос:
|
||||
op - "CreateAuthSession"
|
||||
requestId - id запроса
|
||||
storagePwd - ключ/пароль хранилища клиента, base64(32 bytes)
|
||||
timeMs - время на клиенте в мс (для защиты от повторов и проверки рассинхрона)
|
||||
signatureB64 - подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64)
|
||||
clientInfo - короткая строка о клиенте/устройстве (до 50 символов), опционально
|
||||
|
||||
Ответ (успех):
|
||||
op - "CreateAuthSession"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
sessionId - идентификатор сессии, Base64Url(32 bytes)
|
||||
sessionPwd - секрет сессии, Base64Url(32 bytes)
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "CreateAuthSession"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- RefreshSession
|
||||
Повторный вход без подписи: проверяет sessionId+sessionPwd, обновляет метаданные сессии и возвращает storagePwd.
|
||||
|
||||
Запрос:
|
||||
op - "RefreshSession"
|
||||
requestId - id запроса
|
||||
sessionId - идентификатор ранее выданной сессии (base64/url-safe строка)
|
||||
sessionPwd - секрет ранее выданной сессии (base64/url-safe строка)
|
||||
clientInfo - короткая строка о клиенте/устройстве (до 50 символов), опционально
|
||||
|
||||
Ответ (успех):
|
||||
op - "RefreshSession"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
storagePwd - пароль хранилища, сохранённый в сессии (base64(32 bytes))
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "RefreshSession"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- ListSessions
|
||||
Возвращает список всех активных сессий текущего пользователя (для управления устройствами/входами).
|
||||
|
||||
Запрос:
|
||||
op - "ListSessions"
|
||||
requestId - id запроса
|
||||
timeMs - время на клиенте в мс (нужно только если статус AUTH_IN_PROGRESS)
|
||||
signatureB64 - подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (нужно только если статус AUTH_IN_PROGRESS)
|
||||
|
||||
Ответ (успех):
|
||||
op - "ListSessions"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
sessions - массив активных сессий пользователя
|
||||
|
||||
Поля элемента sessions[i]:
|
||||
sessionId - идентификатор сессии, Base64Url(32 bytes)
|
||||
clientInfoFromClient - что прислал клиент в clientInfo
|
||||
clientInfoFromRequest - что собрал сервер из окружения (UA/платформа и т.п.)
|
||||
geo - строка "Country, City" или "unknown"
|
||||
lastAuthirificatedAtMs - время последней успешной авторизации/refresh (мс)
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "ListSessions"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- CloseActiveSession
|
||||
Закрывает одну активную сессию пользователя (указанную или текущую), при необходимости подтверждая владение ключом подписью.
|
||||
|
||||
Запрос:
|
||||
op - "CloseActiveSession"
|
||||
requestId - id запроса
|
||||
sessionId - идентификатор сессии для закрытия (если пусто, закрывается текущая)
|
||||
timeMs - время на клиенте в мс (нужно только если статус AUTH_IN_PROGRESS)
|
||||
signatureB64 - подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (нужно только если статус AUTH_IN_PROGRESS)
|
||||
|
||||
Ответ (успех):
|
||||
op - "CloseActiveSession"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "CloseActiveSession"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
Группа: Блокчейн (загрузка блоков)
|
||||
|
||||
Эта группа отвечает за приём и валидацию блоков (цепочка, линии, подпись/хэш) и атомарную запись в БД+файл.
|
||||
|
||||
----- AddBlock
|
||||
Добавляет следующий блок в конкретный blockchainName, строго проверяя номера, prev-хэши, линии, подпись и лимит размера.
|
||||
|
||||
Запрос:
|
||||
op - "AddBlock"
|
||||
requestId - id запроса
|
||||
blockchainName - имя цепочки (по нему вычисляется login)
|
||||
globalNumber - глобальный номер добавляемого блока (должен быть serverLastGlobalNumber+1)
|
||||
prevGlobalHash - HEX(64) предыдущего глобального хэша (или "" только там, где это допускается правилами)
|
||||
blockBytesB64 - полный блок (raw+signature+hash) в Base64
|
||||
|
||||
Ответ (успех):
|
||||
op - "AddBlock"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
reasonCode - null при успехе
|
||||
serverLastGlobalNumber - последний глобальный номер, который сервер считает актуальным после обработки
|
||||
serverLastGlobalHash - последний глобальный хэш (HEX(64)) после обработки
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "AddBlock"
|
||||
requestId - id запроса
|
||||
status - код ошибки (например 400/404/413/500)
|
||||
reasonCode - строка причины (например bad_block_base64, bad_global_number, limit_exceeded и т.п.)
|
||||
serverLastGlobalNumber - текущий серверный lastGlobalNumber
|
||||
serverLastGlobalHash - текущий серверный lastGlobalHash (HEX(64), если известен)
|
||||
|
||||
Группа: Параметры пользователя (UserParams)
|
||||
|
||||
Эта группа хранит и отдаёт подписанные клиентом параметры (ключ-значение) для синхронизации и состояния.
|
||||
|
||||
----- UpsertUserParam
|
||||
Добавляет или обновляет параметр пользователя, проверяя подпись Ed25519 и применяя запись только если time_ms новее.
|
||||
|
||||
Запрос:
|
||||
op - "UpsertUserParam"
|
||||
requestId - id запроса
|
||||
login - логин пользователя
|
||||
param - имя параметра
|
||||
time_ms - метка времени значения в мс
|
||||
value - значение параметра
|
||||
device_key - публичный ключ устройства, base64(32 bytes)
|
||||
signature - подпись Ed25519 от строки USER_PARAMETER_PREFIX + login + param + time_ms + value
|
||||
|
||||
Ответ (успех):
|
||||
op - "UpsertUserParam"
|
||||
requestId - id запроса
|
||||
status - 200 если успех
|
||||
|
||||
Ответ (ошибка):
|
||||
op - "UpsertUserParam"
|
||||
requestId - id запроса
|
||||
status - код ошибки
|
||||
code - строковый код ошибки
|
||||
message - человекочитаемое описание ошибки
|
||||
|
||||
----- GetUserParam
|
||||
Возвращает один сохранённый параметр пользователя.
|
||||
|
||||
Запрос:
|
||||
op - "GetUserParam"
|
||||
requestId - id запроса
|
||||
login - логин пользователя
|
||||
param - имя параметра
|
||||
|
||||
Ответ (успех):
|
||||
op - "GetUserParam"
|
||||
requestId - id запроса
|
||||
status - 200
|
||||
login
|
||||
param
|
||||
time_ms
|
||||
value
|
||||
device_key
|
||||
signature
|
||||
|
||||
Ответ (не найдено):
|
||||
op - "GetUserParam"
|
||||
requestId
|
||||
status - 404
|
||||
|
||||
Ответ (ошибка):
|
||||
op
|
||||
requestId
|
||||
status
|
||||
code
|
||||
message
|
||||
|
||||
----- ListUserParams
|
||||
Возвращает все сохранённые параметры пользователя.
|
||||
|
||||
Запрос:
|
||||
op - "ListUserParams"
|
||||
requestId
|
||||
login
|
||||
|
||||
Ответ (успех):
|
||||
op
|
||||
requestId
|
||||
status - 200
|
||||
login
|
||||
params - массив параметров
|
||||
|
||||
Поля params[i]:
|
||||
login
|
||||
param
|
||||
time_ms
|
||||
value
|
||||
device_key
|
||||
signature
|
||||
|
||||
Ответ (ошибка):
|
||||
op
|
||||
requestId
|
||||
status
|
||||
code
|
||||
message
|
||||
|
||||
Группа: Тестовые/временные операции
|
||||
|
||||
Эта группа предназначена для отладки и первичного наполнения БД.
|
||||
|
||||
----- AddUser
|
||||
Тестовая регистрация локального пользователя.
|
||||
|
||||
Запрос:
|
||||
op - "AddUser"
|
||||
requestId
|
||||
login
|
||||
blockchainName
|
||||
solanaKey
|
||||
deviceKey
|
||||
bchLimit
|
||||
|
||||
Ответ (успех):
|
||||
op
|
||||
requestId
|
||||
status - 200
|
||||
|
||||
Ответ (ошибка):
|
||||
op
|
||||
requestId
|
||||
status
|
||||
code
|
||||
message
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user