diff --git a/Audio/beep/fc1.wav b/Audio/beep/fc1.wav new file mode 100644 index 0000000..0b05dbe Binary files /dev/null and b/Audio/beep/fc1.wav differ diff --git a/CloudServer/server.js b/CloudServer/server.js index c938c71..0445dfa 100644 --- a/CloudServer/server.js +++ b/CloudServer/server.js @@ -297,7 +297,7 @@ function publishStationUpdate(req) { } function getInterruptModeForPriority(priority) { - if (priority >= 2) return 'all'; + if (priority >= 3) return 'all'; if (priority >= 1) return 'ifLower'; return 'none'; } @@ -859,7 +859,7 @@ function enqueueStationAudioTask(stationName, fallbackWs, task) { const newP = Number(task.priority || 0); const cur = q.current; - if (task.interruptMode === 'all' || newP >= 2) { + if (task.interruptMode === 'all') { cancelStationAudioTask(cur); q.queue.length = 0; sendAudioControl(cleanStation, { type: 'audio_stop', mode: 'all', reason: 'emergency' }, fallbackWs); @@ -1802,6 +1802,9 @@ function handleMessage(ws, data) { case 'train_approaching': handleTrainApproaching(ws, data); break; + case 'train_alert': + handleTrainAlert(ws, data); + break; case 'train_departure': handleTrainDeparture(ws, data); break; @@ -1982,11 +1985,8 @@ function handleTrainApproaching(ws, data) { const lineCode = data.lineCode || (stationCode ? stationCode.split('-')[0] : 'UNK'); const destCode = data.destCode || getStationCodeFromName(data.destination) || 'UNK'; - const delay = Math.max(0, Number(data.delay) || 0); // 获取延误信息(分钟) - const cancelled = Boolean(data.cancelled); - const rawReason = String(data.reason || '').trim(); - log('INFO', `Train ${trainId} approaching ${stationName} (Delay: ${delay} min). Broadcasting pre-arrival...`); + log('INFO', `Train ${trainId} approaching ${stationName}. Broadcasting pre-arrival...`); ws.send(JSON.stringify({ type: 'train_approaching_ack', @@ -2000,30 +2000,6 @@ function handleTrainApproaching(ws, data) { seq.push('ja-fc1', `ja-${finalPlatform}`, 'ja-fc2', `ja-${destCode}`, 'ja-fc3'); seq.push('ch-fc1', `ch-${finalPlatform}`, 'ch-fc2', 'ch-fc3', `ch-${destCode}`, 'ch-fc4'); - const normalizeReason = (reason, lang) => { - let r = String(reason || '').trim(); - if (!r || r === 'delay') r = 'default_delay'; - if (lang === 'ch' && r === 'train') r = 'delaytrain'; - return r; - }; - - if (cancelled) { - seq.push( - 'ksts', - 'ja-cancel1', `ja-${lineCode}`, 'ja-cancel2', - 'ch-cancel1', `ch-${lineCode}`, 'ch-cancel2' - ); - } else if (delay > 0) { - const d = Math.max(1, Math.round(delay)); - const jaReason = normalizeReason(rawReason, 'ja'); - const chReason = normalizeReason(rawReason, 'ch'); - seq.push( - 'ksts', - 'ja-delay1', `ja-${lineCode}`, `ja-${jaReason}`, 'ja-delay2', `ja-${d}min`, 'ja-delay3', - 'ch-delay1', `ch-${lineCode}`, 'ch-delay2', `ch-${chReason}`, 'ch-delay3', `ch-${d}`, 'ch-delay4' - ); - } - enqueueHlsPush(stationName, seq); const audioId = `approaching-${stationCode}-${Date.now()}`; @@ -2035,7 +2011,7 @@ function handleTrainApproaching(ws, data) { sourcePath: built.sourcePath, streamUrl: built.streamUrl, durationSec: built.durationSec, - priority: 1, + priority: 2, // Priority 2 for Approaching interruptMode: 'ifLower', source: 'train_approaching', audioId, @@ -2047,6 +2023,74 @@ function handleTrainApproaching(ws, data) { } } +function handleTrainAlert(ws, data) { + const trainId = data.trainId; + const stationName = getCorrectStationName(data.stationName); + const stationCode = data.stationCode; + const platform = data.stationPlatform || data.platform || '1'; + + const lineCode = data.lineCode || (stationCode ? stationCode.split('-')[0] : 'UNK'); + const delay = Math.max(0, Number(data.delay) || 0); // 获取延误信息(分钟) + const cancelled = Boolean(data.cancelled); + const rawReason = String(data.reason || '').trim(); + + log('INFO', `Train ${trainId} alert at ${stationName} (Delay: ${delay} min, Cancelled: ${cancelled}). Broadcasting alert...`); + + ws.send(JSON.stringify({ + type: 'train_alert_ack', + success: true, + trainId + })); + + const seq = ['ksts']; + + const normalizeReason = (reason, lang) => { + let r = String(reason || '').trim(); + if (!r || r === 'delay') r = 'default_delay'; + if (lang === 'ch' && r === 'train') r = 'delaytrain'; + return r; + }; + + if (cancelled) { + seq.push( + 'ja-cancel1', `ja-${lineCode}`, 'ja-cancel2', + 'ch-cancel1', `ch-${lineCode}`, 'ch-cancel2' + ); + } else if (delay > 0) { + const d = Math.max(1, Math.round(delay)); + const jaReason = normalizeReason(rawReason, 'ja'); + const chReason = normalizeReason(rawReason, 'ch'); + seq.push( + 'ja-delay1', `ja-${lineCode}`, `ja-${jaReason}`, 'ja-delay2', `ja-${d}min`, 'ja-delay3', + 'ch-delay1', `ch-${lineCode}`, 'ch-delay2', `ch-${chReason}`, 'ch-delay3', `ch-${d}`, 'ch-delay4' + ); + } else { + return; + } + + enqueueHlsPush(stationName, seq); + + const audioId = `alert-${stationCode}-${Date.now()}`; + const built = buildSequenceWavFile(seq); + if (built) { + log('INFO', `Alert audio queued for ${stationName}: ${audioId} (${built.durationSec.toFixed(2)}s)`); + enqueueStationAudioTask(stationName, ws, { + audioFormat: built.audioFormat, + sourcePath: built.sourcePath, + streamUrl: built.streamUrl, + durationSec: built.durationSec, + priority: 1, // Priority 1 for Alert + interruptMode: 'ifLower', + source: 'train_alert', + audioId, + consoleStreamStart: { type: 'stream_start', station: stationName, seq, streamName: getStationStreamName(stationName), hlsUrl: getStationHlsUrl(getStationStreamName(stationName)) }, + consoleStreamEnd: { type: 'stream_end', station: stationName } + }); + } else { + log('WARN', `Alert audio build returned empty for ${stationName}: ${audioId}`); + } +} + function handleTrainArrival(ws, data) { const trainId = data.trainId; const stationName = getCorrectStationName(data.stationName); @@ -2077,11 +2121,17 @@ function handleTrainArrival(ws, data) { saveData(); broadcastUpdate(); + const departureBeep = data.departureBeep || 'fc1'; + const departureSeq = [departureBeep, 'ja-doorwarn', 'ch-doorwarn']; + const departureBuilt = buildSequenceWavFile(departureSeq); + const departureDuration = departureBuilt ? departureBuilt.durationSec : 10; + ws.send(JSON.stringify({ type: 'train_arrival_ack', success: true, message: 'Train arrival processed successfully', - trainId + trainId, + departureDuration })); // Broadcast Audio Logic @@ -2101,8 +2151,8 @@ function handleTrainArrival(ws, data) { sourcePath: built.sourcePath, streamUrl: built.streamUrl, durationSec: built.durationSec, - priority: 1, - interruptMode: 'ifLower', + priority: 3, + interruptMode: 'all', source: 'train_arrival', audioId, consoleStreamStart: { type: 'stream_start', station: stationName, seq, streamName: getStationStreamName(stationName), hlsUrl: getStationHlsUrl(getStationStreamName(stationName)) }, @@ -2150,8 +2200,8 @@ function handleTrainDeparture(ws, data) { sourcePath: built.sourcePath, streamUrl: built.streamUrl, durationSec: built.durationSec, - priority: 1, - interruptMode: 'ifLower', + priority: 3, + interruptMode: 'all', source: 'train_departure', audioId, consoleStreamStart: { type: 'stream_start', station: stationName, seq, streamName: getStationStreamName(stationName), hlsUrl: getStationHlsUrl(getStationStreamName(stationName)) }, @@ -3130,7 +3180,7 @@ app.post('/api/music/start', express.json(), (req, res) => { audioFormat: 'wav-stream-live', sourcePath: playbackAudio.sourcePath, durationSec: playbackAudio.durationSec, - priority: -1, + priority: 0, interruptMode: 'none', source: 'music', loop, @@ -3147,7 +3197,7 @@ app.post('/api/music/start', express.json(), (req, res) => { buffer: playbackAudio.buffer, streamUrl: playbackAudio.streamUrl, durationSec: playbackAudio.durationSec, - priority: -1, + priority: 0, interruptMode: 'none', source: 'music', loop, diff --git a/station_broadcast.lua b/station_broadcast.lua index 4b665d6..7094131 100644 --- a/station_broadcast.lua +++ b/station_broadcast.lua @@ -1,6 +1,6 @@ -- 1. Configuration & Constants -local APP_VERSION_TEXT = "v1.0.58" +local APP_VERSION_TEXT = "v1.0.59" local CONFIG = { CLOUD_URL = "ws://stabadmin.fse-media.group", @@ -2161,6 +2161,11 @@ function Network.messageLoop() local flushBelow = data.flushBelow Audio.stop(mode, flushBelow) end + elseif data.type == "train_arrival_ack" then + if State.currentTrain and State.currentTrain.id == data.trainId then + State.currentTrain.departureDuration = tonumber(data.departureDuration) or 10 + Utils.log("INFO", "Got departure audio duration: " .. State.currentTrain.departureDuration .. "s") + end elseif data.type == "station_update" then local targetVersion = AutoUpdate.getTargetVersion(data) if targetVersion == "" then @@ -2752,7 +2757,7 @@ function Main.loop() State.announcedAlerts[alertKey] = now Utils.log("INFO", string.format("Train Alert: %s (ETA: %ds, Delay: %dm, Cancelled: %s, Reason: %s)", trainName, etaSeconds, displayDelayMinutes, tostring(cancelled), tostring(reason or ""))) Network.send({ - type = "train_approaching", + type = "train_alert", stationName = State.stationName, stationCode = State.stationCode, stationPlatform = platform, @@ -2908,7 +2913,7 @@ function Main.loop() State.announcedAlerts[alertKey] = now Utils.log("INFO", string.format("Train Delay Alert: %s (ETA: %ds, Delay: %dm)", tName, etaSeconds, delay)) Network.send({ - type = "train_approaching", + type = "train_alert", stationName = State.stationName, stationCode = State.stationCode, stationPlatform = platform or State.stationPlatform or "1", @@ -3029,11 +3034,14 @@ function Main.loop() trainId = id, destination = dest, nextStation = nextStation, - departureTime = State.currentTrain.departureTime + departureTime = State.currentTrain.departureTime, + departureBeep = State.departureBeep }) else local now = os.epoch("utc") / 1000 - if not State.currentTrain.departureSent and (State.currentTrain.departureTime - now) <= 10 then + -- 增加 1.5 秒网络与预缓冲补偿,确保音频结束时列车刚好发车 + local triggerOffset = (State.currentTrain.departureDuration or 10) + 1.5 + if not State.currentTrain.departureSent and (State.currentTrain.departureTime - now) <= triggerOffset then State.currentTrain.departureSent = true Network.send({ type = "train_departure", diff --git a/test_keywords.lua b/test_keywords.lua new file mode 100644 index 0000000..9634050 --- /dev/null +++ b/test_keywords.lua @@ -0,0 +1,49 @@ +function getKeywords(text) + if not text then return {} end + local s = tostring(text):lower() + + local keywords = {} + + for code in s:gmatch("[%a%d]+%-[%a%d]+") do + keywords[code] = true + end + for code in s:gmatch("%a+%d+") do + keywords[code] = true + end + + for zh in s:gmatch("[\128-\255]+") do + local cleanZh = zh:gsub("站台", ""):gsub("轨道", ""):gsub("号", ""):gsub("线", "") + if cleanZh and cleanZh ~= "" then + keywords[cleanZh] = true + end + end + + local list = {} + for k, _ in pairs(keywords) do + table.insert(list, k) + end + return list +end + +local function test(a, b) + local k1 = getKeywords(a) + local k2 = getKeywords(b) + local match = false + for _, w1 in ipairs(k1) do + for _, w2 in ipairs(k2) do + if w1 == w2 or w1:find(w2, 1, true) or w2:find(w1, 1, true) then + match = true + break + end + end + end + print(a .. " vs " .. b .. " -> " .. tostring(match)) + print(" K1: " .. table.concat(k1, ", ")) + print(" K2: " .. table.concat(k2, ", ")) +end + +test("CE-03 (桃园)", "桃园") +test("CE-04", "CE-04 [西门]") +test("CRH-01", "CRH-01 南山线") +test("南山线", "南山") +test("???", "Unknown")