Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 214287193e | |||
| 9773a78f6b | |||
| e9c396df66 | |||
| 12e99f896a | |||
| efea28e5d6 | |||
| 364dc6b2f7 | |||
| 5eb9620468 | |||
| 6bf06c8867 | |||
| a4be0d73ae | |||
| 98ae52d3a8 | |||
| 0fb3c22f79 | |||
| ac9d6dc0d3 | |||
| fb800bb5bd | |||
| e5233efdf3 | |||
| ca55f16085 | |||
| 3c6acf47a5 | |||
| 23e969c1b6 |
@@ -12,7 +12,7 @@ jobs:
|
|||||||
Build:
|
Build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
app_name: [Brainfuck, Breakout, Calculator, Diceware, EpubReader, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
|
app_name: [Brainfuck, Breakout, BookPlayer, Calculator, Diceware, EpubReader, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(AudioTest)
|
||||||
|
tactility_project(AudioTest)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
/**
|
||||||
|
* Audio Test - Record & Playback
|
||||||
|
*
|
||||||
|
* Records from the ES8311 microphone via I2S and plays it back through
|
||||||
|
* the FM8002E speaker amplifier. Uses the Tactility kernel I2S controller API.
|
||||||
|
*
|
||||||
|
* UI: Two buttons - Record and Play
|
||||||
|
* Record: holds recording for up to ~3 seconds (16kHz 16-bit mono ≈ 96KB)
|
||||||
|
* Play: plays back the last recorded buffer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/drivers/i2s_controller.h>
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
#define TAG "AudioTest"
|
||||||
|
|
||||||
|
/* ─── Audio params ─── */
|
||||||
|
#define SAMPLE_RATE 16000
|
||||||
|
#define BITS_PER_SAMPLE 16
|
||||||
|
#define RECORD_SECONDS 3
|
||||||
|
|
||||||
|
/* 16-bit mono → 2 bytes per sample */
|
||||||
|
#define AUDIO_BUF_SIZE (SAMPLE_RATE * (BITS_PER_SAMPLE / 8) * RECORD_SECONDS)
|
||||||
|
|
||||||
|
/* Chunk size for read/write calls (512 samples = 1024 bytes at 16-bit) */
|
||||||
|
#define CHUNK_SAMPLES 512
|
||||||
|
#define CHUNK_BYTES (CHUNK_SAMPLES * (BITS_PER_SAMPLE / 8))
|
||||||
|
|
||||||
|
/* ─── App state ─── */
|
||||||
|
typedef enum {
|
||||||
|
STATE_IDLE,
|
||||||
|
STATE_RECORDING,
|
||||||
|
STATE_PLAYING
|
||||||
|
} AudioState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
struct Device* i2s_dev;
|
||||||
|
uint8_t* audio_buf;
|
||||||
|
size_t recorded_bytes;
|
||||||
|
AudioState state;
|
||||||
|
lv_obj_t* btn_record;
|
||||||
|
lv_obj_t* btn_play;
|
||||||
|
lv_obj_t* lbl_status;
|
||||||
|
lv_obj_t* bar_progress;
|
||||||
|
TaskHandle_t task_handle;
|
||||||
|
} AppCtx;
|
||||||
|
|
||||||
|
/* ─── Forward decls ─── */
|
||||||
|
static void record_task(void* arg);
|
||||||
|
static void play_task(void* arg);
|
||||||
|
static void update_ui(AppCtx* ctx);
|
||||||
|
|
||||||
|
/* ─── Callbacks ─── */
|
||||||
|
static void on_record_click(lv_event_t* e) {
|
||||||
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
|
if (ctx->state != STATE_IDLE) return;
|
||||||
|
if (ctx->i2s_dev == NULL) return;
|
||||||
|
|
||||||
|
ctx->state = STATE_RECORDING;
|
||||||
|
ctx->recorded_bytes = 0;
|
||||||
|
update_ui(ctx);
|
||||||
|
|
||||||
|
xTaskCreate(record_task, "audio_rec", 4096, ctx, 5, &ctx->task_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_play_click(lv_event_t* e) {
|
||||||
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
|
if (ctx->state != STATE_IDLE) return;
|
||||||
|
if (ctx->recorded_bytes == 0) return;
|
||||||
|
if (ctx->i2s_dev == NULL) return;
|
||||||
|
|
||||||
|
ctx->state = STATE_PLAYING;
|
||||||
|
update_ui(ctx);
|
||||||
|
|
||||||
|
xTaskCreate(play_task, "audio_play", 4096, ctx, 5, &ctx->task_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Update UI labels/buttons from any context ─── */
|
||||||
|
static void update_ui(AppCtx* ctx) {
|
||||||
|
if (ctx->lbl_status == NULL) return;
|
||||||
|
|
||||||
|
switch (ctx->state) {
|
||||||
|
case STATE_RECORDING:
|
||||||
|
lv_label_set_text(ctx->lbl_status, "🔴 Recording...");
|
||||||
|
lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED);
|
||||||
|
lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED);
|
||||||
|
break;
|
||||||
|
case STATE_PLAYING:
|
||||||
|
lv_label_set_text(ctx->lbl_status, "🔊 Playing...");
|
||||||
|
lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED);
|
||||||
|
lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED);
|
||||||
|
break;
|
||||||
|
case STATE_IDLE:
|
||||||
|
default:
|
||||||
|
if (ctx->recorded_bytes > 0) {
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), "Ready (%u bytes recorded)", (unsigned)ctx->recorded_bytes);
|
||||||
|
lv_label_set_text(ctx->lbl_status, buf);
|
||||||
|
} else {
|
||||||
|
lv_label_set_text(ctx->lbl_status, "Press Record to capture audio");
|
||||||
|
}
|
||||||
|
lv_obj_clear_state(ctx->btn_record, LV_STATE_DISABLED);
|
||||||
|
if (ctx->recorded_bytes > 0) {
|
||||||
|
lv_obj_clear_state(ctx->btn_play, LV_STATE_DISABLED);
|
||||||
|
} else {
|
||||||
|
lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Record task ─── */
|
||||||
|
static void record_task(void* arg) {
|
||||||
|
AppCtx* ctx = (AppCtx*)arg;
|
||||||
|
size_t total = 0;
|
||||||
|
size_t bytes_read = 0;
|
||||||
|
|
||||||
|
/* Configure I2S for recording */
|
||||||
|
struct I2sConfig cfg = {
|
||||||
|
.communication_format = I2S_FORMAT_STAND_I2S,
|
||||||
|
.sample_rate = SAMPLE_RATE,
|
||||||
|
.bits_per_sample = BITS_PER_SAMPLE,
|
||||||
|
.channel_left = 0,
|
||||||
|
.channel_right = I2S_CHANNEL_NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
device_lock(ctx->i2s_dev);
|
||||||
|
error_t err = i2s_controller_set_config(ctx->i2s_dev, &cfg);
|
||||||
|
device_unlock(ctx->i2s_dev);
|
||||||
|
|
||||||
|
if (err != ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "Failed to set I2S config for recording: %d", err);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
ctx->state = STATE_IDLE;
|
||||||
|
update_ui(ctx);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Recording started (max %d bytes)", AUDIO_BUF_SIZE);
|
||||||
|
|
||||||
|
while (total < AUDIO_BUF_SIZE && ctx->state == STATE_RECORDING) {
|
||||||
|
size_t remaining = AUDIO_BUF_SIZE - total;
|
||||||
|
size_t to_read = (remaining < CHUNK_BYTES) ? remaining : CHUNK_BYTES;
|
||||||
|
|
||||||
|
err = i2s_controller_read(ctx->i2s_dev,
|
||||||
|
ctx->audio_buf + total,
|
||||||
|
to_read,
|
||||||
|
&bytes_read,
|
||||||
|
pdMS_TO_TICKS(1000));
|
||||||
|
if (err != ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "I2S read error: %d", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
total += bytes_read;
|
||||||
|
|
||||||
|
/* Update progress bar */
|
||||||
|
int pct = (int)((total * 100) / AUDIO_BUF_SIZE);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->recorded_bytes = total;
|
||||||
|
ctx->state = STATE_IDLE;
|
||||||
|
ctx->task_handle = NULL;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Recording complete: %u bytes", (unsigned)total);
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
lv_bar_set_value(ctx->bar_progress, 0, LV_ANIM_OFF);
|
||||||
|
update_ui(ctx);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Play task ─── */
|
||||||
|
static void play_task(void* arg) {
|
||||||
|
AppCtx* ctx = (AppCtx*)arg;
|
||||||
|
size_t total = 0;
|
||||||
|
size_t bytes_written = 0;
|
||||||
|
|
||||||
|
/* Configure I2S for playback */
|
||||||
|
struct I2sConfig cfg = {
|
||||||
|
.communication_format = I2S_FORMAT_STAND_I2S,
|
||||||
|
.sample_rate = SAMPLE_RATE,
|
||||||
|
.bits_per_sample = BITS_PER_SAMPLE,
|
||||||
|
.channel_left = 0,
|
||||||
|
.channel_right = I2S_CHANNEL_NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
device_lock(ctx->i2s_dev);
|
||||||
|
error_t err = i2s_controller_set_config(ctx->i2s_dev, &cfg);
|
||||||
|
device_unlock(ctx->i2s_dev);
|
||||||
|
|
||||||
|
if (err != ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "Failed to set I2S config for playback: %d", err);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
ctx->state = STATE_IDLE;
|
||||||
|
update_ui(ctx);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Playback started (%u bytes)", (unsigned)ctx->recorded_bytes);
|
||||||
|
|
||||||
|
while (total < ctx->recorded_bytes && ctx->state == STATE_PLAYING) {
|
||||||
|
size_t remaining = ctx->recorded_bytes - total;
|
||||||
|
size_t to_write = (remaining < CHUNK_BYTES) ? remaining : CHUNK_BYTES;
|
||||||
|
|
||||||
|
err = i2s_controller_write(ctx->i2s_dev,
|
||||||
|
ctx->audio_buf + total,
|
||||||
|
to_write,
|
||||||
|
&bytes_written,
|
||||||
|
pdMS_TO_TICKS(1000));
|
||||||
|
if (err != ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "I2S write error: %d", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
total += bytes_written;
|
||||||
|
|
||||||
|
/* Update progress bar */
|
||||||
|
int pct = (int)((total * 100) / ctx->recorded_bytes);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->state = STATE_IDLE;
|
||||||
|
ctx->task_handle = NULL;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Playback complete");
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
lv_bar_set_value(ctx->bar_progress, 0, LV_ANIM_OFF);
|
||||||
|
update_ui(ctx);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── App lifecycle ─── */
|
||||||
|
static AppCtx g_ctx;
|
||||||
|
|
||||||
|
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
||||||
|
memset(&g_ctx, 0, sizeof(g_ctx));
|
||||||
|
|
||||||
|
/* Allocate audio buffer */
|
||||||
|
g_ctx.audio_buf = (uint8_t*)malloc(AUDIO_BUF_SIZE);
|
||||||
|
if (g_ctx.audio_buf == NULL) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate audio buffer (%d bytes)", AUDIO_BUF_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find I2S device */
|
||||||
|
g_ctx.i2s_dev = device_find_by_name("i2s0");
|
||||||
|
if (g_ctx.i2s_dev == NULL) {
|
||||||
|
ESP_LOGE(TAG, "I2S device 'i2s0' not found!");
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Found I2S device: %s", g_ctx.i2s_dev->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── UI ─── */
|
||||||
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||||
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
|
/* Status label */
|
||||||
|
g_ctx.lbl_status = lv_label_create(parent);
|
||||||
|
lv_label_set_text(g_ctx.lbl_status, "Initializing...");
|
||||||
|
lv_obj_set_width(g_ctx.lbl_status, lv_pct(90));
|
||||||
|
lv_label_set_long_mode(g_ctx.lbl_status, LV_LABEL_LONG_WRAP);
|
||||||
|
lv_obj_set_style_text_align(g_ctx.lbl_status, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
|
lv_obj_align(g_ctx.lbl_status, LV_ALIGN_TOP_MID, 0, 50);
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
g_ctx.bar_progress = lv_bar_create(parent);
|
||||||
|
lv_obj_set_size(g_ctx.bar_progress, lv_pct(80), 10);
|
||||||
|
lv_bar_set_range(g_ctx.bar_progress, 0, 100);
|
||||||
|
lv_bar_set_value(g_ctx.bar_progress, 0, LV_ANIM_OFF);
|
||||||
|
lv_obj_align(g_ctx.bar_progress, LV_ALIGN_CENTER, 0, -10);
|
||||||
|
|
||||||
|
/* Button container */
|
||||||
|
lv_obj_t* btn_container = lv_obj_create(parent);
|
||||||
|
lv_obj_remove_style_all(btn_container);
|
||||||
|
lv_obj_set_size(btn_container, lv_pct(90), 50);
|
||||||
|
lv_obj_set_flex_flow(btn_container, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(btn_container, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_align(btn_container, LV_ALIGN_CENTER, 0, 30);
|
||||||
|
|
||||||
|
/* Record button */
|
||||||
|
g_ctx.btn_record = lv_btn_create(btn_container);
|
||||||
|
lv_obj_set_size(g_ctx.btn_record, 100, 40);
|
||||||
|
lv_obj_t* lbl_rec = lv_label_create(g_ctx.btn_record);
|
||||||
|
lv_label_set_text(lbl_rec, LV_SYMBOL_AUDIO " Record");
|
||||||
|
lv_obj_center(lbl_rec);
|
||||||
|
lv_obj_add_event_cb(g_ctx.btn_record, on_record_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
|
/* Play button */
|
||||||
|
g_ctx.btn_play = lv_btn_create(btn_container);
|
||||||
|
lv_obj_set_size(g_ctx.btn_play, 100, 40);
|
||||||
|
lv_obj_t* lbl_play = lv_label_create(g_ctx.btn_play);
|
||||||
|
lv_label_set_text(lbl_play, LV_SYMBOL_PLAY " Play");
|
||||||
|
lv_obj_center(lbl_play);
|
||||||
|
lv_obj_add_event_cb(g_ctx.btn_play, on_play_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
|
/* Check initialization status */
|
||||||
|
if (g_ctx.i2s_dev == NULL) {
|
||||||
|
lv_label_set_text(g_ctx.lbl_status, "ERROR: I2S device not found");
|
||||||
|
lv_obj_add_state(g_ctx.btn_record, LV_STATE_DISABLED);
|
||||||
|
lv_obj_add_state(g_ctx.btn_play, LV_STATE_DISABLED);
|
||||||
|
} else if (g_ctx.audio_buf == NULL) {
|
||||||
|
lv_label_set_text(g_ctx.lbl_status, "ERROR: Out of memory");
|
||||||
|
lv_obj_add_state(g_ctx.btn_record, LV_STATE_DISABLED);
|
||||||
|
lv_obj_add_state(g_ctx.btn_play, LV_STATE_DISABLED);
|
||||||
|
} else {
|
||||||
|
g_ctx.state = STATE_IDLE;
|
||||||
|
update_ui(&g_ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
tt_app_register((AppRegistration) {
|
||||||
|
.onShow = onShowApp
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.7.0-dev
|
||||||
|
platforms=esp32s3
|
||||||
|
[app]
|
||||||
|
id=one.tactility.audiotest
|
||||||
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=Audio Test
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(BibleVerse)
|
||||||
|
tactility_project(BibleVerse)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# BibleVerse for Tactility
|
||||||
|
|
||||||
|
Single-verse bible reader. Shows one verse fullscreen, auto-advances every minute. Tap to toggle controls.
|
||||||
|
|
||||||
|
## UX
|
||||||
|
|
||||||
|
- **Default view (no chrome)**: immersive context — just the verse text + reference, centered, large font. Like BookPlayer image-only view.
|
||||||
|
- **Tap**: shows header (current ref + position) and footer controls:
|
||||||
|
- Close X, Prev, Pause/Play (auto-advance), Next, Highlight (star), Audio memo placeholder.
|
||||||
|
- Highlighted verses saved to `favorites.txt` in app user data. Long-term: record voice memo, read aloud TTS.
|
||||||
|
|
||||||
|
## Bible data format (split per book)
|
||||||
|
|
||||||
|
We fetch public domain bibles from https://github.com/sajeevavahini/bibles (Zefania XML).
|
||||||
|
|
||||||
|
Offline conversion:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 tools/convert_bible.py --src "World English Bible.xml" --dst assets/bible
|
||||||
|
```
|
||||||
|
|
||||||
|
Result structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/bible/
|
||||||
|
books.json # {"books":[{"bnumber":1,"bname":"Genesis","verses":1533,...}], total_verses, translation, version}
|
||||||
|
01.bin / 01.idx # Genesis
|
||||||
|
02.bin / 02.idx # Exodus
|
||||||
|
...
|
||||||
|
66.bin / 66.idx
|
||||||
|
```
|
||||||
|
|
||||||
|
- `.bin`: concatenated NUL-terminated UTF8 verse texts. No parsing needed, low RAM.
|
||||||
|
- `.idx`: header 10 bytes: magic 'BIBK', uint16 version, uint32 count. Then per entry 8 bytes: uint32 offset, uint16 cnum, uint16 vnum.
|
||||||
|
|
||||||
|
This split makes memory footprint tiny for ESP32:
|
||||||
|
- Only one book loaded at a time (max ~215KB for Psalms, typical 12-100KB).
|
||||||
|
- Index ~19KB max for Psalms, typically <5KB.
|
||||||
|
|
||||||
|
## SD card fallback
|
||||||
|
|
||||||
|
App also scans:
|
||||||
|
|
||||||
|
- `/sdcard/bibles/` and `/sdcard/bible/`
|
||||||
|
|
||||||
|
If `books.json` not found in assets, it will try to use SD folder. This lets users drop extra translations offline.
|
||||||
|
|
||||||
|
## Settings persistence
|
||||||
|
|
||||||
|
Same pattern as EpubReader / BookPlayer (but those two didn't persist in BookPlayer, EpubReader does):
|
||||||
|
|
||||||
|
Using Tactility app user data paths:
|
||||||
|
|
||||||
|
- `tt_app_get_user_data_path()` -> e.g. `/sdcard/user/app/one.tactility.bibleverse`
|
||||||
|
- `progress.txt`: stores global verse index + b/c/v for debug
|
||||||
|
```
|
||||||
|
1234
|
||||||
|
1 1 1
|
||||||
|
0 0
|
||||||
|
```
|
||||||
|
- `favorites.txt`: list of global indices, one per line.
|
||||||
|
|
||||||
|
This lives on SD card (or internal data partition), survives app reinstalls.
|
||||||
|
|
||||||
|
## Build (color screen device, esp32s3)
|
||||||
|
|
||||||
|
Same as BookPlayer — external app, compiled for esp32s3 target (covers CYD-2432s028r, m5stack-cores3, etc).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# source IDF (example)
|
||||||
|
source ~/esp/esp-idf/export.sh
|
||||||
|
export TACTILITY_SDK_PATH=/path/to/TactilitySDK # or use --local-sdk
|
||||||
|
|
||||||
|
python3 tactility.py Apps/BibleVerse build esp32s3 --local-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `build/BibleVerse.app` installable via Tactility file manager or `tactility.py ... install <ip>`
|
||||||
|
|
||||||
|
## Future extensions
|
||||||
|
|
||||||
|
- Voice memo: record to `/sdcard/bible_memos/<global>.wav` using same I2S code as BookPlayer
|
||||||
|
- TTS read aloud: use esp tts or pre-generated mp3 like BookPlayer pages
|
||||||
|
- Book picker UI (like BookPlayer picker but for bible books)
|
||||||
|
- Highlight color, search, jump to reference dialog
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user