diff --git a/Cargo.toml b/Cargo.toml
index 2b65f8d..cd7da48 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "echokit"
-version = "0.2.2"
+version = "0.3.0"
authors = ["csh <458761603@qq.com>"]
edition = "2021"
resolver = "2"
@@ -26,7 +26,7 @@ experimental = ["esp-idf-svc/experimental"]
boards = ["voice_interrupt"]
_no_default = []
-box = ["_no_default", "voice_interrupt"]
+box = ["_no_default", "voice_interrupt", "custom_ui"]
cube = ["_no_default", "voice_interrupt"]
cube2 = ["_no_default", "voice_interrupt"]
nfc_cube2 = ["cube2", "mfrc522", "exio"]
@@ -38,6 +38,7 @@ extra_server = []
i2c = []
voice_interrupt = []
+custom_ui = []
[dependencies]
log = "0.4"
@@ -53,14 +54,17 @@ serde_json = "1.0"
rmp-serde = "1"
esp32-nimble = "0.11.1"
-# embedded-websocket = { version = "0.9.4" }
+
+# UI libraries
embedded-graphics = "0.8.1"
embedded-text = "0.7.2"
-# async-io = "2.4.0"
-
u8g2-fonts = { version = "0.6.0", features = ["embedded_graphics_textstyle"] }
-tinygif = "0.0.4"
-# futures-lite = "2.6.0"
+image = { version = "0.25.6", default-features = false, features = [
+ "png",
+ "gif",
+ "webp",
+] }
+
futures-util = { version = "0.3.31", features = ["sink"] }
# futures-sink = "0.3.31"
diff --git a/assets/96x96.png b/assets/96x96.png
new file mode 100755
index 0000000..9e44b4c
Binary files /dev/null and b/assets/96x96.png differ
diff --git a/assets/avatar.gif b/assets/avatar.gif
new file mode 100755
index 0000000..14b2043
Binary files /dev/null and b/assets/avatar.gif differ
diff --git a/assets/lm_320x240.png b/assets/lm_320x240.png
new file mode 100755
index 0000000..45eace3
Binary files /dev/null and b/assets/lm_320x240.png differ
diff --git a/assets/xx.gif b/assets/xx.gif
new file mode 100644
index 0000000..f759970
Binary files /dev/null and b/assets/xx.gif differ
diff --git a/components/hal_driver/lcd.c b/components/hal_driver/lcd.c
index 0aa2b7d..0ccab51 100644
--- a/components/hal_driver/lcd.c
+++ b/components/hal_driver/lcd.c
@@ -24,6 +24,8 @@
static const char *TAG = "LCD";
esp_lcd_panel_handle_t panel_handle = NULL; /* LCD句柄 */
+uint16_t *lcd_dma_buffer = NULL;
+
uint32_t g_back_color = 0xFFFF;
lcd_obj_t lcd_dev;
@@ -129,23 +131,23 @@ void lcd_color_fill(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, uint16_t
uint16_t height = ey - sy;
uint32_t buf_index = 0;
- uint16_t *buffer = heap_caps_malloc(width * sizeof(uint16_t), MALLOC_CAP_INTERNAL);
+ // uint16_t *buffer = heap_caps_malloc(width * sizeof(uint16_t), MALLOC_CAP_INTERNAL);
for (uint16_t y_index = 0; y_index < height; y_index++)
{
for (uint16_t x_index = 0; x_index < width; x_index++)
{
- buffer[x_index] = color[buf_index];
+ lcd_dma_buffer[x_index] = color[buf_index];
buf_index++;
}
for (uint16_t i = 0; i < width; i += 80)
{
- esp_lcd_panel_draw_bitmap(panel_handle, sx + i, sy + y_index, sx + i + 80, sy + y_index + 1, &buffer[i]);
+ esp_lcd_panel_draw_bitmap(panel_handle, sx + i, sy + y_index, sx + i + 80, sy + y_index + 1, &lcd_dma_buffer[i]);
}
}
/* 释放内存 */
- heap_caps_free(buffer);
+ // heap_caps_free(buffer);
}
/**
@@ -284,7 +286,8 @@ void lcd_init(lcd_cfg_t lcd_config)
},
.bus_width = 8,
.max_transfer_bytes = lcd_dev.pwidth * lcd_dev.pheight * sizeof(uint16_t),
- .psram_trans_align = 64,
+ // .psram_trans_align = 64,
+ .dma_burst_size = 64,
.sram_trans_align = 4,
};
ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); /* 新建80并口总线 */
@@ -293,7 +296,7 @@ void lcd_init(lcd_cfg_t lcd_config)
/* 80并口配置 */
.cs_gpio_num = lcd_dev.cs,
.pclk_hz = (10 * 1000 * 1000),
- .trans_queue_depth = 10,
+ .trans_queue_depth = 15,
.dc_levels = {
.dc_idle_level = 0,
.dc_cmd_level = 0,
@@ -327,4 +330,6 @@ void lcd_init(lcd_cfg_t lcd_config)
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); /* 启动屏幕 */
lcd_clear(WHITE); /* 默认填充白色 */
LCD_BL(1); /* 打开背光 */
+
+ lcd_dma_buffer = esp_lcd_i80_alloc_draw_buffer(io_handle, lcd_dev.pwidth * sizeof(uint16_t), MALLOC_CAP_DMA);
}
diff --git a/components/hal_driver/lcd.h b/components/hal_driver/lcd.h
index 4c4c1f2..c619f0d 100644
--- a/components/hal_driver/lcd.h
+++ b/components/hal_driver/lcd.h
@@ -104,6 +104,7 @@ typedef struct _lcd_config_t
/* 导出相关变量 */
extern lcd_obj_t lcd_dev;
extern esp_lcd_panel_handle_t panel_handle; /* LCD句柄 */
+extern uint16_t *lcd_dma_buffer;
/* lcd相关函数 */
void lcd_init(lcd_cfg_t lcd_config); /* 初始化lcd */
void lcd_clear(uint16_t color); /* 清除屏幕 */
diff --git a/sdkconfig.defaults b/sdkconfig.defaults
index 7a44a6c..395f0e4 100644
--- a/sdkconfig.defaults
+++ b/sdkconfig.defaults
@@ -46,7 +46,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
-CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=7000
+#CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=7000
#CONFIG_BT_NIMBLE_NVS_PERSIST=y
diff --git a/setup/index.html b/setup/index.html
index ff002cb..bc1dbf6 100644
--- a/setup/index.html
+++ b/setup/index.html
@@ -92,6 +92,75 @@
Background Image
+
+
+
+
+
+ 🛠️ Advanced Settings
+
+
+
+
+
+
+
AFE Linear Gain
+
+
+
+
+
+
+
+
+
AGC Target Level
+
+
+
+
+
+
+
+
+
AGC Compression
+ Gain
+
+
+
+
+
+
+
@@ -125,6 +194,9 @@ ⚠️ Device Reset Required
const SERVER_URL_ID = "cef520a9-bcb5-4fc6-87f7-82804eee2b20";
const BACKGROUND_IMAGE_ID = "d1f3b2c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d";
const RESET_ID = "f0e1d2c3-b4a5-6789-0abc-def123456789";
+ const AFE_LINEAR_GAIN_ID = "a1b2c3d4-e5f6-4789-0abc-def123456789";
+ const AGC_TARGET_LEVEL_ID = "b2c3d4e5-f6a7-4890-1bcd-ef2345678901";
+ const AGC_COMPRESSION_GAIN_ID = "c3d4e5f6-a7b8-4901-2cde-f34567890123";
// global variables
let device = null;
@@ -156,6 +228,22 @@ ⚠️ Device Reset Required
const toastMessage = document.getElementById('toastMessage');
const resetNotSupportedModal = document.getElementById('resetNotSupportedModal');
+ // AFE related DOM elements
+ const afeLinearGainRange = document.getElementById('afeLinearGainRange');
+ const afeLinearGainValue = document.getElementById('afeLinearGainValue');
+ const saveAfeLinearGainButton = document.getElementById('saveAfeLinearGainButton');
+ const afeLinearGainTitle = document.getElementById('afeLinearGainTitle');
+
+ const agcTargetLevelRange = document.getElementById('agcTargetLevelRange');
+ const agcTargetLevelValue = document.getElementById('agcTargetLevelValue');
+ const saveAgcTargetLevelButton = document.getElementById('saveAgcTargetLevelButton');
+ const agcTargetLevelTitle = document.getElementById('agcTargetLevelTitle');
+
+ const agcCompressionGainRange = document.getElementById('agcCompressionGainRange');
+ const agcCompressionGainValue = document.getElementById('agcCompressionGainValue');
+ const saveAgcCompressionGainButton = document.getElementById('saveAgcCompressionGainButton');
+ const agcCompressionGainTitle = document.getElementById('agcCompressionGainTitle');
+
// Track modified fields
const modifiedFields = {
ssid: false,
@@ -175,7 +263,9 @@ ⚠️ Device Reset Required
// Clear field modification mark
function clearFieldModification(fieldName, titleElement) {
modifiedFields[fieldName] = false;
- titleElement.textContent = titleElement.textContent.replace(' *', '');
+ if (titleElement) {
+ titleElement.textContent = titleElement.textContent.replace(' *', '');
+ }
updateSaveButtonState();
}
@@ -335,6 +425,11 @@ ⚠️ Device Reset Required
backgroundImage.disabled = false;
clearBgButton.disabled = false;
controlPanel.classList.remove('opacity-50', 'pointer-events-none');
+
+ // Enable AFE controls
+ afeLinearGainRange.disabled = false;
+ agcTargetLevelRange.disabled = false;
+ agcCompressionGainRange.disabled = false;
}
// Disable all controls
@@ -348,6 +443,14 @@ ⚠️ Device Reset Required
writeBgButton.disabled = true;
clearBgButton.disabled = true;
controlPanel.classList.add('opacity-50', 'pointer-events-none');
+
+ // Disable AFE controls
+ afeLinearGainRange.disabled = true;
+ agcTargetLevelRange.disabled = true;
+ agcCompressionGainRange.disabled = true;
+ saveAfeLinearGainButton.disabled = true;
+ saveAgcTargetLevelButton.disabled = true;
+ saveAgcCompressionGainButton.disabled = true;
}
// Reads Characteristic
@@ -388,6 +491,11 @@ ⚠️ Device Reset Required
await readCharacteristic(PASS_ID, passInput);
await readCharacteristic(SERVER_URL_ID, serverUrlInput);
+ // Load AFE parameters
+ await readAfeLinearGain();
+ await readAgcTargetLevel();
+ await readAgcCompressionGain();
+
// Clear all modification marks
clearFieldModification('ssid', ssidTitle);
clearFieldModification('pass', passTitle);
@@ -614,6 +722,173 @@ ⚠️ Device Reset Required
showNotification('Message', 'Background image cleared');
});
+ // Read AFE Linear Gain (string format f32)
+ async function readAfeLinearGain() {
+ if (!isConnected || !service) return false;
+ try {
+ const characteristic = await service.getCharacteristic(AFE_LINEAR_GAIN_ID);
+ const value = await characteristic.readValue();
+ const decoder = new TextDecoder();
+ const stringValue = decoder.decode(value);
+ const gain = parseFloat(stringValue);
+ if (!isNaN(gain)) {
+ afeLinearGainRange.value = Math.round(gain * 10);
+ afeLinearGainValue.textContent = gain.toFixed(1);
+ // Reset title (remove not supported label)
+ afeLinearGainTitle.textContent = 'AFE Linear Gain';
+ saveAfeLinearGainButton.disabled = true;
+ }
+ return true;
+ } catch (error) {
+ console.error('Failed to read AFE Linear Gain:', error);
+ // Backward compatibility: disable this control
+ afeLinearGainRange.disabled = true;
+ saveAfeLinearGainButton.disabled = true;
+ afeLinearGainTitle.textContent = 'AFE Linear Gain (Not Supported)';
+ return false;
+ }
+ }
+
+ // Read AGC Target Level (i32, 4 bytes little endian)
+ async function readAgcTargetLevel() {
+ if (!isConnected || !service) return false;
+ try {
+ const characteristic = await service.getCharacteristic(AGC_TARGET_LEVEL_ID);
+ const value = await characteristic.readValue();
+ // value is already DataView, use directly
+ const level = value.getInt32(0, true);
+ agcTargetLevelRange.value = level;
+ agcTargetLevelValue.textContent = level;
+ // Reset title (remove not supported label)
+ agcTargetLevelTitle.textContent = 'AGC Target Level';
+ saveAgcTargetLevelButton.disabled = true;
+ return true;
+ } catch (error) {
+ console.error('Failed to read AGC Target Level:', error);
+ // Backward compatibility: disable this control
+ agcTargetLevelRange.disabled = true;
+ saveAgcTargetLevelButton.disabled = true;
+ agcTargetLevelTitle.textContent = 'AGC Target Level (Not Supported)';
+ return false;
+ }
+ }
+
+ // Read AGC Compression Gain (i32, 4 bytes little endian)
+ async function readAgcCompressionGain() {
+ if (!isConnected || !service) return false;
+ try {
+ const characteristic = await service.getCharacteristic(AGC_COMPRESSION_GAIN_ID);
+ const value = await characteristic.readValue();
+ // value is already DataView, use directly
+ const gain = value.getInt32(0, true);
+ agcCompressionGainRange.value = gain;
+ agcCompressionGainValue.textContent = gain;
+ // Reset title (remove not supported label)
+ agcCompressionGainTitle.textContent = 'AGC Compression Gain';
+ saveAgcCompressionGainButton.disabled = true;
+ return true;
+ } catch (error) {
+ console.error('Failed to read AGC Compression Gain:', error);
+ // Backward compatibility: disable this control
+ agcCompressionGainRange.disabled = true;
+ saveAgcCompressionGainButton.disabled = true;
+ agcCompressionGainTitle.textContent = 'AGC Compression Gain (Not Supported)';
+ return false;
+ }
+ }
+
+ // AFE Linear Gain save function
+ async function saveAfeLinearGain() {
+ if (!isConnected || !service) {
+ showNotification('Error', 'Device not connected', true);
+ return;
+ }
+ try {
+ const gain = parseFloat((afeLinearGainRange.value / 10).toFixed(1));
+ const characteristic = await service.getCharacteristic(AFE_LINEAR_GAIN_ID);
+ const encoder = new TextEncoder();
+ const data = encoder.encode(gain.toString());
+ await characteristic.writeValue(data);
+ showNotification('Success', `AFE Linear Gain set to ${gain}`);
+ clearFieldModification('afeLinearGain', afeLinearGainTitle);
+ saveAfeLinearGainButton.disabled = true;
+ } catch (error) {
+ console.error('Failed to save AFE Linear Gain:', error);
+ showNotification('Error', 'Failed to save AFE Linear Gain: ' + error.message, true);
+ }
+ }
+
+ // AGC Target Level save function
+ async function saveAgcTargetLevel() {
+ if (!isConnected || !service) {
+ showNotification('Error', 'Device not connected', true);
+ return;
+ }
+ try {
+ const level = parseInt(agcTargetLevelRange.value);
+ const characteristic = await service.getCharacteristic(AGC_TARGET_LEVEL_ID);
+ const data = new ArrayBuffer(4);
+ const view = new DataView(data);
+ view.setInt32(0, level, true);
+ await characteristic.writeValue(data);
+ showNotification('Success', `AGC Target Level set to ${level}`);
+ clearFieldModification('agcTargetLevel', agcTargetLevelTitle);
+ saveAgcTargetLevelButton.disabled = true;
+ } catch (error) {
+ console.error('Failed to save AGC Target Level:', error);
+ showNotification('Error', 'Failed to save AGC Target Level: ' + error.message, true);
+ }
+ }
+
+ // AGC Compression Gain save function
+ async function saveAgcCompressionGain() {
+ if (!isConnected || !service) {
+ showNotification('Error', 'Device not connected', true);
+ return;
+ }
+ try {
+ const gain = parseInt(agcCompressionGainRange.value);
+ const characteristic = await service.getCharacteristic(AGC_COMPRESSION_GAIN_ID);
+ const data = new ArrayBuffer(4);
+ const view = new DataView(data);
+ view.setInt32(0, gain, true);
+ await characteristic.writeValue(data);
+ showNotification('Success', `AGC Compression Gain set to ${gain}`);
+ clearFieldModification('agcCompressionGain', agcCompressionGainTitle);
+ saveAgcCompressionGainButton.disabled = true;
+ } catch (error) {
+ console.error('Failed to save AGC Compression Gain:', error);
+ showNotification('Error', 'Failed to save AGC Compression Gain: ' + error.message, true);
+ }
+ }
+
+ // AFE Linear Gain slider event
+ afeLinearGainRange.addEventListener('input', () => {
+ const gain = (afeLinearGainRange.value / 10).toFixed(1);
+ afeLinearGainValue.textContent = gain;
+ markFieldAsModified('afeLinearGain', afeLinearGainTitle);
+ saveAfeLinearGainButton.disabled = false;
+ });
+
+ // AGC Target Level slider event
+ agcTargetLevelRange.addEventListener('input', () => {
+ agcTargetLevelValue.textContent = agcTargetLevelRange.value;
+ markFieldAsModified('agcTargetLevel', agcTargetLevelTitle);
+ saveAgcTargetLevelButton.disabled = false;
+ });
+
+ // AGC Compression Gain slider event
+ agcCompressionGainRange.addEventListener('input', () => {
+ agcCompressionGainValue.textContent = agcCompressionGainRange.value;
+ markFieldAsModified('agcCompressionGain', agcCompressionGainTitle);
+ saveAgcCompressionGainButton.disabled = false;
+ });
+
+ // AFE save button events
+ saveAfeLinearGainButton.addEventListener('click', saveAfeLinearGain);
+ saveAgcTargetLevelButton.addEventListener('click', saveAgcTargetLevel);
+ saveAgcCompressionGainButton.addEventListener('click', saveAgcCompressionGain);
+
if (!navigator.bluetooth) {
showNotification('Error', 'Your browser does not support the Web Bluetooth API. Please use Chrome or Edge', true);
connectButton.disabled = true;
@@ -624,4 +899,4 @@ ⚠️ Device Reset Required