Commit 8d1ec7b8 authored by 957dd's avatar 957dd

无网络语音播报到45s一次加优化了wfii扫描

parent 1ce13541
...@@ -2,15 +2,37 @@ ...@@ -2,15 +2,37 @@
#include "device_wifi_change.h" #include "device_wifi_change.h"
#include "device_wifi_manager.h" #include "device_wifi_manager.h"
#include "modules_common.h" #include "modules_common.h"
#include <sys/types.h>
#include <sys/wait.h>
#define MAX_WIFI_ENTRIES 32 #define MAX_WIFI_ENTRIES 32
#define MAX_SAVED_WIFI_SLOTS 10 #define MAX_SAVED_WIFI_SLOTS 10
#define SSID_MAX_LENGTH 64 #define SSID_MAX_LENGTH 64
#define WIFI_PSK_MAX_LENGTH 63 #define WIFI_PSK_MAX_LENGTH 63
#define NM_CONN_DIR "/etc/NetworkManager/system-connections/" #define NM_CONN_DIR "/etc/NetworkManager/system-connections/"
#define WIFI_DELETE_COMPAT_DELAY_MS 2000
#define WIFI_SAVE_DELETE_GUARD_SEC 5
#define WIFI_SAVE_GUARD_SLOTS 8
#define WIFI_DELETED_PASSWORD_CACHE_SEC 60
static ThreadPool_t *s_wifi_pool = NULL; static ThreadPool_t *s_wifi_pool = NULL;
static pthread_mutex_t s_wifi_pool_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t s_wifi_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t s_wifi_save_guard_mutex = PTHREAD_MUTEX_INITIALIZER;
typedef struct {
char ssid[SSID_MAX_LENGTH];
time_t saved_at;
} wifi_save_guard_t;
static wifi_save_guard_t s_wifi_save_guards[WIFI_SAVE_GUARD_SLOTS];
typedef struct {
char ssid[SSID_MAX_LENGTH];
char password[WIFI_PSK_MAX_LENGTH + 1];
time_t cached_at;
} wifi_deleted_credential_t;
static wifi_deleted_credential_t s_deleted_credentials[WIFI_SAVE_GUARD_SLOTS];
typedef struct { typedef struct {
char ssid[SSID_MAX_LENGTH]; char ssid[SSID_MAX_LENGTH];
...@@ -19,6 +41,11 @@ typedef struct { ...@@ -19,6 +41,11 @@ typedef struct {
typedef struct { typedef struct {
char ssid[SSID_MAX_LENGTH]; char ssid[SSID_MAX_LENGTH];
int signal;
} visible_wifi_entry_t;
typedef struct {
char ssid[SSID_MAX_LENGTH];
char password[WIFI_PSK_MAX_LENGTH + 1]; char password[WIFI_PSK_MAX_LENGTH + 1];
} wifi_save_task_t; } wifi_save_task_t;
...@@ -29,6 +56,129 @@ typedef struct { ...@@ -29,6 +56,129 @@ typedef struct {
static void wifi_save_worker(void *arg); static void wifi_save_worker(void *arg);
static void wifi_delete_worker(void *arg); static void wifi_delete_worker(void *arg);
static int submit_wifi_task(void (*worker)(void *), void *arg, const char *what); static int submit_wifi_task(void (*worker)(void *), void *arg, const char *what);
static int read_wifi_psk_from_profile(const char *ssid, char *psk_out, size_t psk_sz);
static void note_wifi_save_request(const char *ssid) {
int slot = -1;
int oldest_slot = 0;
if (!ssid || !ssid[0])
return;
pthread_mutex_lock(&s_wifi_save_guard_mutex);
for (int i = 0; i < WIFI_SAVE_GUARD_SLOTS; i++) {
if (s_wifi_save_guards[i].ssid[0] &&
strcmp(s_wifi_save_guards[i].ssid, ssid) == 0) {
slot = i;
break;
}
if (!s_wifi_save_guards[i].ssid[0] && slot < 0)
slot = i;
if (s_wifi_save_guards[i].saved_at < s_wifi_save_guards[oldest_slot].saved_at)
oldest_slot = i;
}
if (slot < 0)
slot = oldest_slot;
strncpy(s_wifi_save_guards[slot].ssid, ssid,
sizeof(s_wifi_save_guards[slot].ssid) - 1);
s_wifi_save_guards[slot].ssid[sizeof(s_wifi_save_guards[slot].ssid) - 1] = '\0';
s_wifi_save_guards[slot].saved_at = time(NULL);
pthread_mutex_unlock(&s_wifi_save_guard_mutex);
}
static int wifi_delete_blocked_by_recent_save(const char *ssid) {
time_t now = time(NULL);
int blocked = 0;
pthread_mutex_lock(&s_wifi_save_guard_mutex);
for (int i = 0; ssid && ssid[0] && i < WIFI_SAVE_GUARD_SLOTS; i++) {
double age;
if (!s_wifi_save_guards[i].ssid[0] ||
strcmp(ssid, s_wifi_save_guards[i].ssid) != 0)
continue;
age = difftime(now, s_wifi_save_guards[i].saved_at);
if (age >= 0.0 && age <= WIFI_SAVE_DELETE_GUARD_SEC)
blocked = 1;
break;
}
pthread_mutex_unlock(&s_wifi_save_guard_mutex);
return blocked;
}
static void cache_wifi_password_before_delete(const char *ssid) {
char password[WIFI_PSK_MAX_LENGTH + 1];
int slot = -1;
int oldest_slot = 0;
if (read_wifi_psk_from_profile(ssid, password, sizeof(password)) != 0 ||
password[0] == '\0')
return;
pthread_mutex_lock(&s_wifi_save_guard_mutex);
for (int i = 0; i < WIFI_SAVE_GUARD_SLOTS; i++) {
if (s_deleted_credentials[i].ssid[0] &&
strcmp(s_deleted_credentials[i].ssid, ssid) == 0) {
slot = i;
break;
}
if (!s_deleted_credentials[i].ssid[0] && slot < 0)
slot = i;
if (s_deleted_credentials[i].cached_at < s_deleted_credentials[oldest_slot].cached_at)
oldest_slot = i;
}
if (slot < 0)
slot = oldest_slot;
strncpy(s_deleted_credentials[slot].ssid, ssid,
sizeof(s_deleted_credentials[slot].ssid) - 1);
s_deleted_credentials[slot].ssid[sizeof(s_deleted_credentials[slot].ssid) - 1] = '\0';
strncpy(s_deleted_credentials[slot].password, password,
sizeof(s_deleted_credentials[slot].password) - 1);
s_deleted_credentials[slot].password[sizeof(s_deleted_credentials[slot].password) - 1] = '\0';
s_deleted_credentials[slot].cached_at = time(NULL);
pthread_mutex_unlock(&s_wifi_save_guard_mutex);
memset(password, 0, sizeof(password));
}
static int get_recent_deleted_password(const char *ssid, char *password, size_t size) {
time_t now = time(NULL);
int found = 0;
if (!ssid || !password || size == 0)
return -1;
password[0] = '\0';
pthread_mutex_lock(&s_wifi_save_guard_mutex);
for (int i = 0; i < WIFI_SAVE_GUARD_SLOTS; i++) {
double age;
if (!s_deleted_credentials[i].ssid[0] ||
strcmp(s_deleted_credentials[i].ssid, ssid) != 0)
continue;
age = difftime(now, s_deleted_credentials[i].cached_at);
if (age >= 0.0 && age <= WIFI_DELETED_PASSWORD_CACHE_SEC &&
s_deleted_credentials[i].password[0]) {
strncpy(password, s_deleted_credentials[i].password, size - 1);
password[size - 1] = '\0';
found = 1;
}
break;
}
pthread_mutex_unlock(&s_wifi_save_guard_mutex);
return found ? 0 : -1;
}
static void clear_recent_deleted_password(const char *ssid) {
pthread_mutex_lock(&s_wifi_save_guard_mutex);
for (int i = 0; ssid && i < WIFI_SAVE_GUARD_SLOTS; i++) {
if (strcmp(s_deleted_credentials[i].ssid, ssid) != 0)
continue;
memset(&s_deleted_credentials[i], 0, sizeof(s_deleted_credentials[i]));
break;
}
pthread_mutex_unlock(&s_wifi_save_guard_mutex);
}
static int is_default_wifi_ssid(const char *ssid) { static int is_default_wifi_ssid(const char *ssid) {
return ssid != NULL && strcmp(ssid, default_SSID) == 0; return ssid != NULL && strcmp(ssid, default_SSID) == 0;
...@@ -311,6 +461,153 @@ static int collect_saved_wifi_entries(saved_wifi_entry_t *entries, int max_count ...@@ -311,6 +461,153 @@ static int collect_saved_wifi_entries(saved_wifi_entry_t *entries, int max_count
return count; return count;
} }
static void nmcli_unescape_field(char *field) {
char *src;
char *dst;
if (!field)
return;
src = dst = field;
while (*src) {
if (*src == '\\' && src[1] != '\0')
src++;
*dst++ = *src++;
}
*dst = '\0';
}
static int visible_wifi_index(const visible_wifi_entry_t *entries, int count,
const char *ssid) {
for (int i = 0; i < count; i++) {
if (strcmp(entries[i].ssid, ssid) == 0)
return i;
}
return -1;
}
static int visible_wifi_signal_desc(const void *a, const void *b) {
const visible_wifi_entry_t *wa = a;
const visible_wifi_entry_t *wb = b;
return wb->signal - wa->signal;
}
static int scan_visible_wifi(visible_wifi_entry_t *entries, int max_count) {
FILE *fp;
char line[256];
int count = 0;
fp = popen("nmcli -t --escape yes -f SSID,SIGNAL device wifi list --rescan yes 2>/dev/null", "r");
if (!fp) {
my_zlog_error("无法执行周围 WiFi 扫描");
return -1;
}
while (fgets(line, sizeof(line), fp) != NULL) {
char *separator;
char *signal_text;
int signal;
int idx;
trim_line(line);
separator = strrchr(line, ':');
if (!separator)
continue;
*separator = '\0';
signal_text = separator + 1;
nmcli_unescape_field(line);
if (!line[0])
continue;
signal = atoi(signal_text);
idx = visible_wifi_index(entries, count, line);
if (idx >= 0) {
if (signal > entries[idx].signal)
entries[idx].signal = signal;
continue;
}
if (count >= max_count)
continue;
strncpy(entries[count].ssid, line, sizeof(entries[count].ssid) - 1);
entries[count].ssid[sizeof(entries[count].ssid) - 1] = '\0';
entries[count].signal = signal;
count++;
}
pclose(fp);
qsort(entries, count, sizeof(entries[0]), visible_wifi_signal_desc);
return count;
}
static int activate_saved_wifi_profile(const char *profile_name) {
pid_t pid;
int status;
pid = fork();
if (pid < 0) {
my_zlog_error("启动 nmcli 失败: %s", strerror(errno));
return -1;
}
if (pid == 0) {
execlp("nmcli", "nmcli", "connection", "up", "id", profile_name,
"ifname", "wlan0", (char *)NULL);
_exit(127);
}
if (waitpid(pid, &status, 0) < 0)
return -1;
if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
return 0;
return -1;
}
int device_wifi_connect_best_saved_visible(void) {
saved_wifi_entry_t saved[MAX_WIFI_ENTRIES];
visible_wifi_entry_t visible[MAX_WIFI_ENTRIES];
int saved_count;
int visible_count;
saved_count = collect_saved_wifi_entries(saved, MAX_WIFI_ENTRIES);
if (saved_count <= 0) {
my_zlog_warn("断网恢复: 没有已保存的 WiFi 配置");
return 0;
}
visible_count = scan_visible_wifi(visible, MAX_WIFI_ENTRIES);
if (visible_count < 0)
return -1;
if (visible_count == 0) {
my_zlog_warn("断网恢复: 未扫描到周围 WiFi");
return 0;
}
get_current_wifi();
/* 先尝试其它已保存网络;没有其它选择时再重新激活当前网络。 */
for (int pass = 0; pass < 2; pass++) {
for (int i = 0; i < visible_count; i++) {
int is_current;
if (entry_index_by_name(saved, saved_count, visible[i].ssid) < 0)
continue;
is_current = g_current_ssid[0] && strcmp(g_current_ssid, visible[i].ssid) == 0;
if ((pass == 0 && is_current) || (pass == 1 && !is_current))
continue;
my_zlog_info("断网恢复: 尝试已保存 WiFi=%s, signal=%d",
visible[i].ssid, visible[i].signal);
if (activate_saved_wifi_profile(visible[i].ssid) == 0) {
my_zlog_info("断网恢复: NetworkManager 已激活 WiFi=%s", visible[i].ssid);
return 1;
}
my_zlog_warn("断网恢复: 激活 WiFi 失败=%s", visible[i].ssid);
}
}
my_zlog_warn("断网恢复: 周围网络与已保存配置无可用连接");
return 0;
}
static int nmcli_wifi_connection_exists(const char *ssid) { static int nmcli_wifi_connection_exists(const char *ssid) {
FILE *fp; FILE *fp;
char line[256]; char line[256];
...@@ -710,17 +1007,55 @@ int get_saved_wifi_list(char ssid_list[][SSID_MAX_LENGTH], int max_count) { ...@@ -710,17 +1007,55 @@ int get_saved_wifi_list(char ssid_list[][SSID_MAX_LENGTH], int max_count) {
} }
int orange_pi_save_wifi(const char *ssid, const char *password) { int orange_pi_save_wifi(const char *ssid, const char *password) {
if (validate_wifi_credentials(ssid, password) != 0) int already_saved;
int used_cached_password = 0;
char cached_password[WIFI_PSK_MAX_LENGTH + 1] = {0};
if (!ssid || !ssid[0] || strlen(ssid) >= SSID_MAX_LENGTH ||
strpbrk(ssid, "\"`$|;\\")) {
my_zlog_error("SSID无效");
return -1; return -1;
}
if (!wifi_connection_exists(ssid) && enforce_saved_wifi_limit(ssid) != 0) already_saved = wifi_connection_exists(ssid);
/*
* 后台同名更新可能不重复下发密码。此时保留 NetworkManager 中的原密码,
* 不删除、不重建配置,并按幂等保存成功处理。
*/
if (!password || password[0] == '\0') {
if (already_saved) {
clear_recent_deleted_password(ssid);
my_zlog_info("[SUCCESS] 同名WiFi密码为空,保留原密码: %s", ssid);
return 0;
}
if (get_recent_deleted_password(ssid, cached_password,
sizeof(cached_password)) != 0) {
my_zlog_error("新WiFi密码为空,无法创建配置: %s", ssid);
return -1;
}
password = cached_password;
used_cached_password = 1;
my_zlog_info("同名WiFi已被后台提前删除,使用短期缓存的原密码恢复: %s", ssid);
}
if (validate_wifi_credentials(ssid, password) != 0) {
memset(cached_password, 0, sizeof(cached_password));
return -1;
}
if (!already_saved && enforce_saved_wifi_limit(ssid) != 0)
return -1; return -1;
if (save_wifi_via_nmcli(ssid, password) != 0) { if (save_wifi_via_nmcli(ssid, password) != 0) {
memset(cached_password, 0, sizeof(cached_password));
my_zlog_error("[ERROR] 保存配置失败: %s", ssid); my_zlog_error("[ERROR] 保存配置失败: %s", ssid);
return -1; return -1;
} }
clear_recent_deleted_password(ssid);
if (used_cached_password)
memset(cached_password, 0, sizeof(cached_password));
my_zlog_info("[SUCCESS] 已保存WiFi配置: %s", ssid); my_zlog_info("[SUCCESS] 已保存WiFi配置: %s", ssid);
return 0; return 0;
} }
...@@ -786,6 +1121,19 @@ static void wifi_delete_worker(void *arg) { ...@@ -786,6 +1121,19 @@ static void wifi_delete_worker(void *arg) {
if (!task) if (!task)
return; return;
/*
* 后台会先发删除再发保存;设备连接多个 MQTT 服务时两类消息可能乱序。
* 短暂等待保存接收函数登记 SSID,再决定是否真的删除。
*/
usleep(WIFI_DELETE_COMPAT_DELAY_MS * 1000);
if (wifi_delete_blocked_by_recent_save(task->ssid)) {
my_zlog_info("兼容同名保存: 忽略迟到/重复的删除请求: %s", task->ssid);
free(task);
return;
}
/* 若后台坚持先删后存,暂存原密码,供随后同名空密码保存恢复。 */
cache_wifi_password_before_delete(task->ssid);
rc = device_wifi_delete_saved_profile(task->ssid); rc = device_wifi_delete_saved_profile(task->ssid);
if (rc == 0) { if (rc == 0) {
my_zlog_info("删除成功"); my_zlog_info("删除成功");
...@@ -828,6 +1176,8 @@ void device_wifi_rec_sava(cJSON *body) { ...@@ -828,6 +1176,8 @@ void device_wifi_rec_sava(cJSON *body) {
} }
strncpy(task->ssid, wifi_ssid->valuestring, sizeof(task->ssid) - 1); strncpy(task->ssid, wifi_ssid->valuestring, sizeof(task->ssid) - 1);
strncpy(task->password, wifi_password->valuestring, sizeof(task->password) - 1); strncpy(task->password, wifi_password->valuestring, sizeof(task->password) - 1);
/* 入队前立即登记,避免单线程 WiFi 队列中的删除任务先执行。 */
note_wifi_save_request(task->ssid);
my_zlog_debug("接收到保存WiFi,后台 nmcli 保存"); my_zlog_debug("接收到保存WiFi,后台 nmcli 保存");
if (submit_wifi_task(wifi_save_worker, task, "save wifi") != 0) if (submit_wifi_task(wifi_save_worker, task, "save wifi") != 0)
device_wifi_public_sendmqtt(2); device_wifi_public_sendmqtt(2);
......
...@@ -18,6 +18,12 @@ int orange_pi_save_wifi(const char *ssid, const char *password); ...@@ -18,6 +18,12 @@ int orange_pi_save_wifi(const char *ssid, const char *password);
/* 保存并立即连接 WiFi(含满 10 个自动删最旧逻辑) */ /* 保存并立即连接 WiFi(含满 10 个自动删最旧逻辑) */
int orange_pi_connect_wifi(const char *ssid, const char *password); int orange_pi_connect_wifi(const char *ssid, const char *password);
/*
* 扫描周围 WiFi,与已保存的 NetworkManager 配置匹配,并优先连接信号最强的配置。
* 返回 1=已激活一个已保存网络,0=没有可用匹配或全部连接失败,-1=扫描失败。
*/
int device_wifi_connect_best_saved_visible(void);
/* ===== App WiFi 管理 (UDP 18888) 接口 ===== */ /* ===== App WiFi 管理 (UDP 18888) 接口 ===== */
/* 查询已保存 WiFi 列表,返回 cJSON 数组(调用方负责 cJSON_Delete): /* 查询已保存 WiFi 列表,返回 cJSON 数组(调用方负责 cJSON_Delete):
...@@ -37,4 +43,4 @@ int app_wifi_switch(const char *ssid); ...@@ -37,4 +43,4 @@ int app_wifi_switch(const char *ssid);
* 返回 0=成功, -1=未找到/失败, -2=受保护不可删 */ * 返回 0=成功, -1=未找到/失败, -2=受保护不可删 */
int app_wifi_delete(const char *ssid); int app_wifi_delete(const char *ssid);
#endif #endif
\ No newline at end of file
...@@ -82,7 +82,8 @@ static int init_worker_threads(void) ...@@ -82,7 +82,8 @@ static int init_worker_threads(void)
return thread_start_init(thread_exit_time, thread_mqtt_beat, return thread_start_init(thread_exit_time, thread_mqtt_beat,
thread_open_browser, thread_mqtt_reconnect, thread_open_browser, thread_mqtt_reconnect,
thread_time_calculation, thread_play_mp3, thread_time_calculation, thread_play_mp3,
thread_steering_control); thread_steering_control,
thread_wifi_offline_recovery);
} }
static const AppInitStep s_init_steps[] = { static const AppInitStep s_init_steps[] = {
...@@ -168,4 +169,4 @@ int main() ...@@ -168,4 +169,4 @@ int main()
sleep(3600); sleep(3600);
return 0; return 0;
} }
\ No newline at end of file
...@@ -35,7 +35,7 @@ int device_shot_cooling_init(); ...@@ -35,7 +35,7 @@ int device_shot_cooling_init();
int thread_start_init(ThreadFunc thread_exit_time, ThreadFunc thread_mqtt_beat, int thread_start_init(ThreadFunc thread_exit_time, ThreadFunc thread_mqtt_beat,
ThreadFunc thread_open_browser, ThreadFunc thread_mqtt_reconnect, ThreadFunc thread_open_browser, ThreadFunc thread_mqtt_reconnect,
ThreadFunc thread_time_calculation, ThreadFunc thread_play_mp3, ThreadFunc thread_time_calculation, ThreadFunc thread_play_mp3,
ThreadFunc thread_steering_control); ThreadFunc thread_steering_control, ThreadFunc thread_wifi_recovery);
/*像服务器请求*/ /*像服务器请求*/
int request_date(); int request_date();
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
#include "webrtcpush_config.h" #include "webrtcpush_config.h"
#include "webrtcpush_run.h" #include "webrtcpush_run.h"
#define PTHREAD_MAX 7 #define PTHREAD_MAX 8
static pthread_t s_thread[PTHREAD_MAX]; // 全局线程句柄数组(或传参) static pthread_t s_thread[PTHREAD_MAX]; // 全局线程句柄数组(或传参)
...@@ -19,7 +19,8 @@ static void *s_args[PTHREAD_MAX] = {NULL}; ...@@ -19,7 +19,8 @@ static void *s_args[PTHREAD_MAX] = {NULL};
int thread_start_init(ThreadFunc thread_exit_time, ThreadFunc thread_mqtt_beat, int thread_start_init(ThreadFunc thread_exit_time, ThreadFunc thread_mqtt_beat,
ThreadFunc thread_open_browser, ThreadFunc thread_mqtt_reconnect, ThreadFunc thread_open_browser, ThreadFunc thread_mqtt_reconnect,
ThreadFunc thread_time_calculation, ThreadFunc thread_play_mp3,ThreadFunc thread_steering_control) ThreadFunc thread_time_calculation, ThreadFunc thread_play_mp3,
ThreadFunc thread_steering_control, ThreadFunc thread_wifi_recovery)
{ {
ThreadFunc thread_funcs[] = { ThreadFunc thread_funcs[] = {
...@@ -29,7 +30,8 @@ int thread_start_init(ThreadFunc thread_exit_time, ThreadFunc thread_mqtt_beat, ...@@ -29,7 +30,8 @@ int thread_start_init(ThreadFunc thread_exit_time, ThreadFunc thread_mqtt_beat,
thread_mqtt_reconnect, thread_mqtt_reconnect,
thread_time_calculation, thread_time_calculation,
thread_play_mp3, thread_play_mp3,
thread_steering_control}; thread_steering_control,
thread_wifi_recovery};
for (int i = 0; i < PTHREAD_MAX; i++) for (int i = 0; i < PTHREAD_MAX; i++)
{ {
......
...@@ -7,7 +7,7 @@ typedef void *(*ThreadFunc)(void *); ...@@ -7,7 +7,7 @@ typedef void *(*ThreadFunc)(void *);
int thread_start_init(ThreadFunc thread_exit_time, ThreadFunc thread_mqtt_beat, int thread_start_init(ThreadFunc thread_exit_time, ThreadFunc thread_mqtt_beat,
ThreadFunc thread_open_browser, ThreadFunc thread_mqtt_reconnect, ThreadFunc thread_open_browser, ThreadFunc thread_mqtt_reconnect,
ThreadFunc thread_time_calculation, ThreadFunc thread_play_mp3, ThreadFunc thread_time_calculation, ThreadFunc thread_play_mp3,
ThreadFunc thread_steering_control); ThreadFunc thread_steering_control, ThreadFunc thread_wifi_recovery);
void *thread_exit_time(void *arg) ;//异常停止线程 void *thread_exit_time(void *arg) ;//异常停止线程
void *thread_mqtt_beat(void *arg) ;//心跳线程 void *thread_mqtt_beat(void *arg) ;//心跳线程
...@@ -19,4 +19,4 @@ void *thread_steering_control(void *arg);//方向盘控制线程 ...@@ -19,4 +19,4 @@ void *thread_steering_control(void *arg);//方向盘控制线程
void thread_end_close() ; void thread_end_close() ;
#endif #endif
\ No newline at end of file
#include "wifi_autoconfig.h" #include "wifi_autoconfig.h"
#include "main.h" #include "main.h"
#include "http_config_mqtt.h" #include "http_config_mqtt.h"
#include "device_wifi_manager.h"
#include "audioplay.h"
#include "cJSON.h" #include "cJSON.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
...@@ -8,6 +10,10 @@ ...@@ -8,6 +10,10 @@
#include <unistd.h> #include <unistd.h>
#include <sys/stat.h> #include <sys/stat.h>
#define WIFI_OFFLINE_RECOVERY_INTERVAL_SEC 40
#define WIFI_RECOVERY_SETTLE_SEC 5
#define WIFI_RECOVERY_MAX_SWITCH_ATTEMPTS 3
static const char *s_audio_files[] = { static const char *s_audio_files[] = {
"noCameraDetected.mp3", "noCameraDetected.mp3",
"connectionFailed.mp3", "connectionFailed.mp3",
...@@ -155,23 +161,12 @@ static void build_lang_filepath(char *filepath, size_t size, const char *lang, c ...@@ -155,23 +161,12 @@ static void build_lang_filepath(char *filepath, size_t size, const char *lang, c
} }
static int try_play_filepath(const char *filepath) { static int try_play_filepath(const char *filepath) {
char cmd[512];
if (access(filepath, F_OK) != 0) { if (access(filepath, F_OK) != 0) {
return 0; return 0;
} }
if (is_command_available("ffplay")) { if (audioplay_file_with_browser_preempt(filepath, 1.0) == 0) {
snprintf(cmd, sizeof(cmd), "ffplay -nodisp -autoexit -loglevel quiet \"%s\"", filepath); my_zlog_debug("离线音频播放成功: %s", filepath);
if (system(cmd) == 0) { return 1;
my_zlog_debug("ffplay 播放成功: %s", filepath);
return 1;
}
}
if (is_command_available("paplay")) {
snprintf(cmd, sizeof(cmd), "paplay \"%s\"", filepath);
if (system(cmd) == 0) {
my_zlog_debug("paplay 播放成功: %s", filepath);
return 1;
}
} }
my_zlog_warn("音频播放失败: %s", filepath); my_zlog_warn("音频播放失败: %s", filepath);
return 0; return 0;
...@@ -467,6 +462,48 @@ static int has_internet_connection() { ...@@ -467,6 +462,48 @@ static int has_internet_connection() {
return has_business_api_connection(); return has_business_api_connection();
} }
/* 一次完整的断网恢复周期:播报与扫描主动连接必须在同一个周期执行。 */
static int run_offline_recovery_cycle(void) {
my_zlog_warn("断网恢复: 开始 40 秒周期播报和已保存 WiFi 检测");
play_audio_file("connectionFailed.mp3");
for (int attempt = 0; attempt < WIFI_RECOVERY_MAX_SWITCH_ATTEMPTS; attempt++) {
int connect_result = device_wifi_connect_best_saved_visible();
if (connect_result <= 0)
break;
sleep(WIFI_RECOVERY_SETTLE_SEC);
if (has_internet_connection()) {
my_zlog_info("断网恢复: 已恢复互联网连接");
prepare_offline_audio_after_network();
play_audio_file("connectionSuccessful.mp3");
return 1;
}
my_zlog_warn("断网恢复: WiFi 已连接但互联网仍不可用,继续尝试其它已保存网络");
}
return 0;
}
void *thread_wifi_offline_recovery(void *arg) {
(void)arg;
my_zlog_info("断网恢复线程已启动,检测间隔=%d秒",
WIFI_OFFLINE_RECOVERY_INTERVAL_SEC);
while (1) {
time_t cycle_started = time(NULL);
if (!has_internet_connection())
run_offline_recovery_cycle();
time_t cycle_finished = time(NULL);
int elapsed = (int)difftime(cycle_finished, cycle_started);
int remaining = WIFI_OFFLINE_RECOVERY_INTERVAL_SEC - elapsed;
if (remaining > 0)
sleep((unsigned int)remaining);
}
return NULL;
}
// 尝试连接 WiFi // 尝试连接 WiFi
static int try_connect_wifi(const char *ssid, const char *password) { static int try_connect_wifi(const char *ssid, const char *password) {
char cmd[512]; char cmd[512];
...@@ -495,29 +532,23 @@ void check_and_autoconfig_wifi() { ...@@ -495,29 +532,23 @@ void check_and_autoconfig_wifi() {
my_zlog_warn("进入扫码循环,将每隔几秒提示一次..."); my_zlog_warn("进入扫码循环,将每隔几秒提示一次...");
const char *img_path = "/tmp/wifi_scan.jpg"; const char *img_path = "/tmp/wifi_scan.jpg";
time_t last_prompt_time = 0; time_t last_recovery_time = 0;
time_t last_camera_warning_time = 0;
// 首次播放
// play_audio_file("toScanQRCode.mp3"); // 改为进入循环后由定时逻辑触发(且会先检查网络)
last_prompt_time = 0; // 设为 0,确保进入循环第一次就会满足 difftime > 10.0 并触发播放逻辑
while (1) { while (1) {
// 0. 优先检查网络是否已恢复 (比如插上了网线)
if (has_internet_connection()) {
my_zlog_info("检测到网络已恢复,退出扫码模式。");
prepare_offline_audio_after_network();
play_audio_file("connectionSuccessful.mp3");
break;
}
// 1. 周期性播放提示音 (每 10 秒)
time_t now = time(NULL); time_t now = time(NULL);
if (difftime(now, last_prompt_time) >= 10.0) { if (last_recovery_time == 0 ||
// 再检查一次,如果有网就不播了,避免干扰 difftime(now, last_recovery_time) >= WIFI_OFFLINE_RECOVERY_INTERVAL_SEC) {
if (!has_internet_connection()) { /* 从本轮开始计时,保证两轮检测启动时间约间隔 40 秒。 */
play_audio_file("toScanQRCode.mp3"); last_recovery_time = now;
if (has_internet_connection()) {
my_zlog_info("检测到网络已恢复,退出扫码模式。");
prepare_offline_audio_after_network();
play_audio_file("connectionSuccessful.mp3");
break;
} }
last_prompt_time = now; if (run_offline_recovery_cycle())
break;
} }
// 2. 调用 fswebcam 拍照 // 2. 调用 fswebcam 拍照
...@@ -532,8 +563,13 @@ void check_and_autoconfig_wifi() { ...@@ -532,8 +563,13 @@ void check_and_autoconfig_wifi() {
if (cap_ret != 0) { if (cap_ret != 0) {
// 拍照失败,通常意味着没插摄像头或设备忙 // 拍照失败,通常意味着没插摄像头或设备忙
my_zlog_error("摄像头拍照失败 (fswebcam 返回 %d),请检查设备连接。", cap_ret); my_zlog_error("摄像头拍照失败 (fswebcam 返回 %d),请检查设备连接。", cap_ret);
play_audio_file("noCameraDetected.mp3"); now = time(NULL);
sleep(10); // 失败后多睡一会儿 if (last_camera_warning_time == 0 ||
difftime(now, last_camera_warning_time) >= WIFI_OFFLINE_RECOVERY_INTERVAL_SEC) {
play_audio_file("noCameraDetected.mp3");
last_camera_warning_time = now;
}
sleep(5);
continue; continue;
} }
......
...@@ -21,6 +21,9 @@ ...@@ -21,6 +21,9 @@
// 启动断网自动配网流程 // 启动断网自动配网流程
void check_and_autoconfig_wifi(); void check_and_autoconfig_wifi();
// 运行期断网守护:每 40 秒播报、扫描并尝试连接已保存 WiFi
void *thread_wifi_offline_recovery(void *arg);
// 启动时加载离线语言配置(默认中文) // 启动时加载离线语言配置(默认中文)
void audio_init_offline_language(void); void audio_init_offline_language(void);
......
No preview for this file type
#include "common.h"
#include "audioplay.h"
#include "audio_sink.h" /* USB声卡排队锁 */
#include "device_identity.h"
#include "mqtt_init.h"
#include "http_config_mqtt.h"
#include "audiotts_play.h"
#include "wifi_autoconfig.h"
#include <stdio.h>
#include <pthread.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#define AUDIO_USB_ALSA_DEVICE "hw:2,0"
#define AUDIO_LOCAL_ALSA_DEVICE "plughw:2,0"
#define AUDIO_LOCAL_PLAY_TIMEOUT_SEC 8
static int s_audio_status=7;
static char s_urlbuf[512];
static char s_keybuf[256];
static double s_audio_volume=0.8;
#define AUDIO_DRIVING_LEVEL_MAX 30
static int s_local_play_pending = 0;
static char s_local_filepath[512];
static pthread_mutex_t s_local_play_mutex = PTHREAD_MUTEX_INITIALIZER;
static int local_is_cn_lang(const char *lang) {
return lang != NULL && (strcmp(lang, AUDIO_LANG_ZH) == 0 || strcmp(lang, "cn") == 0);
}
static int local_is_en_lang(const char *lang) {
return lang != NULL &&
(strcmp(lang, AUDIO_LANG_EN) == 0 || strcmp(lang, "en") == 0 || strcmp(lang, "EN") == 0);
}
static void local_resolve_lang(const char *language_override, char *out, size_t size) {
if (language_override == NULL || language_override[0] == '\0' ||
strcasecmp(language_override, "default") == 0) {
snprintf(out, size, "%s", audio_get_language());
return;
}
if (local_is_cn_lang(language_override)) {
snprintf(out, size, "%s", AUDIO_LANG_ZH);
return;
}
if (local_is_en_lang(language_override)) {
snprintf(out, size, "%s", AUDIO_LANG_EN);
return;
}
snprintf(out, size, "%s", audio_get_language());
my_zlog_warn("2017 未知 language: %s,使用当前语言: %s", language_override, out);
}
static int local_resolve_filepath(const char *filename, const char *language_override,
char *out, size_t size) {
char lang[8];
int use_default_lang = (language_override == NULL || language_override[0] == '\0' ||
strcasecmp(language_override, "default") == 0);
local_resolve_lang(language_override, lang, sizeof(lang));
if (local_is_cn_lang(lang)) {
snprintf(out, size, "%s/%s", AUDIO_LOCAL_BASE, filename);
return access(out, F_OK) == 0;
}
snprintf(out, size, "%s/%s/%s", AUDIO_LOCAL_BASE, lang, filename);
if (access(out, F_OK) == 0) {
return 1;
}
if (use_default_lang) {
snprintf(out, size, "%s/%s", AUDIO_LOCAL_BASE, filename);
return access(out, F_OK) == 0;
}
return 0;
}
static void local_queue_play(const char *filename, const char *language) {
char resolved[512];
if (!local_resolve_filepath(filename, language, resolved, sizeof(resolved))) {
my_zlog_warn("2017 本地音频不存在: %s", filename);
return;
}
pthread_mutex_lock(&s_local_play_mutex);
snprintf(s_local_filepath, sizeof(s_local_filepath), "%s", resolved);
s_local_play_pending = 1;
pthread_mutex_unlock(&s_local_play_mutex);
my_zlog_info("2017 已排队本地音频: %s", resolved);
}
void audioplay_local_mqtt_receive(cJSON *body) {
if (!cJSON_IsObject(body)) {
my_zlog_warn("2017 body 无效");
return;
}
cJSON *status = cJSON_GetObjectItemCaseSensitive(body, "status");
cJSON *level_item = cJSON_GetObjectItemCaseSensitive(body, "level");
cJSON *language = cJSON_GetObjectItemCaseSensitive(body, "language");
if (!cJSON_IsString(status) || status->valuestring == NULL) {
my_zlog_warn("2017 缺少 status");
return;
}
const char *lang = "default";
if (cJSON_IsString(language) && language->valuestring != NULL) {
lang = language->valuestring;
}
if (strcmp(status->valuestring, "speed") == 0) {
local_queue_play("Speedupforoneminute.mp3", lang);
return;
}
if (strcmp(status->valuestring, "drivinglevel") == 0) {
int level = 0;
if (cJSON_IsString(level_item) && level_item->valuestring != NULL) {
level = atoi(level_item->valuestring);
} else if (cJSON_IsNumber(level_item)) {
level = level_item->valueint;
} else {
my_zlog_warn("2017 drivinglevel 缺少 level");
return;
}
if (level < 1 || level > AUDIO_DRIVING_LEVEL_MAX) {
my_zlog_warn("2017 drivinglevel level 无效: %d (1-%d)",
level, AUDIO_DRIVING_LEVEL_MAX);
return;
}
char filename[48];
snprintf(filename, sizeof(filename), "DrivingLevel%d.mp3", level);
local_queue_play(filename, lang);
return;
}
my_zlog_warn("2017 未知 status: %s", status->valuestring);
}
static double audioplay_volume_clamp(double v) {
if (v < 0.0) {
return 0.0;
}
if (v > 2.0) {
return 2.0;
}
return v;
}
int audio_wheat_init();
static int audio_system_exit_code(int status) {
if (status == -1) {
return -1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
if (WIFSIGNALED(status)) {
return 128 + WTERMSIG(status);
}
return -1;
}
static void shell_single_quote(char *out, size_t size, const char *in) {
size_t pos = 0;
if (!out || size == 0) {
return;
}
out[pos++] = '\'';
if (in) {
for (const char *p = in; *p && pos + 5 < size; p++) {
if (*p == '\'') {
const char *esc = "'\\''";
for (const char *e = esc; *e && pos + 1 < size; e++) {
out[pos++] = *e;
}
} else {
out[pos++] = *p;
}
}
}
if (pos + 1 < size) {
out[pos++] = '\'';
}
out[pos] = '\0';
}
static int play_local_audio_file(const char *filepath) {
char quoted_path[1024];
char command[2048];
int ret;
int exit_code;
shell_single_quote(quoted_path, sizeof(quoted_path), filepath);
/* 排队等待USB声卡: 如果audio_sink正在播放手机音频, 等它释放 */
audio_sink_lock_alsa();
snprintf(command, sizeof(command),
/* sync=true 防止 EOS 提前关 ALSA 丢尾部(只播前半段); channels=2 上混规避 USB 声卡单声道 ring_buffer CRITICAL */
"timeout %ds gst-launch-1.0 -q filesrc location=%s ! mpegaudioparse ! mpg123audiodec ! audioconvert ! audioresample ! audio/x-raw,channels=2 ! alsasink device=%s sync=true >/dev/null 2>&1",
AUDIO_LOCAL_PLAY_TIMEOUT_SEC, quoted_path, AUDIO_LOCAL_ALSA_DEVICE);
ret = system(command);
exit_code = audio_system_exit_code(ret);
if (exit_code != 0) {
my_zlog_warn("本地音频 GStreamer 播放失败 exit=%d,尝试 ffplay: %s",
exit_code, filepath);
snprintf(command, sizeof(command),
"timeout %ds ffplay -nodisp -autoexit -loglevel warning %s",
AUDIO_LOCAL_PLAY_TIMEOUT_SEC, quoted_path);
ret = system(command);
exit_code = audio_system_exit_code(ret);
}
audio_sink_unlock_alsa();
return exit_code;
}
//接收音频播放
void audioplay_mqtt_receive(cJSON *json) {
// 解析"audioLink"字段(修正了原始JSON中的拼写错误)
cJSON *audio_link = cJSON_GetObjectItemCaseSensitive(json, "audioLink");
if (cJSON_IsString(audio_link) && (audio_link->valuestring != NULL)) {
my_zlog_debug("音频链接: %s", audio_link->valuestring);
char *url=audio_link->valuestring;
snprintf(s_urlbuf, sizeof(s_urlbuf), "%s", url);
} else {
my_zlog_warn("错误:无法解析音频链接字段");
}
// 解析"key"字段
cJSON *key_char = cJSON_GetObjectItemCaseSensitive(json, "key");
if (cJSON_IsString(key_char) && (key_char->valuestring != NULL)) {
my_zlog_debug("音频链接KEY: %s", key_char->valuestring);
char *key=key_char->valuestring;
snprintf(s_keybuf, sizeof(s_keybuf), "%s", key);
} else {
my_zlog_warn("错误:无法解析音频链接");
s_audio_status=5;
}
// 解析"status"字段
cJSON *s_audio_status_val = cJSON_GetObjectItemCaseSensitive(json, "status");
if (cJSON_IsNumber(s_audio_status_val)) {
my_zlog_debug("标志: %d", s_audio_status_val->valueint);
s_audio_status=s_audio_status_val->valueint;
} else {
my_zlog_warn("错误:无法解析标志字段");
}
// 解析"volume"字段
cJSON *volume = cJSON_GetObjectItemCaseSensitive(json, "volume");
if (cJSON_IsNumber(volume)) {
my_zlog_debug("声量大小: %.3f", volume->valuedouble);
s_audio_volume = audioplay_volume_clamp(volume->valuedouble);
} else {
my_zlog_warn("错误:无法解析声量字段");
}
}
//发送音频播放是否完毕
void audioplay_send_mqtt() {
cJSON *root = cJSON_CreateObject();
cJSON *body = cJSON_CreateObject();
cJSON *head = cJSON_CreateObject();
// 添加各个字段到 JSON 对象
cJSON_AddStringToObject(body, "type", "audio");
cJSON_AddStringToObject(body, "audioLink", s_urlbuf);
cJSON_AddStringToObject(body, "key", s_keybuf);
cJSON_AddNumberToObject(body, "status", s_audio_status);
cJSON_AddNumberToObject(body, "volume", s_audio_volume);
cJSON_AddNumberToObject(head, "message_type",3001);
cJSON_AddItemToObject(root, "body", body);
cJSON_AddItemToObject(root, "head",head);
// 将 JSON 对象转换为字符串
char* json_string = cJSON_PrintUnformatted(root);
my_zlog_debug("%s",json_string);
mqtt_publish_to_all(mqtt_topic_pure_number(), json_string, 0);
free(json_string);
cJSON_Delete(root);
}
//音频播放
void audioplay_cycle(){
char command[2048];
int ret;
while(1){
if(s_audio_status==0){
char *urlmoddle=s_urlbuf;
s_audio_volume = audioplay_volume_clamp(s_audio_volume);
snprintf(command, sizeof(command),
"sudo ffplay -nodisp -autoexit -loglevel quiet -af \"volume=%.3f\" \"%s\"",
s_audio_volume,urlmoddle);
my_zlog_debug("播放地址: %s", s_urlbuf);
my_zlog_debug("执行播放命令: %s", command);
ret = system(command);
if (ret != 0) {
my_zlog_error("播放失败");
s_audio_status=2;
}
if (WIFEXITED(ret) && WEXITSTATUS(ret) == 0) {
my_zlog_debug("播放已成功完成 : %s ", s_urlbuf);
s_audio_status=1;
} else {
my_zlog_warn("播放失败或中断: %s ", s_urlbuf);
s_audio_status=2;
}
audioplay_send_mqtt();
}
char local_filepath[sizeof(s_local_filepath)];
int local_play_pending = 0;
pthread_mutex_lock(&s_local_play_mutex);
if (s_local_play_pending) {
s_local_play_pending = 0;
snprintf(local_filepath, sizeof(local_filepath), "%s", s_local_filepath);
local_play_pending = 1;
}
pthread_mutex_unlock(&s_local_play_mutex);
if (local_play_pending) {
int exit_code;
my_zlog_info("播放本地音频: %s", local_filepath);
exit_code = play_local_audio_file(local_filepath);
if (exit_code == 0) {
my_zlog_debug("本地音频播放完成: %s", local_filepath);
} else {
my_zlog_warn("本地音频播放失败 exit=%d: %s", exit_code, local_filepath);
}
}
video_tts_play();
delay_ms(100);
}
}
static void trim_line(char *s)
{
if (!s)
return;
s[strcspn(s, "\r\n")] = '\0';
}
static int pulse_pactl_exit_code(int status)
{
if (status == -1)
return -1;
if (WIFEXITED(status))
return WEXITSTATUS(status);
return -1;
}
static int pulse_output_means_already_loaded(const char *output)
{
if (!output || !output[0])
return 0;
return strstr(output, "already loaded") != NULL ||
strstr(output, "Already loaded") != NULL ||
strstr(output, "Module exists") != NULL;
}
static int pulse_module_exists(const char *module_name, const char *device)
{
FILE *fp = popen("pactl list modules short 2>/dev/null", "r");
char line[512];
char needle[96];
if (!fp)
return 0;
snprintf(needle, sizeof(needle), "device=%s", device);
while (fgets(line, sizeof(line), fp) != NULL) {
if (strstr(line, module_name) != NULL && strstr(line, needle) != NULL) {
pclose(fp);
return 1;
}
}
pclose(fp);
return 0;
}
static int pulse_load_alsa_module(const char *module_name, const char *label)
{
char cmd[256];
char output[512];
char line[256];
if (pulse_module_exists(module_name, AUDIO_USB_ALSA_DEVICE)) {
my_zlog_info("%s 已注册,跳过 (%s device=%s)",
label, module_name, AUDIO_USB_ALSA_DEVICE);
return 0;
}
snprintf(cmd, sizeof(cmd), "pactl load-module %s device=%s 2>&1",
module_name, AUDIO_USB_ALSA_DEVICE);
FILE *fp = popen(cmd, "r");
if (!fp) {
my_zlog_warn("%s 注册失败: 无法执行 pactl (%s)", label, strerror(errno));
return -1;
}
output[0] = '\0';
while (fgets(line, sizeof(line), fp) != NULL) {
trim_line(line);
if (!line[0])
continue;
if (output[0]) {
strncat(output, "; ", sizeof(output) - strlen(output) - 1);
}
strncat(output, line, sizeof(output) - strlen(output) - 1);
}
int status = pclose(fp);
int exit_code = pulse_pactl_exit_code(status);
if (exit_code == 0) {
my_zlog_info("%s 注册成功 (%s device=%s)%s%s",
label, module_name, AUDIO_USB_ALSA_DEVICE,
output[0] ? ", pactl: " : "", output[0] ? output : "");
return 0;
}
if (pulse_output_means_already_loaded(output) ||
pulse_module_exists(module_name, AUDIO_USB_ALSA_DEVICE)) {
my_zlog_info("%s 已注册,跳过 (%s device=%s)%s%s",
label, module_name, AUDIO_USB_ALSA_DEVICE,
output[0] ? ": " : "", output[0] ? output : "");
return 0;
}
my_zlog_warn("%s 注册失败 (%s device=%s): exit=%d%s%s",
label, module_name, AUDIO_USB_ALSA_DEVICE, exit_code,
output[0] ? ", pactl: " : ", pactl 无输出",
output[0] ? output : "");
return -1;
}
int audio_wheat_init(){
return pulse_load_alsa_module("module-alsa-sink", "喇叭");
}
int audio_speaker_init() {
return pulse_load_alsa_module("module-alsa-source", "麦");
}
int audio_init(){
delay_s(1);
audio_wheat_init();
delay_s(1);
audio_speaker_init();
return 0;
}
#define CONFIG_FILE "/usr/share/pulseaudio/alsa-mixer/profile-sets/default.conf"
static const char *s_yundea_block =
"# 为 Yundea 1076 USB 二合一设备添加配置\n"
"[Mapping yundea-input]\n"
"device-strings = hw:%f,0\n"
"direction = input\n"
"priority = 100\n"
"channel-map = mono\n"
"\n"
"[Mapping yundea-output]\n"
"device-strings = hw:%f,0\n"
"direction = output\n"
"priority = 100\n"
"channel-map = left,right\n"
"\n"
"[Profile yundea-duplex]\n"
"input-mappings = yundea-input\n"
"output-mappings = yundea-output\n"
"priority = 200\n";
/*
*功能:加入usb声卡配置
*
*/
int audio_config_init() {
delay_s(12);
FILE *fp = fopen(CONFIG_FILE, "r");
if (!fp) {
perror("无法打开配置文件进行读取");
return 1;
}
// 获取文件大小
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
fseek(fp, 0, SEEK_SET);
char *buffer = malloc(size + 1);
if (!buffer) {
my_zlog_error("内存分配失败");
fclose(fp);
return 1;
}
size_t result =fread(buffer, 1, size, fp);
if (result != size) {
my_zlog_warn("Error: Failed to read audio data");
}
buffer[size] = '\0';
fclose(fp);
// 检查是否包含完整配置块
if (strstr(buffer, s_yundea_block) != NULL) {
my_zlog_debug("配置文件中已包含指定配置,跳过添加。");
free(buffer);
return 0;
}
free(buffer);
my_zlog_info("未找到指定配置,正在追加到文件末尾...");
// 打开文件追加配置
fp = fopen(CONFIG_FILE, "a");
if (!fp) {
my_zlog_error("无法打开配置文件进行写入");
return 1;
}
fprintf(fp, "\n%s", s_yundea_block);
fclose(fp);
my_zlog_info("配置已成功追加。");
return 0;
}
#ifndef AUDIOPLAY_H__
#define AUDIOPLAY_H__
#include <cjson/cJSON.h>
void audioplay_mqtt_receive(cJSON *body); //接收音频mqtt播放函数
void audioplay_local_mqtt_receive(cJSON *body); // 2017 有人驾驶本地音频
void audioplay_cycle();//音频播放线程中函数
int audio_wheat_init();
int audio_speaker_init();
int audio_init();
int audio_config_init();//加入配置
#endif
\ No newline at end of file
#include "audiotts_play.h"
#include "audioplay.h"
#include "device_identity.h"
#include "mqtt_init.h"
#include "http_config_mqtt.h"
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#define TTS_ADUINO_PTAH PROJECT_ROOT_DIR "/audio.mp3"
// 全局变量:存储 HTTP 响应数据
static char *s_http_response = NULL;
static size_t s_http_response_len = 0;
static bool s_audio_tts_index=0;
static double s_volume=1.0;
// HTTP 响应回调函数:接收服务器返回的 JSON 数据
static size_t http_write_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {
size_t total = size * nmemb;
// 扩容并拷贝响应数据
char *temp = realloc(s_http_response, s_http_response_len + total + 1);
if (!temp) {
my_zlog_error("Realloc response buffer failed");
return 0;
}
s_http_response = temp;
memcpy(s_http_response + s_http_response_len, ptr, total);
s_http_response_len += total;
s_http_response[s_http_response_len] = '\0'; // 末尾加结束符
return total;
}
// 生成 UUID(去除连字符,保证 reqid 唯一性)
static void generate_uuid(char *buf, size_t len) {
if (len < 33) return; // UUID 去除连字符后 32 位
uuid_t uuid;
uuid_generate_random(uuid);
uuid_unparse(uuid, buf);
// 移除连字符
char *p = strchr(buf, '-');
while (p) {
memmove(p, p + 1, strlen(p));
p = strchr(buf, '-');
}
}
// Base64 解码(将响应中的音频数据解码为二进制)
static unsigned char *base64_decode(const char *input, size_t *output_len) {
BIO *bio, *b64;
size_t len = strlen(input);
unsigned char *buffer = (unsigned char *)malloc(len * 3 / 4 + 1024); // 预留足够空间
if (!buffer) return NULL;
// 初始化 OpenSSL Base64 解码
OpenSSL_add_all_algorithms(); // 初始化 OpenSSL 算法(避免解码失败)
b64 = BIO_new(BIO_f_base64());
bio = BIO_new_mem_buf(input, len);
bio = BIO_push(b64, bio);
// 禁用换行符处理(关键:避免 Base64 字符串含换行导致解码失败)
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
*output_len = BIO_read(bio, buffer, len * 3 / 4 + 1024);
// 清理资源
BIO_free_all(bio);
// 调整缓冲区大小为实际解码长度
buffer = realloc(buffer, *output_len);
return buffer;
}
/**
* 保存音频数据到文件(目录已存在)
* @param data 音频数据指针
* @param len 音频数据长度
* @param filepath 完整文件路径
* @return 0成功,1失败
*/
static int save_audio_file(const void *data, size_t len, const char *filepath) {
if (!data || len == 0 || !filepath) {
my_zlog_error("Invalid parameters for save_audio_file\n");
return 1;
}
FILE *fp = fopen(filepath, "wb");
if (!fp) {
my_zlog_error( "Open file %s failed: %s\n", filepath, strerror(errno));
return 1;
}
size_t written = fwrite(data, 1, len, fp);
fclose(fp);
if (written != len) {
my_zlog_error("Write file %s incomplete: %zu/%zu bytes", filepath, written, len);
remove(filepath); // 删除不完整的文件
return 1;
}
my_zlog_debug("Success! Audio saved to: %s (%zu bytes)", filepath, written);
return 0;
}
/*
*功能:获得随机字符串,不使用uuid
*/
static char* generate_random_string() {
// 设置随机种子(建议在main函数中调用一次即可)
static int seeded = 0;
if (!seeded) {
srand((unsigned int)time(NULL));
seeded = 1;
}
// 动态分配内存(10个字符 + 终止符)
char* str = (char*)malloc(11 * sizeof(char));
if (!str) return NULL;
// 生成数字字符(ASCII 48-57)
for (int i = 0; i < 10; i++) {
str[i] = '0' + (rand() % 10);
}
str[10] = '\0'; // 添加终止符
return str;
}
char *get_audio_text_tts(char *date ,bool flag){
static char text[256] = {0};
if(flag==1){
return text;
}else {
size_t date_len = strlen(date);
if (date) {
strcpy(text, date);
text[date_len] = '\0'; // 确保以null结尾
} else {
text[0] = '\0'; // 如果date为NULL,清空文本
}
}
}
/**
* 保存音频数据到文件(目录已存在)
* @param text 传入需要转化的文字指针
* @return 0成功,1失败
*/
int audio_tts(const char *text) {
// 1. 配置参数(替换为你的真实信息,已验证有效)
const char *appid = "5745089507"; // 你的火山引擎 AppID
const char *access_token = "0BeoBb_DOjrxfM4aTsJfUFVVS0k5V9mt"; // 你的访问令牌
const char *voice_type = "zh_female_linjianvhai_moon_bigtts"; // 非流式音色
const char *cluster = "volcano_tts"; // 固定值(必选)
char *uid = generate_random_string(); // 任意非空字符串
const char *encoding = "mp3"; // 输出格式(mp3/wav/pcm/ogg_opus)
const char *endpoint = "https://openspeech.bytedance.com/api/v1/tts"; // HTTP 接口地址
// 2. 生成唯一 reqid(每次调用必须不同)
char reqid[33] = {0};
generate_uuid(reqid, sizeof(reqid));
my_zlog_debug("Generated ReqID: %s", reqid);
// 3. 构建请求 JSON(严格遵循接口参数格式)
cJSON *root = cJSON_CreateObject();
if (!root) {
my_zlog_error("Create JSON root failed");
free(uid);
return 1;
}
// app 节点(必选)
cJSON *app = cJSON_CreateObject();
cJSON_AddStringToObject(app, "appid", appid);
cJSON_AddStringToObject(app, "token", access_token);
cJSON_AddStringToObject(app, "cluster", cluster);
cJSON_AddItemToObject(root, "app", app);
// user 节点(必选)
cJSON *user = cJSON_CreateObject();
cJSON_AddStringToObject(user, "uid", uid);
cJSON_AddItemToObject(root, "user", user);
// audio 节点(必选)
cJSON *audio = cJSON_CreateObject();
cJSON_AddStringToObject(audio, "voice_type", voice_type);
cJSON_AddStringToObject(audio, "encoding", encoding);
cJSON_AddNumberToObject(audio, "speed_ratio", 1.0); // 语速(0.1~2.0)
cJSON_AddNumberToObject(audio, "rate", 16000); // 采样率(默认24000)
cJSON_AddNumberToObject(audio, "loudness_ratio", 1.0); // 音量(0.5~2.0)
cJSON_AddBoolToObject(audio, "enable_emotion", true); // 开启情感
cJSON_AddStringToObject(audio, "emotion", "gentle"); // 情感类型(gentle=温柔)
cJSON_AddNumberToObject(audio, "emotion_scale", 4.0); // 情感强度(1~5,默认4)
cJSON_AddItemToObject(root, "audio", audio);
// request 节点(必选)
cJSON *request = cJSON_CreateObject();
cJSON_AddStringToObject(request, "reqid", reqid);
cJSON_AddStringToObject(request, "text", text);
cJSON_AddStringToObject(request, "operation", "query"); // HTTP 非流式固定为 query
cJSON_AddStringToObject(request, "extra_param", "{\"disable_markdown_filter\":true}"); // 附加参数
cJSON_AddItemToObject(root, "request", request);
// 生成 JSON 字符串(无格式,减少传输体积)
char *payload = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!payload) {
my_zlog_error("Generate JSON payload failed");
free(uid);
return 1;
}
my_zlog_debug("Request Payload: %s", payload);
// 4. 初始化 curl 并配置 HTTP 请求
curl_global_init(CURL_GLOBAL_DEFAULT);
CURL *curl = curl_easy_init();
if (!curl) {
my_zlog_error("Initialize curl failed");
free(payload);
free(uid);
return 1;
}
// 设置请求头(关键:Authorization 分号分隔,Content-Type 为 JSON)
struct curl_slist *headers = NULL;
char auth_header[256] = {0};
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer;%s", access_token);
headers = curl_slist_append(headers, auth_header);
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "Accept: application/json");
if (s_http_response == NULL) {
s_http_response_len = 0;
}else {
return 1;
}
// curl 核心配置
curl_easy_setopt(curl, CURLOPT_URL, endpoint);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POST, 1L); // POST 方法
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload); // 请求体
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(payload));
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_write_callback); // 接收响应回调
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 开发环境禁用 SSL 证书校验(生产环境建议启用)
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); // 超时时间 15 秒(避免网络慢导致超时)
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); // 连接超时 10 秒
// 5. 执行 HTTP 请求
my_zlog_debug("Sending request to %s...", endpoint);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
my_zlog_error("curl_easy_perform failed: %s", curl_easy_strerror(res));
free(uid);
free(payload);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
// 6. 解析 HTTP 响应(JSON 格式)
my_zlog_debug("Response Length: %zu bytes", s_http_response_len);
if (s_http_response_len == 0) {
my_zlog_error("Empty response from server");
free(uid);
free(payload);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
// 解析 JSON 响应
cJSON *response_json = cJSON_Parse(s_http_response);
if (!response_json) {
my_zlog_error("Parse response JSON failed: %s", cJSON_GetErrorPtr());
free(uid);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
// // 打印完整响应(便于调试,可根据需要注释)
// char *response_str = cJSON_Print(response_json);
// printf("Full Response JSON (abbreviated):\n%s\n", response_str);
// free(response_str);
// 提取响应状态(火山引擎该接口成功码:3000 + message=Success)
cJSON *code = cJSON_GetObjectItem(response_json, "code");
cJSON *message = cJSON_GetObjectItem(response_json, "message");
if (!code || code->valueint != 3000 || !message || strcmp(message->valuestring, "Success") != 0) {
my_zlog_error("Request failed! Code: %d, Message: %s",
code ? code->valueint : -1,
message ? message->valuestring : "No message");
cJSON_Delete(response_json);
free(uid);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
my_zlog_debug("Request succeeded! Code: %d, Message: %s", code->valueint, message->valuestring);
// 关键:直接提取 data 字段的 Base64 音频字符串(接口实际返回格式)
cJSON *audio_base64 = cJSON_GetObjectItem(response_json, "data");
if (!audio_base64 || !audio_base64->valuestring || strlen(audio_base64->valuestring) == 0) {
my_zlog_error("Response missing audio data (data field is empty)");
cJSON_Delete(response_json);
free(uid);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
my_zlog_debug("Audio Base64 Length: %zu bytes", strlen(audio_base64->valuestring));
// 7. Base64 解码音频数据
size_t audio_len = 0;
unsigned char *audio_data = base64_decode(audio_base64->valuestring, &audio_len);
if (!audio_data || audio_len == 0) {
my_zlog_error("Base64 decode audio failed");
cJSON_Delete(response_json);
free(uid);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
my_zlog_debug("Decoded Audio Length: %zu bytes", audio_len);
if(save_audio_file(audio_data, audio_len, TTS_ADUINO_PTAH) != 0) {//保存文件
my_zlog_error("save_audio_file fail");
}
// 9. 清理资源(避免内存泄漏)
cJSON_Delete(response_json);
free(uid);
free(audio_data);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
// 初始化响应缓冲
s_http_response = NULL;
s_http_response_len = 0;
return 0;
}
/*
*功能: 设置播放标识
*/
void set_audio_tts_index(bool index){
s_audio_tts_index=index;
}
/*
*功能: 发送mqtt
*/
int mqtt_audio_tts_send(int status){
// 创建根对象
cJSON* root = cJSON_CreateObject();
if (root == NULL) {
return 1;
}
// 创建body对象
cJSON* body = cJSON_CreateObject();
if (body == NULL) {
cJSON_Delete(root);
return 1;
}
cJSON *head = cJSON_CreateObject();
if (head == NULL) {
cJSON_Delete(root);
return 1;
}
// 将body添加到root
cJSON_AddItemToObject(root, "body", body);
// 添加type和status字段到body
cJSON_AddStringToObject(body, "type", "tts_audio");
cJSON_AddNumberToObject(body, "status", status);
cJSON_AddNumberToObject(head, "message_type",3020);
cJSON_AddItemToObject(root, "body", body);
cJSON_AddItemToObject(root, "head",head);
// 生成JSON字符串
char* json_str = cJSON_PrintUnformatted(root);
my_zlog_debug("%s",json_str);
mqtt_publish_to_all(mqtt_topic_pure_number(), json_str, 0);
free(json_str);
cJSON_Delete(root);
return 0;
}
int mqtt_audio_tts(cJSON *body){
// 解析type字段
cJSON* type = cJSON_GetObjectItem(body, "type");
if(type == NULL || strcmp(type->valuestring, "tts_audio") != 0){
my_zlog_error("type is tts_audio error");
return 1;
}
// 解析date字段
cJSON* date = cJSON_GetObjectItem(body, "date");
if(date!=NULL){
get_audio_text_tts(date->valuestring,0);
my_zlog_debug("text:%s",date->valuestring);
}else {
return 1;
}
// 解析status字段
cJSON* status = cJSON_GetObjectItem(body, "status");
if(mqtt_audio_tts_send(status->valueint)==0){
set_audio_tts_index(status->valueint);
}else{
set_audio_tts_index(0);
my_zlog_error("statis error");
return 1;
}
cJSON* volume = cJSON_GetObjectItem(body, "volume");
s_volume = volume->valuedouble;
return 0;
}
/*
*功能,tts播放
*/
void video_tts_play() {
if(s_audio_tts_index == 1) {
if(audio_tts(get_audio_text_tts(NULL, 1))!=0){
my_zlog_error("audio_tts fail");
s_audio_tts_index = 0;
return;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:创建新的会话组,脱离父进程组
setsid(); // 关键!创建新的会话组
char command[256];
snprintf(command, sizeof(command),
"ffmpeg -i " TTS_ADUINO_PTAH " -af \"volume=%.1f\" -f alsa default 2>/dev/null",
s_volume);
// 重定向标准输入输出,避免占用终端
if (freopen("/dev/null", "r", stdin) == NULL) {
my_zlog_error("重定向stdin失败: %s", strerror(errno));
// 可以选择退出或继续
}
if (freopen("/dev/null", "w", stdout) == NULL) {
my_zlog_error("重定向stdout失败: %s", strerror(errno));
}
if (freopen("/dev/null", "w", stderr) == NULL) {
my_zlog_error("重定向stderr失败: %s", strerror(errno));
}
int res = system(command);
_exit(res); // 使用_exit避免清理父进程资源
}
else if (pid > 0) {
// 父进程:非阻塞等待,避免僵尸进程
int status;
waitpid(pid, &status, WNOHANG); // 不阻塞的等待
my_zlog_debug("启动独立播放进程 PID: %d", pid);
s_audio_tts_index = 0;
}
else {
my_zlog_error("fork()失败");
s_audio_tts_index = 0;
}
}
}
// void video_tts_play(){
// if(s_audio_tts_index == 1) {
// audio_tts(get_audio_text_tts(NULL,1));
// char command[256];
// snprintf(command, sizeof(command),
// "ffmpeg -i " TTS_ADUINO_PTAH " -af \"volume=%.1f\" -f alsa default",
// s_volume);
// int res = system(command);
// if(res == 0)
// my_zlog_info("play success, volume: %.1f", s_volume);
// else
// my_zlog_error("system play fail, volume: %.1f", s_volume);
// s_audio_tts_index == 0;
// }
// }
\ No newline at end of file
...@@ -9,4 +9,4 @@ file perms = 600 ...@@ -9,4 +9,4 @@ file perms = 600
millisecond = "%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n" millisecond = "%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n"
[rules] [rules]
my_log.* "/home/orangepi/car/master/log/log_2026-07-14.log"; millisecond my_log.* "/home/orangepi/car/master/log/log_2026-07-20.log"; millisecond
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment