2025-07-03 15:12:57 +08:00
|
|
|
|
#include "cm_iomux.h"
|
|
|
|
|
|
#include "cm_gpio.h"
|
|
|
|
|
|
#include "stdio.h"
|
|
|
|
|
|
#include "stdlib.h"
|
|
|
|
|
|
#include "stdarg.h"
|
|
|
|
|
|
#include "math.h"
|
|
|
|
|
|
#include "cm_os.h"
|
|
|
|
|
|
#include "cm_mem.h"
|
|
|
|
|
|
#include "cm_sys.h"
|
|
|
|
|
|
#include "cm_uart.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include "app_common.h"
|
|
|
|
|
|
#include "app_uart.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include "attr_broadcast.h"
|
|
|
|
|
|
#include "gps_config.h"
|
|
|
|
|
|
#include "local_tts.h"
|
|
|
|
|
|
|
2025-07-10 10:01:23 +08:00
|
|
|
|
#if 1
|
2025-07-03 15:12:57 +08:00
|
|
|
|
#include "app_uart.h"
|
2025-07-10 10:01:23 +08:00
|
|
|
|
#define DEBUG(fmt, args...) app_printf("[Broadcast]" fmt, ##args)
|
2025-07-03 15:12:57 +08:00
|
|
|
|
#else
|
|
|
|
|
|
#include "app_uart.h"
|
|
|
|
|
|
#define DEBUG(fmt, ...)
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
#define MAX_ATTRACTIONS 50 // 最大景点数量
|
|
|
|
|
|
#define PLAY_DISTANCE_THRESHOLD 50.0 // 有效播报距离(米)
|
|
|
|
|
|
#define DISTANCE_CHANGE_THRESHOLD 10.0 // 距离变化阈值(米)
|
|
|
|
|
|
#define TTS_PRIORITY 5 // TTS播报优先级
|
|
|
|
|
|
|
|
|
|
|
|
static nmeaPARSER parser;
|
|
|
|
|
|
|
2025-07-10 10:05:00 +08:00
|
|
|
|
//该版本仅实现基本的靠近播报功能,长文字播放功能待开发
|
|
|
|
|
|
|
2025-07-03 15:12:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 全局景点链表
|
|
|
|
|
|
typedef struct AttractionNode {
|
|
|
|
|
|
Attraction attraction;
|
|
|
|
|
|
struct AttractionNode* next;
|
|
|
|
|
|
} AttractionNode;
|
|
|
|
|
|
|
|
|
|
|
|
// 播报状态
|
|
|
|
|
|
typedef struct {
|
2025-07-10 10:01:23 +08:00
|
|
|
|
AttractionNode* last_broadcast;
|
|
|
|
|
|
double last_distance;
|
|
|
|
|
|
uint8_t is_playing;
|
|
|
|
|
|
double distance_threshold;
|
2025-07-03 15:12:57 +08:00
|
|
|
|
} BroadcastState;
|
|
|
|
|
|
|
2025-07-10 10:01:23 +08:00
|
|
|
|
|
|
|
|
|
|
typedef int BOOL;
|
|
|
|
|
|
#define true 1
|
|
|
|
|
|
#define false 0
|
|
|
|
|
|
|
|
|
|
|
|
const char *park_desc[] = {
|
|
|
|
|
|
"尊敬的游客欢迎来到顾村公园游玩",
|
|
|
|
|
|
"顾村公园,位于上海市宝山区顾村镇沪太路4788号",
|
|
|
|
|
|
"东起沪太路,西至陈广路,北邻镜泊湖路,南靠外环",
|
|
|
|
|
|
"高速公路,占帝总面积430公顷,其中一期占帝180",
|
|
|
|
|
|
"公顷,二期占帝244.7公顷,顾村公园一期于2007年8月",
|
|
|
|
|
|
"动工建设,二期于2012年10月22日动工建设.公园",
|
|
|
|
|
|
"建设理念为“亲民人文,以生态休闲娱乐为主题.",
|
|
|
|
|
|
"集生态防护、景观观赏、休闲健身、文化娱乐旅游",
|
|
|
|
|
|
"度假等功能于一体,与传统的城市公园形成功能互补",
|
|
|
|
|
|
"并以中心河为界分两期实施。顾村公园为国家4A级",
|
|
|
|
|
|
"景区,上海市科普教育基地、上海市志愿者服务基地等。"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static osMutexId_t attractions_mutex = NULL;
|
|
|
|
|
|
static AttractionNode* attractions_head = NULL;
|
|
|
|
|
|
static BroadcastState broadcast_state = {0};
|
|
|
|
|
|
static int tts_speed = 5;
|
|
|
|
|
|
static int tts_volume = 10;
|
|
|
|
|
|
static osThreadId_t Attr_Broadcast_ThreadId = NULL; //串口数据接收、解析任务Handle
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//多文字tts,景区播报专用
|
|
|
|
|
|
void safe_tts_play(const char* segments[], int count) {
|
|
|
|
|
|
for(int i = 0; i < count; i++) {
|
|
|
|
|
|
local_tts_text_play(segments[i], 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 等待当前播放完成
|
|
|
|
|
|
while(local_tts_get_play_state() != 0) {
|
|
|
|
|
|
osDelay(100); // 每100ms检查一次
|
|
|
|
|
|
}
|
|
|
|
|
|
osDelay(200); // 额外缓冲
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-03 15:12:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//计算到景点的距离
|
|
|
|
|
|
double location_service_calculate_distance(Location loc1, Location loc2) {
|
|
|
|
|
|
// 使用Haversine公式计算真实距离
|
|
|
|
|
|
const double R = 6371000; // 地球半径(米)
|
|
|
|
|
|
double dLat = (loc2.latitude - loc1.latitude) * M_PI / 180.0;
|
|
|
|
|
|
double dLon = (loc2.longitude - loc1.longitude) * M_PI / 180.0;
|
|
|
|
|
|
|
|
|
|
|
|
double a = sin(dLat/2) * sin(dLat/2) +
|
|
|
|
|
|
cos(loc1.latitude * M_PI / 180.0) *
|
|
|
|
|
|
cos(loc2.latitude * M_PI / 180.0) *
|
|
|
|
|
|
sin(dLon/2) * sin(dLon/2);
|
|
|
|
|
|
|
|
|
|
|
|
double c = 2 * atan2(sqrt(a), sqrt(1-a));
|
|
|
|
|
|
return R * c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前位置
|
|
|
|
|
|
Location location_service_get_current(void) {
|
|
|
|
|
|
Location current = {0};
|
|
|
|
|
|
|
|
|
|
|
|
// 加锁保护全局数据
|
2025-07-10 10:01:23 +08:00
|
|
|
|
osMutexAcquire(gps_data.mutex, osWaitForever);
|
2025-07-03 15:12:57 +08:00
|
|
|
|
// 检查数据是否有效
|
|
|
|
|
|
if (gps_data.info.sig > 0 && gps_data.info.fix > 0) {
|
2025-07-10 10:01:23 +08:00
|
|
|
|
current.longitude = gps_data.longitude;
|
|
|
|
|
|
current.latitude = gps_data.latitude;
|
2025-07-03 15:12:57 +08:00
|
|
|
|
}
|
2025-07-10 10:01:23 +08:00
|
|
|
|
osMutexRelease(gps_data.mutex);
|
|
|
|
|
|
|
2025-07-03 15:12:57 +08:00
|
|
|
|
|
|
|
|
|
|
return current;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 添加景点
|
|
|
|
|
|
void attr_broadcast_add_attraction(double lon, double lat,
|
|
|
|
|
|
const char* name, const char* desc) {
|
|
|
|
|
|
if (attractions_mutex == NULL) return;
|
|
|
|
|
|
|
|
|
|
|
|
osMutexAcquire(attractions_mutex, osWaitForever);
|
|
|
|
|
|
|
|
|
|
|
|
AttractionNode* new_node = malloc(sizeof(AttractionNode));
|
|
|
|
|
|
if (!new_node) {
|
|
|
|
|
|
osMutexRelease(attractions_mutex);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 填充景点信息
|
|
|
|
|
|
new_node->attraction.longitude = lon;
|
|
|
|
|
|
new_node->attraction.latitude = lat;
|
|
|
|
|
|
strncpy(new_node->attraction.name, name, sizeof(new_node->attraction.name)-1);
|
|
|
|
|
|
strncpy(new_node->attraction.description, desc, sizeof(new_node->attraction.description)-1);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加到链表头部
|
|
|
|
|
|
new_node->next = attractions_head;
|
|
|
|
|
|
attractions_head = new_node;
|
|
|
|
|
|
|
|
|
|
|
|
osMutexRelease(attractions_mutex);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 设置播报距离阈值 (米)
|
|
|
|
|
|
void attr_broadcast_set_distance_threshold(double threshold) {
|
|
|
|
|
|
if (threshold > 0) {
|
|
|
|
|
|
broadcast_state.distance_threshold = threshold;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 根据当前位置查找最近的景区节点
|
|
|
|
|
|
static AttractionNode* find_nearest_attraction(Location current_pos) {
|
|
|
|
|
|
if (attractions_head == NULL) return NULL;
|
|
|
|
|
|
|
|
|
|
|
|
osMutexAcquire(attractions_mutex, osWaitForever);
|
|
|
|
|
|
|
|
|
|
|
|
AttractionNode* current = attractions_head;
|
|
|
|
|
|
AttractionNode* nearest = attractions_head;
|
|
|
|
|
|
double min_distance = location_service_calculate_distance(
|
|
|
|
|
|
(Location){current->attraction.longitude, current->attraction.latitude},
|
|
|
|
|
|
current_pos
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
while (current != NULL) {
|
|
|
|
|
|
double distance = location_service_calculate_distance(
|
|
|
|
|
|
(Location){current->attraction.longitude, current->attraction.latitude},
|
|
|
|
|
|
current_pos
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (distance < min_distance) {
|
|
|
|
|
|
min_distance = distance;
|
|
|
|
|
|
nearest = current;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
current = current->next;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
osMutexRelease(attractions_mutex);
|
|
|
|
|
|
return nearest;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static void play_attraction_info(AttractionNode* attraction, double distance) {
|
|
|
|
|
|
if (!attraction) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 构建播报文本
|
|
|
|
|
|
char buffer[300];
|
|
|
|
|
|
snprintf(buffer, sizeof(buffer), "您已到达%s,%s。距离%.1f米。",
|
|
|
|
|
|
attraction->attraction.name,
|
|
|
|
|
|
attraction->attraction.description,
|
|
|
|
|
|
distance);
|
2025-07-10 10:01:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
broadcast_state.is_playing = 1;
|
2025-07-03 15:12:57 +08:00
|
|
|
|
// 调用TTS接口播放文本
|
|
|
|
|
|
local_tts_text_play(buffer, 0, 1);
|
|
|
|
|
|
|
2025-07-10 10:01:23 +08:00
|
|
|
|
//等待
|
|
|
|
|
|
while(local_tts_get_play_state() != 0) {
|
|
|
|
|
|
osDelay(100);
|
|
|
|
|
|
}
|
2025-07-03 15:12:57 +08:00
|
|
|
|
// 更新播报状态
|
|
|
|
|
|
broadcast_state.last_broadcast = attraction;
|
|
|
|
|
|
broadcast_state.last_distance = distance;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static void attr_broadcast_task(void* arg) {
|
|
|
|
|
|
(void)arg; // 避免未使用参数警告
|
2025-07-10 10:01:23 +08:00
|
|
|
|
BOOL should_play; //是否播放最后判断量
|
|
|
|
|
|
BOOL introduc=true; //是否播报景区介绍
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEBUG("task begin\r\n");
|
|
|
|
|
|
|
|
|
|
|
|
if(0)
|
|
|
|
|
|
{
|
|
|
|
|
|
safe_tts_play(park_desc,11);
|
2025-07-03 15:12:57 +08:00
|
|
|
|
|
2025-07-10 10:01:23 +08:00
|
|
|
|
introduc=false;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2025-07-03 15:12:57 +08:00
|
|
|
|
while (1) {
|
|
|
|
|
|
// 获取当前位置
|
|
|
|
|
|
Location current_pos = location_service_get_current();
|
|
|
|
|
|
|
|
|
|
|
|
// 查找最近景点
|
|
|
|
|
|
AttractionNode* nearest = find_nearest_attraction(current_pos);
|
2025-07-10 10:01:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEBUG("location ok\r\n");
|
2025-07-03 15:12:57 +08:00
|
|
|
|
if (nearest != NULL) {
|
|
|
|
|
|
// 计算距离
|
|
|
|
|
|
double distance = location_service_calculate_distance(
|
|
|
|
|
|
(Location){nearest->attraction.longitude, nearest->attraction.latitude},
|
|
|
|
|
|
current_pos
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-07-10 10:01:23 +08:00
|
|
|
|
DEBUG("calculate ok\r\n");
|
|
|
|
|
|
|
2025-07-03 15:12:57 +08:00
|
|
|
|
// 检查是否在有效范围内
|
|
|
|
|
|
if (distance <= broadcast_state.distance_threshold) {
|
|
|
|
|
|
// 检查是否需要播报
|
2025-07-10 10:01:23 +08:00
|
|
|
|
should_play = false;
|
|
|
|
|
|
DEBUG("check ok:%d\r\n",should_play);
|
2025-07-03 15:12:57 +08:00
|
|
|
|
|
|
|
|
|
|
if (broadcast_state.last_broadcast == NULL) {
|
|
|
|
|
|
should_play = true; // 首次播报
|
2025-07-10 10:01:23 +08:00
|
|
|
|
DEBUG("check ok:%d\r\n",should_play);
|
2025-07-03 15:12:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
else if (broadcast_state.last_broadcast != nearest) {
|
|
|
|
|
|
should_play = true; // 新景点
|
2025-07-10 10:01:23 +08:00
|
|
|
|
|
2025-07-03 15:12:57 +08:00
|
|
|
|
}
|
2025-07-10 10:01:23 +08:00
|
|
|
|
else if (fabs(distance - broadcast_state.last_distance) > 50.0) {
|
|
|
|
|
|
should_play = true; // 同一景点但距离变化大 是否需要播报可以商榷
|
2025-07-03 15:12:57 +08:00
|
|
|
|
}
|
2025-07-10 10:01:23 +08:00
|
|
|
|
DEBUG("stat ok:%d\r\n",should_play);
|
2025-07-03 15:12:57 +08:00
|
|
|
|
// 触发播报
|
|
|
|
|
|
if (should_play && !broadcast_state.is_playing) {
|
2025-07-10 10:01:23 +08:00
|
|
|
|
DEBUG("ready to play\r\n");
|
2025-07-03 15:12:57 +08:00
|
|
|
|
play_attraction_info(nearest, distance);
|
2025-07-10 10:01:23 +08:00
|
|
|
|
broadcast_state.is_playing=0;
|
|
|
|
|
|
DEBUG("play ok\r\n");
|
2025-07-03 15:12:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-10 10:01:23 +08:00
|
|
|
|
DEBUG("dist:%0.3f,threshold:%0.3f\r\n",distance,broadcast_state.distance_threshold);
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-03 15:12:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5秒检测一次
|
2025-07-10 10:01:23 +08:00
|
|
|
|
osDelay(5000/5);
|
2025-07-03 15:12:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-10 10:01:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-07-03 15:12:57 +08:00
|
|
|
|
void attr_broadcast_init(void) {
|
|
|
|
|
|
// 初始化互斥锁
|
|
|
|
|
|
if (attractions_mutex == NULL) {
|
|
|
|
|
|
attractions_mutex = osMutexNew(NULL);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-10 10:01:23 +08:00
|
|
|
|
//增加初始景点
|
|
|
|
|
|
attr_broadcast_add_attraction(121.364371, 31.343164, "2号门便民处", "测试景点,看起来应该非常的方便游客");
|
|
|
|
|
|
attr_broadcast_add_attraction(121.364270, 31.343103, "2号门归还点", "您好二号门停放点到了,您可以把我归还至这里,并在手机小程序中结束订单,如果");
|
|
|
|
|
|
attr_broadcast_add_attraction(121.364350, 31.342175, "彩虹桥", "测试景点,看起来有很多很多彩虹");
|
|
|
|
|
|
attr_broadcast_add_attraction(121.36799,31.34449, "恐龙乐园正门", "您好自然谷恐龙园到了,这是一个人工搭建的,模拟三叠纪,侏罗纪,白垩纪等时代的恐龙生活场景的乐园,在这里您可以穿梭于恐龙像群,欣赏超前的7D电影,在自然中感受野趣,在怪石和岩洞中穿行,充分体验远古时代洪荒洞窟的神奇刺激和绝妙采用,这里的各类恐龙有着高仿真,活动自如的特性,虫现远古时代恐龙生活打斗等场景,让恐龙如同真的复活,园内霸王龙,三角龙,阿马加龙,剑龙,恐爪龙等几十条恐龙与您零距离接触,带您进入远古霸主的惊险旅程");
|
|
|
|
|
|
attr_broadcast_add_attraction(121.362142,31.340588, "想家桥", "测试景点,看起来非常的让人想要回家");
|
|
|
|
|
|
attr_broadcast_add_attraction(121.362796,31.339624, "3号游船码头", "游客三号游船码头到了,顾村公园浏中湖景区有约10公顷的湖面和沿岸森林湿地");
|
|
|
|
|
|
attr_broadcast_add_attraction(121.36613,31.33925, "樱花广场", "亲爱的游客现在您看到的是顾村公园樱花广场,一寸春心十年相守,为纪念上海樱花节十周");
|
|
|
|
|
|
attr_broadcast_add_attraction(121.366656983, 31.344300423, "集装箱房间", "测试景点,看起来应该非常的方便测试");
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-03 15:12:57 +08:00
|
|
|
|
// 初始化状态
|
|
|
|
|
|
memset(&broadcast_state, 0, sizeof(broadcast_state));
|
|
|
|
|
|
broadcast_state.distance_threshold = 50.0; // 默认阈值50米
|
|
|
|
|
|
|
2025-07-10 10:01:23 +08:00
|
|
|
|
osThreadAttr_t attr_broadcast_task_attr = {0};
|
|
|
|
|
|
attr_broadcast_task_attr.name = "attr_broadcast_task";
|
|
|
|
|
|
attr_broadcast_task_attr.stack_size = 4096 * 5;
|
|
|
|
|
|
attr_broadcast_task_attr.priority= osPriorityNormal;
|
|
|
|
|
|
|
|
|
|
|
|
Attr_Broadcast_ThreadId= osThreadNew(attr_broadcast_task, 0, &attr_broadcast_task_attr);
|
2025-07-03 15:12:57 +08:00
|
|
|
|
|
|
|
|
|
|
}
|