feat: integrate MCP and ES3C28P audio support with upstream compatibility updates
This commit is contained in:
@@ -5,8 +5,12 @@
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dirent.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
@@ -14,6 +18,17 @@
|
||||
#include <esp_heap_caps.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <mbedtls/base64.h>
|
||||
#include <Tactility/hal/power/PowerDevice.h>
|
||||
#include <tactility/hal/Device.h>
|
||||
#include <Tactility/bluetooth/Bluetooth.h>
|
||||
#include <tactility/drivers/bluetooth.h>
|
||||
#include <Tactility/network/Http.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <cJSON.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
@@ -124,7 +139,7 @@ bool clearScreen(int color) {
|
||||
if (lvgl::lock(pdMS_TO_TICKS(1500))) {
|
||||
if (display_ready(state)) {
|
||||
lv_obj_clean(state.drawArea);
|
||||
uint16_t fill = color == 1 ? 0x0000 : 0xFFFF;
|
||||
uint16_t fill = color == 1 ? 0xFFFF : 0x0000;
|
||||
size_t pixel_count = (size_t)state.drawWidth * state.drawHeight;
|
||||
for (size_t i = 0; i < pixel_count; ++i) {
|
||||
state.framebuffer[i] = fill;
|
||||
@@ -160,7 +175,7 @@ bool drawText(const std::string& text, int x, int y, int size) {
|
||||
|
||||
lv_obj_set_style_text_color(
|
||||
label,
|
||||
state.drawColor == 0 ? lv_color_black() : lv_color_white(),
|
||||
state.drawColor == 0 ? lv_color_white() : lv_color_black(),
|
||||
LV_PART_MAIN
|
||||
);
|
||||
|
||||
@@ -201,7 +216,7 @@ bool drawRgb565(const uint8_t* data, size_t size, int x, int y, int w, int h) {
|
||||
}
|
||||
size_t offset = ((size_t)source_y * w + source_x) * 2;
|
||||
uint16_t color = ((uint16_t)data[offset] << 8) | data[offset + 1];
|
||||
put_pixel(state, destination_x, destination_y, color);
|
||||
put_pixel(state, destination_x, destination_y, ~color);
|
||||
}
|
||||
}
|
||||
lv_obj_invalidate(state.drawArea);
|
||||
@@ -254,7 +269,7 @@ bool drawBmp(const uint8_t* data, size_t size, int x, int y) {
|
||||
state,
|
||||
x + source_x,
|
||||
y + source_y,
|
||||
rgb888_to_rgb565(pixel[2], pixel[1], pixel[0])
|
||||
~rgb888_to_rgb565(pixel[2], pixel[1], pixel[0])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -327,8 +342,8 @@ bool drawPbm(const uint8_t* data, size_t size, int x, int y) {
|
||||
for (int source_y = 0; source_y < h; ++source_y) {
|
||||
const uint8_t* row = data + offset + (size_t)source_y * row_bytes;
|
||||
for (int source_x = 0; source_x < w; ++source_x) {
|
||||
bool black = (row[source_x >> 3] & (0x80 >> (source_x & 7))) != 0;
|
||||
put_pixel(state, x + source_x, y + source_y, black ? 0x0000 : 0xFFFF);
|
||||
bool active = (row[source_x >> 3] & (0x80 >> (source_x & 7))) != 0;
|
||||
put_pixel(state, x + source_x, y + source_y, active ? 0x0000 : 0xFFFF);
|
||||
}
|
||||
}
|
||||
lv_obj_invalidate(state.drawArea);
|
||||
@@ -376,7 +391,7 @@ std::string getScreenshotPbmBase64() {
|
||||
for (int y = 0; y < state.drawHeight; ++y) {
|
||||
uint8_t* row = payload + (size_t)y * row_bytes;
|
||||
for (int x = 0; x < state.drawWidth; ++x) {
|
||||
uint16_t color = state.framebuffer[(size_t)y * state.drawWidth + x];
|
||||
uint16_t color = ~state.framebuffer[(size_t)y * state.drawWidth + x];
|
||||
// Convert RGB565 to simple grayscale threshold (black if sum of components is low)
|
||||
int red = (color >> 11) & 0x1F;
|
||||
int green = (color >> 5) & 0x3F;
|
||||
@@ -929,8 +944,8 @@ bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& er
|
||||
|
||||
bool success = false;
|
||||
bool decoded_audio = false;
|
||||
mp3dec_t* decoder = (mp3dec_t*)psram_malloc(sizeof(mp3dec_t));
|
||||
mp3d_sample_t* pcm = (mp3d_sample_t*)psram_malloc(
|
||||
mp3dec_t* decoder = (mp3dec_t*)malloc(sizeof(mp3dec_t));
|
||||
mp3d_sample_t* pcm = (mp3d_sample_t*)malloc(
|
||||
MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t)
|
||||
);
|
||||
int configured_rate = 0;
|
||||
@@ -945,8 +960,10 @@ bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& er
|
||||
}
|
||||
mp3dec_init(decoder);
|
||||
|
||||
LOGGER.info("playMp3Memory: starting loop, total size={}", (unsigned)size);
|
||||
while (offset < size && state.audioRunning) {
|
||||
size_t consumed = 0;
|
||||
|
||||
if (!play_mp3_buffer(
|
||||
state,
|
||||
decoder,
|
||||
@@ -963,12 +980,14 @@ bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& er
|
||||
goto done;
|
||||
}
|
||||
if (consumed == 0) {
|
||||
LOGGER.info("playMp3Memory: consumed == 0, breaking");
|
||||
break;
|
||||
}
|
||||
decoded_audio = decoded_audio || configured_rate != 0;
|
||||
offset += consumed;
|
||||
taskYIELD();
|
||||
}
|
||||
LOGGER.info("playMp3Memory: loop ended, offset={}", (unsigned)offset);
|
||||
|
||||
free(decoder);
|
||||
free(pcm);
|
||||
@@ -1008,14 +1027,14 @@ bool playMp3File(const std::string& filename, int volume, std::string& error) {
|
||||
}
|
||||
|
||||
{
|
||||
uint8_t* input = (uint8_t*)psram_malloc(MP3_INPUT_BUFFER_SIZE);
|
||||
uint8_t* input = (uint8_t*)malloc(MP3_INPUT_BUFFER_SIZE);
|
||||
if (input == NULL) {
|
||||
set_error(error, "Out of memory for MP3 decoding");
|
||||
goto done;
|
||||
}
|
||||
|
||||
mp3dec_t* decoder = (mp3dec_t*)psram_malloc(sizeof(mp3dec_t));
|
||||
mp3d_sample_t* pcm = (mp3d_sample_t*)psram_malloc(
|
||||
mp3dec_t* decoder = (mp3dec_t*)malloc(sizeof(mp3dec_t));
|
||||
mp3d_sample_t* pcm = (mp3d_sample_t*)malloc(
|
||||
MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t)
|
||||
);
|
||||
if (decoder == NULL || pcm == NULL) {
|
||||
@@ -1101,6 +1120,774 @@ done:
|
||||
return success;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
struct Bmi270Data {
|
||||
float ax, ay, az;
|
||||
float gx, gy, gz;
|
||||
};
|
||||
error_t bmi270_read(struct Device* device, struct Bmi270Data* data) __attribute__((weak));
|
||||
|
||||
struct Mpu6886Data {
|
||||
float ax, ay, az;
|
||||
float gx, gy, gz;
|
||||
};
|
||||
error_t mpu6886_read(struct Device* device, struct Mpu6886Data* data) __attribute__((weak));
|
||||
|
||||
struct Qmi8658Data {
|
||||
float ax, ay, az;
|
||||
float gx, gy, gz;
|
||||
};
|
||||
error_t qmi8658_read(struct Device* device, struct Qmi8658Data* data) __attribute__((weak));
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
bool getBatteryStatus(double& voltage_v, int& percentage_pct, std::string& error) {
|
||||
auto power = hal::findFirstDevice<hal::power::PowerDevice>(hal::Device::Type::Power);
|
||||
if (power == nullptr) {
|
||||
error = "Power device not found";
|
||||
return false;
|
||||
}
|
||||
hal::power::PowerDevice::MetricData data;
|
||||
uint32_t voltage_mv = 0;
|
||||
if (power->getMetric(hal::power::PowerDevice::MetricType::BatteryVoltage, data)) {
|
||||
voltage_mv = data.valueAsUint32;
|
||||
}
|
||||
uint8_t charge_level = 0;
|
||||
if (power->getMetric(hal::power::PowerDevice::MetricType::ChargeLevel, data)) {
|
||||
charge_level = data.valueAsUint8;
|
||||
}
|
||||
voltage_v = (double)voltage_mv / 1000.0;
|
||||
percentage_pct = (int)charge_level;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setLedColor(int r, int g, int b, const std::string& mode, std::string& error) {
|
||||
LOGGER.info("setLedColor: R={}, G={}, B={}, Mode={}", r, g, b, mode.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getSensors(double& temp_c, double& hum_pct, std::string& imu_json, std::string& error) {
|
||||
temp_c = 22.5;
|
||||
hum_pct = 50.0;
|
||||
|
||||
cJSON* imu = cJSON_CreateObject();
|
||||
bool has_imu = false;
|
||||
|
||||
struct Device* bmi = device_find_by_name("bmi270");
|
||||
if (bmi != nullptr && bmi270_read != nullptr) {
|
||||
Bmi270Data data;
|
||||
if (bmi270_read(bmi, &data) == ERROR_NONE) {
|
||||
cJSON_AddNumberToObject(imu, "ax", data.ax);
|
||||
cJSON_AddNumberToObject(imu, "ay", data.ay);
|
||||
cJSON_AddNumberToObject(imu, "az", data.az);
|
||||
cJSON_AddNumberToObject(imu, "gx", data.gx);
|
||||
cJSON_AddNumberToObject(imu, "gy", data.gy);
|
||||
cJSON_AddNumberToObject(imu, "gz", data.gz);
|
||||
has_imu = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_imu) {
|
||||
struct Device* mpu = device_find_by_name("mpu6886");
|
||||
if (mpu != nullptr && mpu6886_read != nullptr) {
|
||||
Mpu6886Data data;
|
||||
if (mpu6886_read(mpu, &data) == ERROR_NONE) {
|
||||
cJSON_AddNumberToObject(imu, "ax", data.ax);
|
||||
cJSON_AddNumberToObject(imu, "ay", data.ay);
|
||||
cJSON_AddNumberToObject(imu, "az", data.az);
|
||||
cJSON_AddNumberToObject(imu, "gx", data.gx);
|
||||
cJSON_AddNumberToObject(imu, "gy", data.gy);
|
||||
cJSON_AddNumberToObject(imu, "gz", data.gz);
|
||||
has_imu = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_imu) {
|
||||
struct Device* qmi = device_find_by_name("qmi8658");
|
||||
if (qmi != nullptr && qmi8658_read != nullptr) {
|
||||
Qmi8658Data data;
|
||||
if (qmi8658_read(qmi, &data) == ERROR_NONE) {
|
||||
cJSON_AddNumberToObject(imu, "ax", data.ax);
|
||||
cJSON_AddNumberToObject(imu, "ay", data.ay);
|
||||
cJSON_AddNumberToObject(imu, "az", data.az);
|
||||
cJSON_AddNumberToObject(imu, "gx", data.gx);
|
||||
cJSON_AddNumberToObject(imu, "gy", data.gy);
|
||||
cJSON_AddNumberToObject(imu, "gz", data.gz);
|
||||
has_imu = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (has_imu) {
|
||||
char* printed = cJSON_PrintUnformatted(imu);
|
||||
if (printed != nullptr) {
|
||||
imu_json = printed;
|
||||
free(printed);
|
||||
}
|
||||
} else {
|
||||
imu_json = "{}";
|
||||
}
|
||||
cJSON_Delete(imu);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool scanBleDevices(int duration_ms, std::string& devices_json, std::string& error) {
|
||||
struct Device* dev = bluetooth_find_first_ready_device();
|
||||
if (dev == nullptr) {
|
||||
error = "BLE device not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto radio_state = bluetooth::getRadioState();
|
||||
bool originally_off = (radio_state == bluetooth::RadioState::Off);
|
||||
if (radio_state != bluetooth::RadioState::On) {
|
||||
LOGGER.info("Enabling BLE radio for scanning");
|
||||
bluetooth_set_radio_enabled(dev, true);
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
if (bluetooth::getRadioState() == bluetooth::RadioState::On) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bluetooth::getRadioState() != bluetooth::RadioState::On) {
|
||||
error = "Failed to enable Bluetooth radio";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("Starting BLE scan for {} ms", duration_ms);
|
||||
bluetooth_scan_start(dev);
|
||||
vTaskDelay(pdMS_TO_TICKS(duration_ms));
|
||||
bluetooth_scan_stop(dev);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
std::vector<bluetooth::PeerRecord> peers = bluetooth::getScanResults();
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON* dev_array = cJSON_AddArrayToObject(root, "devices");
|
||||
for (const auto& peer : peers) {
|
||||
cJSON* item = cJSON_CreateObject();
|
||||
char mac_str[18];
|
||||
snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
peer.addr[0], peer.addr[1], peer.addr[2],
|
||||
peer.addr[3], peer.addr[4], peer.addr[5]);
|
||||
cJSON_AddStringToObject(item, "address", mac_str);
|
||||
cJSON_AddStringToObject(item, "name", peer.name.c_str());
|
||||
cJSON_AddNumberToObject(item, "rssi", peer.rssi);
|
||||
cJSON_AddBoolToObject(item, "paired", peer.paired);
|
||||
cJSON_AddBoolToObject(item, "connected", peer.connected);
|
||||
cJSON_AddItemToArray(dev_array, item);
|
||||
}
|
||||
|
||||
char* printed = cJSON_PrintUnformatted(root);
|
||||
if (printed != nullptr) {
|
||||
devices_json = printed;
|
||||
free(printed);
|
||||
} else {
|
||||
devices_json = "{\"devices\":[]}";
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
|
||||
if (originally_off) {
|
||||
LOGGER.info("Restoring BLE radio state to off");
|
||||
bluetooth_set_radio_enabled(dev, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool resolve_sd_path(const std::string& input_path, std::string& out_resolved_path, std::string& error) {
|
||||
if (input_path.empty()) {
|
||||
error = "Path is empty";
|
||||
return false;
|
||||
}
|
||||
if (input_path.find("..") != std::string::npos) {
|
||||
error = "Directory traversal is forbidden";
|
||||
return false;
|
||||
}
|
||||
std::string rel = input_path;
|
||||
while (rel.starts_with("/")) {
|
||||
rel = rel.substr(1);
|
||||
}
|
||||
while (rel.starts_with("./")) {
|
||||
rel = rel.substr(2);
|
||||
}
|
||||
if (rel.empty()) {
|
||||
error = "Path resolves to empty";
|
||||
return false;
|
||||
}
|
||||
out_resolved_path = "/sdcard/" + rel;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeSdFile(const std::string& filename, const std::string& content, std::string& error) {
|
||||
std::string resolved_path;
|
||||
if (!resolve_sd_path(filename, resolved_path, error)) {
|
||||
return false;
|
||||
}
|
||||
if (!file::findOrCreateParentDirectory(resolved_path, 0755)) {
|
||||
error = "Failed to create parent directory";
|
||||
return false;
|
||||
}
|
||||
if (!file::writeString(resolved_path, content)) {
|
||||
error = "Failed to write file";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readSdFile(const std::string& filename, std::string& content, std::string& error) {
|
||||
std::string resolved_path;
|
||||
if (!resolve_sd_path(filename, resolved_path, error)) {
|
||||
return false;
|
||||
}
|
||||
if (!file::isFile(resolved_path)) {
|
||||
error = "File does not exist";
|
||||
return false;
|
||||
}
|
||||
auto read_buf = file::readString(resolved_path);
|
||||
if (read_buf == nullptr) {
|
||||
error = "Failed to read file";
|
||||
return false;
|
||||
}
|
||||
content = reinterpret_cast<const char*>(read_buf.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool downloadSdFile(const std::string& url, const std::string& filename, std::string& error) {
|
||||
std::string resolved_path;
|
||||
if (!resolve_sd_path(filename, resolved_path, error)) {
|
||||
return false;
|
||||
}
|
||||
if (!file::findOrCreateParentDirectory(resolved_path, 0755)) {
|
||||
error = "Failed to create parent directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
|
||||
if (sem == nullptr) {
|
||||
error = "Failed to create semaphore";
|
||||
return false;
|
||||
}
|
||||
|
||||
struct DownloadState {
|
||||
SemaphoreHandle_t sem;
|
||||
bool success;
|
||||
std::string err_msg;
|
||||
};
|
||||
auto state = std::make_shared<DownloadState>();
|
||||
state->sem = sem;
|
||||
state->success = false;
|
||||
|
||||
tt::network::http::download(
|
||||
url,
|
||||
"/system/certificates/WE1.pem",
|
||||
resolved_path,
|
||||
[state]() {
|
||||
state->success = true;
|
||||
xSemaphoreGive(state->sem);
|
||||
},
|
||||
[state](const char* err) {
|
||||
state->success = false;
|
||||
state->err_msg = err ? err : "Unknown error";
|
||||
xSemaphoreGive(state->sem);
|
||||
}
|
||||
);
|
||||
|
||||
if (xSemaphoreTake(sem, pdMS_TO_TICKS(30000)) != pdTRUE) {
|
||||
error = "Download timed out after 30 seconds";
|
||||
vSemaphoreDelete(sem);
|
||||
return false;
|
||||
}
|
||||
vSemaphoreDelete(sem);
|
||||
if (!state->success) {
|
||||
error = state->err_msg;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void mcp_video_stream_task(void* pvParameters) {
|
||||
auto& state = getState();
|
||||
|
||||
int mono_server_fd = -1;
|
||||
int color_server_fd = -1;
|
||||
int mono_client_fd = -1;
|
||||
int color_client_fd = -1;
|
||||
|
||||
uint8_t* mono_buffer = (uint8_t*)heap_caps_malloc(15000, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (mono_buffer == nullptr) mono_buffer = (uint8_t*)heap_caps_malloc(15000, MALLOC_CAP_8BIT);
|
||||
|
||||
uint8_t* color_buffer = (uint8_t*)heap_caps_malloc(153600, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (color_buffer == nullptr) color_buffer = (uint8_t*)heap_caps_malloc(153600, MALLOC_CAP_8BIT);
|
||||
|
||||
if (mono_buffer == nullptr || color_buffer == nullptr) {
|
||||
LOGGER.error("Failed to allocate video stream buffers");
|
||||
if (mono_buffer) heap_caps_free(mono_buffer);
|
||||
if (color_buffer) heap_caps_free(color_buffer);
|
||||
state.streamRunning = false;
|
||||
vTaskDelete(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Mono TCP Server Socket
|
||||
mono_server_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (mono_server_fd >= 0) {
|
||||
int opt = 1;
|
||||
setsockopt(mono_server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
struct sockaddr_in address;
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(8081);
|
||||
if (bind(mono_server_fd, (struct sockaddr*)&address, sizeof(address)) >= 0) {
|
||||
listen(mono_server_fd, 1);
|
||||
fcntl(mono_server_fd, F_SETFL, O_NONBLOCK);
|
||||
LOGGER.info("Mono TCP Video stream server started on port 8081");
|
||||
} else {
|
||||
LOGGER.error("Failed to bind Mono TCP socket");
|
||||
close(mono_server_fd);
|
||||
mono_server_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Color TCP Server Socket
|
||||
color_server_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (color_server_fd >= 0) {
|
||||
int opt = 1;
|
||||
setsockopt(color_server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
struct sockaddr_in address;
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(8083);
|
||||
if (bind(color_server_fd, (struct sockaddr*)&address, sizeof(address)) >= 0) {
|
||||
listen(color_server_fd, 1);
|
||||
fcntl(color_server_fd, F_SETFL, O_NONBLOCK);
|
||||
LOGGER.info("Color TCP Video stream server started on port 8083");
|
||||
} else {
|
||||
LOGGER.error("Failed to bind Color TCP socket");
|
||||
close(color_server_fd);
|
||||
color_server_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t mono_received = 0;
|
||||
uint32_t color_header_received = 0;
|
||||
uint32_t color_payload_received = 0;
|
||||
uint8_t color_header[16];
|
||||
|
||||
uint16_t color_x = 0, color_y = 0, color_w = 0, color_h = 0;
|
||||
uint32_t color_payload_len = 0;
|
||||
|
||||
uint32_t last_packet_time = xTaskGetTickCount();
|
||||
uint32_t fps_start_time = xTaskGetTickCount();
|
||||
uint32_t fps_frame_count = 0;
|
||||
|
||||
while (state.streamRunning) {
|
||||
uint32_t now = xTaskGetTickCount();
|
||||
|
||||
// Update FPS every 1 second
|
||||
if (now - fps_start_time >= pdMS_TO_TICKS(1000)) {
|
||||
state.lastFps = (double)fps_frame_count * 1000.0 / pdTICKS_TO_MS(now - fps_start_time);
|
||||
fps_frame_count = 0;
|
||||
fps_start_time = now;
|
||||
}
|
||||
|
||||
// Handle inactivity timeout (3 seconds) for active video stream clients
|
||||
bool streamClientConnected = (mono_client_fd >= 0 || color_client_fd >= 0);
|
||||
if (state.overrideActive && streamClientConnected && (now - last_packet_time) > pdMS_TO_TICKS(3000)) {
|
||||
LOGGER.info("Video stream timed out. Returning to normal screensaver.");
|
||||
state.overrideActive = false;
|
||||
if (mono_client_fd >= 0) {
|
||||
close(mono_client_fd);
|
||||
mono_client_fd = -1;
|
||||
}
|
||||
if (color_client_fd >= 0) {
|
||||
close(color_client_fd);
|
||||
color_client_fd = -1;
|
||||
}
|
||||
auto idleService = service::displayidle::findService();
|
||||
if (idleService) {
|
||||
idleService->stopScreensaver();
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Accept Mono client
|
||||
if (mono_server_fd >= 0 && mono_client_fd < 0) {
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t addr_len = sizeof(client_addr);
|
||||
int client = accept(mono_server_fd, (struct sockaddr*)&client_addr, &addr_len);
|
||||
if (client >= 0) {
|
||||
mono_client_fd = client;
|
||||
fcntl(mono_client_fd, F_SETFL, O_NONBLOCK);
|
||||
mono_received = 0;
|
||||
last_packet_time = now;
|
||||
LOGGER.info("Mono TCP Stream client connected");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Read Mono client data
|
||||
if (mono_client_fd >= 0) {
|
||||
int remaining = 15000 - mono_received;
|
||||
int n = recv(mono_client_fd, mono_buffer + mono_received, remaining, 0);
|
||||
if (n > 0) {
|
||||
mono_received += n;
|
||||
last_packet_time = now;
|
||||
state.tcpBytesReceived += n;
|
||||
|
||||
if (mono_received == 15000) {
|
||||
uint32_t draw_start = xTaskGetTickCount();
|
||||
if (ensureOverrideScreen()) {
|
||||
if (lvgl::lock(pdMS_TO_TICKS(1500))) {
|
||||
if (display_ready(state)) {
|
||||
size_t pixel_count = (size_t)state.drawWidth * state.drawHeight;
|
||||
for (size_t i = 0; i < pixel_count; ++i) {
|
||||
state.framebuffer[i] = 0xFFFF; // Fill canvas with white
|
||||
}
|
||||
|
||||
int x_off = (state.drawWidth - 200) / 2;
|
||||
int y_off = (state.drawHeight - 240) / 2;
|
||||
|
||||
for (int index = 0; index < 15000; ++index) {
|
||||
uint8_t val = mono_buffer[index];
|
||||
if (val == 0) continue;
|
||||
|
||||
int byte_x = index / 75;
|
||||
int block_y = index % 75;
|
||||
int x_base = 2 * byte_x + x_off;
|
||||
int y_base = 299 - 4 * block_y + y_off;
|
||||
|
||||
if ((val & 0x80) && y_base >= 0 && y_base < state.drawHeight)
|
||||
put_pixel(state, x_base, y_base, 0x0000);
|
||||
if ((val & 0x40) && y_base >= 0 && y_base < state.drawHeight)
|
||||
put_pixel(state, x_base + 1, y_base, 0x0000);
|
||||
if ((val & 0x20) && (y_base - 1) >= 0 && (y_base - 1) < state.drawHeight)
|
||||
put_pixel(state, x_base, y_base - 1, 0x0000);
|
||||
if ((val & 0x10) && (y_base - 1) >= 0 && (y_base - 1) < state.drawHeight)
|
||||
put_pixel(state, x_base + 1, y_base - 1, 0x0000);
|
||||
if ((val & 0x08) && (y_base - 2) >= 0 && (y_base - 2) < state.drawHeight)
|
||||
put_pixel(state, x_base, y_base - 2, 0x0000);
|
||||
if ((val & 0x04) && (y_base - 2) >= 0 && (y_base - 2) < state.drawHeight)
|
||||
put_pixel(state, x_base + 1, y_base - 2, 0x0000);
|
||||
if ((val & 0x02) && (y_base - 3) >= 0 && (y_base - 3) < state.drawHeight)
|
||||
put_pixel(state, x_base, y_base - 3, 0x0000);
|
||||
if ((val & 0x01) && (y_base - 3) >= 0 && (y_base - 3) < state.drawHeight)
|
||||
put_pixel(state, x_base + 1, y_base - 3, 0x0000);
|
||||
}
|
||||
lv_obj_invalidate(state.drawArea);
|
||||
state.overrideActive = true;
|
||||
}
|
||||
lvgl::unlock();
|
||||
}
|
||||
}
|
||||
state.lastDrawMs = pdTICKS_TO_MS(xTaskGetTickCount() - draw_start);
|
||||
state.framesDrawn++;
|
||||
fps_frame_count++;
|
||||
|
||||
mono_received = 0;
|
||||
}
|
||||
} else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
|
||||
LOGGER.info("Mono TCP Stream client disconnected");
|
||||
close(mono_client_fd);
|
||||
mono_client_fd = -1;
|
||||
mono_received = 0;
|
||||
if (color_client_fd < 0 && state.overrideActive) {
|
||||
state.overrideActive = false;
|
||||
auto idleService = service::displayidle::findService();
|
||||
if (idleService) {
|
||||
idleService->stopScreensaver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Accept Color client
|
||||
if (color_server_fd >= 0 && color_client_fd < 0) {
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t addr_len = sizeof(client_addr);
|
||||
int client = accept(color_server_fd, (struct sockaddr*)&client_addr, &addr_len);
|
||||
if (client >= 0) {
|
||||
color_client_fd = client;
|
||||
fcntl(color_client_fd, F_SETFL, O_NONBLOCK);
|
||||
color_header_received = 0;
|
||||
color_payload_received = 0;
|
||||
last_packet_time = now;
|
||||
LOGGER.info("Color TCP Stream client connected");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Read Color client data
|
||||
if (color_client_fd >= 0) {
|
||||
if (color_header_received < 16) {
|
||||
int remaining = 16 - color_header_received;
|
||||
int n = recv(color_client_fd, color_header + color_header_received, remaining, 0);
|
||||
if (n > 0) {
|
||||
color_header_received += n;
|
||||
last_packet_time = now;
|
||||
state.tcpBytesReceived += n;
|
||||
|
||||
if (color_header_received == 16) {
|
||||
if (memcmp(color_header, "RAW\x01", 4) != 0) {
|
||||
LOGGER.warn("Invalid Color Stream magic! Disconnecting client.");
|
||||
close(color_client_fd);
|
||||
color_client_fd = -1;
|
||||
} else {
|
||||
color_x = ((uint16_t)color_header[4] << 8) | color_header[5];
|
||||
color_y = ((uint16_t)color_header[6] << 8) | color_header[7];
|
||||
color_w = ((uint16_t)color_header[8] << 8) | color_header[9];
|
||||
color_h = ((uint16_t)color_header[10] << 8) | color_header[11];
|
||||
color_payload_len = ((uint32_t)color_header[12] << 24) |
|
||||
((uint32_t)color_header[13] << 16) |
|
||||
((uint32_t)color_header[14] << 8) |
|
||||
color_header[15];
|
||||
color_payload_received = 0;
|
||||
|
||||
if (color_payload_len > 153600) {
|
||||
LOGGER.warn("Color stream payload too large ({} bytes). Disconnecting.", color_payload_len);
|
||||
close(color_client_fd);
|
||||
color_client_fd = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
|
||||
LOGGER.info("Color TCP Stream client disconnected during header read");
|
||||
close(color_client_fd);
|
||||
color_client_fd = -1;
|
||||
if (mono_client_fd < 0 && state.overrideActive) {
|
||||
state.overrideActive = false;
|
||||
auto idleService = service::displayidle::findService();
|
||||
if (idleService) {
|
||||
idleService->stopScreensaver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (color_client_fd >= 0 && color_header_received == 16 && color_payload_len > 0) {
|
||||
int remaining = color_payload_len - color_payload_received;
|
||||
int n = recv(color_client_fd, color_buffer + color_payload_received, remaining, 0);
|
||||
if (n > 0) {
|
||||
color_payload_received += n;
|
||||
last_packet_time = now;
|
||||
state.tcpBytesReceived += n;
|
||||
|
||||
if (color_payload_received == color_payload_len) {
|
||||
uint32_t draw_start = xTaskGetTickCount();
|
||||
mcp::drawRgb565(color_buffer, color_payload_len, color_x, color_y, color_w, color_h);
|
||||
|
||||
state.lastDrawMs = pdTICKS_TO_MS(xTaskGetTickCount() - draw_start);
|
||||
state.framesDrawn++;
|
||||
fps_frame_count++;
|
||||
|
||||
color_header_received = 0;
|
||||
color_payload_len = 0;
|
||||
color_payload_received = 0;
|
||||
}
|
||||
} else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
|
||||
LOGGER.info("Color TCP Stream client disconnected during payload read");
|
||||
close(color_client_fd);
|
||||
color_client_fd = -1;
|
||||
if (mono_client_fd < 0 && state.overrideActive) {
|
||||
state.overrideActive = false;
|
||||
auto idleService = service::displayidle::findService();
|
||||
if (idleService) {
|
||||
idleService->stopScreensaver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
|
||||
if (mono_client_fd >= 0) close(mono_client_fd);
|
||||
if (color_client_fd >= 0) close(color_client_fd);
|
||||
if (mono_server_fd >= 0) close(mono_server_fd);
|
||||
if (color_server_fd >= 0) close(color_server_fd);
|
||||
|
||||
heap_caps_free(mono_buffer);
|
||||
heap_caps_free(color_buffer);
|
||||
|
||||
LOGGER.info("Video stream server task stopped");
|
||||
state.streamTaskHandle = nullptr;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
bool startVideoStreamServer() {
|
||||
auto& state = getState();
|
||||
if (state.streamRunning) {
|
||||
return true;
|
||||
}
|
||||
state.streamRunning = true;
|
||||
state.framesDrawn = 0;
|
||||
state.tcpBytesReceived = 0;
|
||||
state.lastDrawMs = 0;
|
||||
state.lastFps = 0.0;
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
mcp_video_stream_task,
|
||||
"mcp_video_stream",
|
||||
4096,
|
||||
nullptr,
|
||||
5,
|
||||
(TaskHandle_t*)&state.streamTaskHandle,
|
||||
1
|
||||
);
|
||||
if (ret != pdPASS) {
|
||||
state.streamRunning = false;
|
||||
LOGGER.error("Failed to spawn video stream task");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void stopVideoStreamServer() {
|
||||
auto& state = getState();
|
||||
if (!state.streamRunning) {
|
||||
return;
|
||||
}
|
||||
state.streamRunning = false;
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
if (state.streamTaskHandle == nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool getVideoStreamStats(std::string& stats_json) {
|
||||
auto& state = getState();
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(root, "frames_drawn", state.framesDrawn);
|
||||
cJSON_AddNumberToObject(root, "tcp_bytes_received", state.tcpBytesReceived);
|
||||
cJSON_AddNumberToObject(root, "last_draw_ms", state.lastDrawMs);
|
||||
cJSON_AddNumberToObject(root, "last_fps", state.lastFps);
|
||||
cJSON_AddBoolToObject(root, "active", state.overrideActive);
|
||||
|
||||
char* printed = cJSON_PrintUnformatted(root);
|
||||
if (printed != nullptr) {
|
||||
stats_json = printed;
|
||||
free(printed);
|
||||
} else {
|
||||
stats_json = "{}";
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool listApps(std::string& apps_json, std::string& error) {
|
||||
auto manifests = app::getAppManifests();
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON* apps_array = cJSON_AddArrayToObject(root, "apps");
|
||||
|
||||
for (const auto& manifest : manifests) {
|
||||
cJSON* item = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(item, "appId", manifest->appId.c_str());
|
||||
cJSON_AddStringToObject(item, "appName", manifest->appName.c_str());
|
||||
cJSON_AddStringToObject(item, "appIcon", manifest->appIcon.c_str());
|
||||
cJSON_AddStringToObject(item, "appVersionName", manifest->appVersionName.c_str());
|
||||
cJSON_AddNumberToObject(item, "appVersionCode", manifest->appVersionCode);
|
||||
|
||||
const char* category = "User";
|
||||
if (manifest->appCategory == app::Category::System) {
|
||||
category = "System";
|
||||
} else if (manifest->appCategory == app::Category::Settings) {
|
||||
category = "Settings";
|
||||
}
|
||||
cJSON_AddStringToObject(item, "appCategory", category);
|
||||
|
||||
cJSON_AddBoolToObject(item, "isExternal", manifest->appLocation.isExternal());
|
||||
if (manifest->appLocation.isExternal()) {
|
||||
cJSON_AddStringToObject(item, "path", manifest->appLocation.getPath().c_str());
|
||||
} else {
|
||||
cJSON_AddStringToObject(item, "path", "internal");
|
||||
}
|
||||
|
||||
cJSON_AddNumberToObject(item, "appFlags", manifest->appFlags);
|
||||
cJSON_AddItemToArray(apps_array, item);
|
||||
}
|
||||
|
||||
char* printed = cJSON_PrintUnformatted(root);
|
||||
if (printed != nullptr) {
|
||||
apps_json = printed;
|
||||
free(printed);
|
||||
} else {
|
||||
apps_json = "{\"apps\":[]}";
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool runApp(const std::string& appId, std::string& error) {
|
||||
auto manifest = app::findAppManifestById(appId);
|
||||
if (manifest == nullptr) {
|
||||
error = "Application not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
app::start(appId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool listSdFiles(const std::string& directory, std::string& files_json, std::string& error) {
|
||||
std::string resolved_path;
|
||||
if (!resolve_sd_path(directory, resolved_path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DIR* dir = opendir(resolved_path.c_str());
|
||||
if (dir == nullptr) {
|
||||
error = "Failed to open directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON* file_array = cJSON_AddArrayToObject(root, "files");
|
||||
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(dir)) != nullptr) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* item = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(item, "name", entry->d_name);
|
||||
|
||||
std::string full_filepath = resolved_path;
|
||||
if (!full_filepath.ends_with("/")) {
|
||||
full_filepath += "/";
|
||||
}
|
||||
full_filepath += entry->d_name;
|
||||
|
||||
struct stat st;
|
||||
if (stat(full_filepath.c_str(), &st) == 0) {
|
||||
bool is_dir = S_ISDIR(st.st_mode);
|
||||
cJSON_AddStringToObject(item, "type", is_dir ? "directory" : "file");
|
||||
if (!is_dir) {
|
||||
cJSON_AddNumberToObject(item, "size", st.st_size);
|
||||
}
|
||||
} else {
|
||||
cJSON_AddStringToObject(item, "type", entry->d_type == DT_DIR ? "directory" : "file");
|
||||
}
|
||||
cJSON_AddItemToArray(file_array, item);
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
char* printed = cJSON_PrintUnformatted(root);
|
||||
if (printed != nullptr) {
|
||||
files_json = printed;
|
||||
free(printed);
|
||||
} else {
|
||||
files_json = "{\"files\":[]}";
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace tt::mcp
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user