这个项目是我们与浦厂智能部合作的一个项目。 == 网络架构概述 == [[File:PIS200KM系统拓扑结构图.png]] 如上图所示,车厢之间以及车厢内的黑色实线表示网线。车厢之间的绿色虚线表示568线,作为冗入的音频传输线,在车厢网络不能工作时起工作。车厢内的网络设备节点之间的蓝色虚线表示485线,用来接收网络功放的车厢号拨码开关的值。车厢内的DU与屏连接的蓝色实线是控制屏设备的485线。 == MVB通信 == MVB通信由一块专门的MVB板卡(Duagon)提供服务。 === MVB开发调试板 === 工作目录 '''/home/ubuntu/MVB_working-NanJing/D013_modded/D013 Linux Driver_Serial/linux_d013_ser''' sudo make sudo ./tcn_demo 主程序:'''/home/ubuntu/MVB_working-NanJing/D013_modded/D013 Linux Driver_Serial/based_on/d-000543-nnnnnn/sources/src/tcn_demo.c''' === 司机室车厢 === 一列车有前后司机室车厢,任何时候其中的一个司机室车厢是处于激活状态,作为主司机室,另外一个作为从司机室。 通过激活钥匙可以切换主从司机室。司机室车厢中有一个多媒体主机MSU和一个广播主机PSU,这两个主机直接通过网线直连。 === 乘客车厢 === 一列车中的每一节乘客车厢中有一个客室主机PCU。 客室主机PCU的面板由电源模块PW、交换机SW、AMP、DU、HDD、CCTV组成。其中DU通过485控制动态地图和车内屏的显示。其中的AMP面板有一个车厢号码的拔码开关,用来设置当前所在车厢的车厢号,当前车厢的所有网络设备通过485线可以检测到AMP设备的拔码开关的车厢号。 客室主机PCU中连接的网络节点设备类型有DU、报警面板PECU、CCTV、客室摄像头、AMP、IP电视。 === 设备信息 === {| class="wikitable sortable" |- ! 设备类型 !! 设备类型编号 !! 操作系统 !! 远程登录用户名 || 密码 || 主要服务组成 |- | PIS主机 || 11 || Ubuntu14.04 64位|| ntdeck || ntdeck || luna-pudge-broadcast、luna-pudge-ipalloc、ntpis |- | 监控触摸屏 || 无 || Ubuntu14.04 64位 || ntdeck || ntdeck || ntpis-cmon |- | 网络功放 || 31 || ARM Linux || root || 123456 || luna-pudge-2digit、luna-pudge-console |- | PECU || 110开始 || ARM Linux || root || 123456 || luna-pudge-ipalloc、luna-pudge-console |- | DACU || 81 ||ARM Linux || root || 123456 || luna-pudge-ipalloc、luna-pudge-server、ntdriver-box |- | DU || 21 || ARM Ubuntu 14.04 || root || ntdeck || pudge-led-hb、luna-pudge-ipalloc、udp_serialport.rb |- | VES || 50 || Ubuntu 14.04 64位 || ntdeck || ntdeck || luna-pudge-ipalloc、luna-vss、luna-msync |} == 优先级控制 == {| class="wikitable sortable" |- | 操作|| PA || PEI || CI || PEB || DVA || 优先级 |- | PA || — || × || × || × || × || 高 |- | PEI|| || — || × || × || × || |- | CI || || || — || √ || √ || |- | PEB || || || || — || × || |- | DVA || || || || || — || |- | 高 || || || || || || 低 |} 5大功能优先级从高到低: PA:人工广播;PEI:乘客对讲;CI:司机对讲;PEB:预录紧急广播;DVA:数字报站。 表中打钩表示兼容,功能可同时进行; 打叉表示不兼容,当有高优先级功能正在进行,低优先级功能不能激活;当有低优先级功能正在进行,高优先级功能可以激活且将其打断。 例如: 1.当正在进行人工广播,此时点击触摸屏欲进行预录紧急广播,应无响应,忽略此请求。 2.当正在进行数字报站,此时想要进行人工广播,应打断原报站语音功能,激活新功能。 == 功能接口和协议 == === 心跳广播协议 === 心跳广播协议(LunaHeartBeat)用与在当前的PIS网络中广播自身在线的信息,通过UDP广播到全网中,广播地址是'''255.255.255.255''',端口是4096, 数据以大端的方式排列,数据包格式如下: {| class="wikitable sortable" |- ! 帧头 4 !! 设备类型 2 !! COOKIE 4 !! 消息长度 2 !! 消息体 !! CRC 2 |- | 0x4C 0x55 0x48 0x42,即"LUHB"4个字符 || 设备类型,见LunaHeartBeat项目 || 32位随机无符号整数 || 消息体的长度,16位随机无符号整数 || 消息体内容 || 从第0位到CRC之前的数据进行CRC验证 |} 一般建议每2秒发送一次数据包。 当前的设备类型在 luna-heartbeat的头文件lhb.h中有定义,如下:
typedef enum
{
    LHB_SERVICE_TYPE_NONE  = 0,
    LHB_SERVICE_TYPE_PUDGE = 1,
    LHB_SERVICE_TYPE_PANEL = 2,
    LHB_SERVICE_TYPE_AMP   = 3,
    LHB_SERVICE_TYPE_PIS   = 5,
    LHB_SERVICE_TYPE_DACU  = 6,
    LHB_SERVICE_TYPE_BRCU  = 7,
    LHB_SERVICE_TYPE_CMON  = 8,
    LHB_SERVICE_TYPE_DU    = 9,
    LHB_SERVICE_TYPE_VSS   = 10,
    LHB_SERVICE_TYPE_LMC   = 11,

    LHB_SERVICE_TYPE_CCTV_HOST = 50,
    LHB_SERVICE_TYPE_CCTV_TERM = 51
}LHBServiceType;
以上的广播协议已经有封装好了库SO,头文件是, 项目名称叫做luna-heartbeat,分为发送端和接收端,通过其中的示例代码src/test.c可以了解其用法。 接收端的初始化: lhb_service_init(NULL, iface, 0, LHB_SERVICE_TYPE_PIS, 1); lhb_service_payload_feed(lhb_test_service_payload_data_feed_cb, NULL); lhb_service_init函数的第四个参数用与说明当前的设备类型。lhb_service_payload_feed函数用于设置心跳包的额外数据。
static void lhb_test_service_payload_data_feed_cb(guint8 *data, guint16 *size,
    gpointer user_data)
{
    memcpy(data, "Hello, world!", 14);
    *size = 14;
}
=== PIS报站协议 === PIS报站协议是PIS主机通过HTTP的方式提供服务的,包括WEB UI的HTML接口,报站接口、预录广播接口和背景音乐的接口。首先需要获取当前车厢网络中PIS主机的IP地址,这个可以通过心跳广播协议获取得到,相关的解析代码片断如下: 1. 初始化lhb接收端,并设置数据接收的回调函数: lhb_client_init(NULL, "eth0", 0); lhb_client_set_receive_callback(client_heartbeat_receive_cb, this); 2. 接收心跳数据的回调函数: static void client_heartbeat_receive_cb(struct sockaddr_in *source_addr, LHBServiceType service_type, const guint8 *payload_data, guint16 payload_size, gpointer user_data) { PudgeClient *pudge_client = static_cast(user_data); gchar addrstr[INET_ADDRSTRLEN+1] = {0}; inet_ntop(AF_INET, &(source_addr->sin_addr), addrstr, INET_ADDRSTRLEN); pudge_client->process_heartbeat_signal(QString(addrstr), service_type, payload_data, payload_size); } 3. 解析心跳数据的函数,在这个函数中,只需要解析设备类型是PIS主机的数据包,PIS主机发送的数据包里发送了额外的数据,是一个JSON格式,描述了当前路线ID、路线名称、当前站点ID、当前站点状态、预路音频ID、背景音乐ID,是否是主服务器。
{ "route_id" : 1, "route_name" : "G888", "station_id" : 2, "station_status" : 2, "voice_id" : -1, "bg_voice_id" : -1, "is_master" : 1 }
void PudgeClient::process_heartbeat_signal(const QString &host, int host_type, const quint8 *payload_data, quint16 payload_size) {
    switch(host_type) {
        case LHB_SERVICE_TYPE_PUDGE: {
            break;
        }
        case LHB_SERVICE_TYPE_PIS: {
            bool is_master = false; // reference used in follow process_heartbeat_data() fuction.
            iPisClient.process_heartbeat_data(payload_data, payload_size, is_master);

            if(!is_master)
                return;
            bool pis_host_online_status_changed = false;
            if(!_pis_host_online_) {
                LedController::instance().light_connected_led(true);
                pis_host_online_status_changed = true;
                _pis_host_online_ = true;
            }
            if(_pis_host_ != host || pis_host_online_status_changed) {
                _pis_host_ = host;
                PisLogger::instance().info(QString("Found pis host %1").arg(host));
                emit pis_host_changed(_pis_host_);
                LedController::instance().light_connected_led(true);

            } else {
                _pis_host_last_onilne_ = QTime::currentTime();
            }
            break;
        }
        case LHB_SERVICE_TYPE_VSS: {
            break;
        }
        case LHB_SERVICE_TYPE_LMC: {
            break;
        }
        default: {
            break;
        }
    }
}
==== PIS报站协议的测试方法 ==== 为了快速验证PIS报站协议的接口,可以使用curl这个命令行工具进行调试,curl的安装方法: sudo apt-get install curl curl发送GET请求: curl https://example.com/resource.cgi curl发送POST请求: curl curl --data "" https://example.com/resource.cgi curl发送POST请求并携带数据: curl --data "param1=value1¶m2=value2" https://example.com/resource.cgi ==== WEB UI的HTML接口 ==== WEB UI的HTML接口可以直接用网页浏览器直接访问来测试。 * 报站界面 http://PIS_SERVER_IP:3000/ * 预录音频界面 http://PIS_SERVER_IP:3000/voices.html * 背景音乐界面 http://PIS_SERVER_IP:3000/bg_music.html ==== 获取所有路线 ==== GET http://192.168.104.11:3000/routes.json 返回结果: { "routes": [ { "id": 1, "name": "T65", "current_route_station_id": 0, "current_station_status": 1, "running": false, "position": 0, "direction": 0, "stations_count": 4, "reverse_route_id": 2, "reverse_route_name": "" }, { "id": 2, "name": "T66", "current_route_station_id": 6, "current_station_status": 1, "running": false, "position": 0, "direction": 1, "stations_count": 6, "reverse_route_id": 1, "reverse_route_name": "" }, { "id": 3, "name": "G888", "current_route_station_id": 14, "current_station_status": 2, "running": true, "position": 0, "direction": 0, "stations_count": 26, "reverse_route_id": 4, "reverse_route_name": "" }, { "id": 4, "name": "G887", "current_route_station_id": 37, "current_station_status": 1, "running": false, "position": 0, "direction": 1, "stations_count": 26, "reverse_route_id": 3, "reverse_route_name": "" } ], "count": 4, "current_route_id": 3 } ==== 获取路线站点数据 ==== GET http://PIS_SERVER_IP:3000/routes/1.json { "route": { "id": 1, "name": "T65", "current_route_station_id": 0, "current_station_status": 1, "running": false, "position": 0, "direction": 0, "stations_count": 4 }, "stations": [ { "id": 1, "station_id": 1, "name": "北京站", "position": 0 }, { "id": 2, "station_id": 2, "name": "徐州站", "position": 1 }, { "id": 3, "station_id": 3, "name": "蚌埠站", "position": 2 }, { "id": 4, "station_id": 4, "name": "南京站", "position": 3 } ] } ==== 获取当前路线站点状态 ==== GET http://192.168.104.11:3000/station_status.json 返回结果: { "current_station_id": 14, "current_station_status": 2, "current_station_index": 3 } 测试方法: curl http://192.168.104.11:3000/station_status.json ==== 站点播报 ==== POST http://PIS_SERVER_IP:3000/pa?route_station_id=14&status=2 返回结果: ok ==== 获取预录音频 ==== GET http://PIS_SERVER_IP:3000/voices.json 返回结果: { "voices": [ { "id": 11, "remark": "请给需要帮助的乘客让个座", "ticker": "请给需要帮助的乘客让个座", "file_name": "voices/请给需要帮助的乘客让个座.mp3", "position": 0, "play_duration": 4 }, { "id": 12, "remark": "禁烟提示", "ticker": "女士们、先生们,本次列车是无烟列车,请不要在车内吸烟,感谢您的配合", "file_name": "voices/禁烟提示.mp3", "position": 0, "play_duration": 8 }, { "id": 13, "remark": "临时停车", "ticker": "女士们、先生们,列车没有到站,现在是临时停车,请您在座位上耐心等候,不要随意走动,感谢您的配合", "file_name": "voices/临时停车.mp3", "position": 0, "play_duration": 12 } ], "count": 2, "current_voice_id": -1 } ==== 获取音量接口 ==== GET http://PIS_SERVER_IP:3000/volumes.json 返回结果: { "station_pa": 20, "broadcast": 20, "bgmusic": 4 } ==== 获取广播状态 ==== GET http://PIS_SERVER_IP:3000/voice_broadcast_status.json 返回结果: { "voice_id": -1, "bg_voice_id": -1 } ==== 播放预录音频播放 ==== GET http://PIS_SERVER_IP:3000/play_voice?id=音频记录ID 返回结果: ok ==== 播放背景音乐 ==== GET http://PIS_SERVER_IP:3000/play_bg_voice?id=音频记录ID 返回结果: ok ==== 停止预录音频播放 ==== GET http://PIS_SERVER_IP:3000/stop_voice 返回结果: ok ==== 获取背景音月列表 ==== GET http://PIS_SERVER_IP:3000/bg_voices.json 返回结果: { "voices": [ { "id": 1, "remark": "tianzhiheng", "ticker": "", "file_name": "bg_voices/天之痕.mp3", "position": 0, "play_duration": 228 }, { "id": 2, "remark": "梦中的婚礼", "ticker": "", "file_name": "bg_voices/梦中的婚礼.mp3", "position": 0, "play_duration": 232 }, { "id": 3, "remark": "天空之城", "ticker": "", "file_name": "bg_voices/天空之城.mp3", "position": 0, "play_duration": 253 }, { "id": 4, "remark": "322_Fly away", "ticker": "", "file_name": "bg_voices/322_Fly away.mp3", "position": 0, "play_duration": 251 }, { "id": 5, "remark": "龙猫", "ticker": "", "file_name": "bg_voices/龙猫.mp3", "position": 0, "play_duration": 257 }, { "id": 6, "remark": "Angel", "ticker": "", "file_name": "bg_voices/Angel.mp3", "position": 0, "play_duration": 271 }, { "id": 7, "remark": "光辉岁月", "ticker": "", "file_name": "bg_voices/光辉岁月.mp3", "position": 0, "play_duration": 298 }, { "id": 8, "remark": "童年的回忆", "ticker": "", "file_name": "bg_voices/童年的回忆.mp3", "position": 0, "play_duration": 238 } ], "count": 8, "current_voice_id": -1 } ==== 停止背景音乐播放 ==== GET http://PIS_SERVER_IP:3000/stop_bg_voice 返回结果: ok ==== PIS数据库结构 ==== /var/lib/ntpis/data.db ===== routes表 ===== id, name, current_route_station_id, current_station_status, current_station_name, stations_count, direction, reverse_route_id, reverse_route_name, running, position * current_station_status:0预到站,1到站,2出站 * direction: 0上行,1下行 ===== route_stations表 ===== id, station_id, station_name, position, route_id, position, ticker_in, ticker_at, ticker_out ===== 报站音频 ===== /var/lib/ntpis/station_voices/{route_station_id}_{in,at,out}.mp3 === 司机对讲 === 司机对讲服务对应的程序是LunaPudgeServer,端口是2101,TCP连接。数据都是JSON格式。 ==== 发起对讲 ==== QString command_json = QString("{\"command\": \"unicast-outgoing-call\", \"type\": \"driver\", \"dialno\": \"%1\", \"priority\": 0 }\n").arg(target_dialno); QByteArray data; data.append(command_json); socket.write(data); ==== 接听对讲来电 ==== QString command = QString("{\"command\": \"unicast-incoming-accept\", \"uuid\": \"%1\" }\n").arg(call_id); QByteArray data; data.append(command); socket.write(data); ==== 拒绝接听来电 ==== QString command = QString("{\"command\": \"unicast-stop\", \"uuid\": \"%1\", \"reason\": \"user\" }\n").arg(call_id); QByteArray data; data.append(command); int result = socket.write(data); ==== 停止对讲 ==== QString command_json = QString("{\"command\": \"unicast-stop\", \"uuid\": \"%1\" }\n").arg(_current_call_->uuid()); QByteArray data; data.append(command_json); socket.write(data); ==== 发起人工广播 ==== QByteArray data("{\"command\": \"broadcast-microphone-start\", \"channel\": 0 }\n"); socket.write(data); ==== 停止人工广播 ==== QByteArray data("{\"command\": \"broadcast-microphone-stop\" }\n"); socket.write(data); === 乘客紧急呼叫 === === DU协议 === DU是控制信息屏和动态地图的控制单元,它与屏是通过485连接的。 {| class="wikitable sortable" |- ! 屏类型 !! 发送的内容 !! 说明 |- | 车外屏显示 || ntroute*15:40*G887*松江南站*杨高中路 || 发送当前时间、起点站、终点站到车外屏 |- | 动态地图 || ntroute2*AA1A0EBB020214000102FF || 详情见LED动态地图的协议 |- | 车内信息显示 || 0*女士们、先生们,本次列车是无烟列车,请不要在车内吸烟,感谢您的配合 || 发送字幕到车内显示屏 |}