refactor(station_broadcast): 清理冗余代码与注释,移除无用文件并更新gitignore
- 清理station_broadcast.lua中的冗余注释、废弃代码块,移除无用的节分隔注释 - 删除不再使用的austream.lua文件 - 更新.gitignore,新增临时文件与转储文件的忽略规则
This commit is contained in:
+6
-1
@@ -6,4 +6,9 @@ CloudServer/tmp/
|
|||||||
CloudServer/server-data.json
|
CloudServer/server-data.json
|
||||||
Audio/general/
|
Audio/general/
|
||||||
Audio/music/
|
Audio/music/
|
||||||
Audio/station/
|
Audio/station/
|
||||||
|
tmp_vercel_proxy/
|
||||||
|
nbt_dump.txt
|
||||||
|
test.lua
|
||||||
|
temp-broadcast/
|
||||||
|
debug-upcoming-data-missing.md
|
||||||
|
|||||||
-255
@@ -1,255 +0,0 @@
|
|||||||
|
|
||||||
local aukit = require "aukit"
|
|
||||||
|
|
||||||
local speakers = {peripheral.find("speaker")}
|
|
||||||
if #speakers == 0 then error("No speaker attached") end
|
|
||||||
if #speakers == 2 and peripheral.getName(speakers[1]) == "right" and peripheral.getName(speakers[2]) == "left" then speakers = {speakers[2], speakers[1]} end
|
|
||||||
|
|
||||||
local path = ...
|
|
||||||
if not path then error("Usage: austream <file/URL> [arguments for PCM/DFPWM]") end
|
|
||||||
local params = {...}
|
|
||||||
table.remove(params, 1) -- Remove path
|
|
||||||
local v = {}
|
|
||||||
if #params > 0 then
|
|
||||||
-- Try to parse as Lua table string first (original behavior)
|
|
||||||
local paramStr = table.concat(params, " ")
|
|
||||||
-- Remove outer braces if they exist to match original austream format
|
|
||||||
paramStr = paramStr:gsub("^%s*{%s*", ""):gsub("%s*}%s*$", "")
|
|
||||||
|
|
||||||
local fn = load("return {" .. paramStr .. "}", "=unserialize", "t", setmetatable({}, {__index = function(_, idx) return idx end}))
|
|
||||||
if fn then
|
|
||||||
local ok, parsed = pcall(fn)
|
|
||||||
if ok and type(parsed) == "table" then
|
|
||||||
for k, val in pairs(parsed) do v[k] = val end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If that fails, try manual string matching
|
|
||||||
if not next(v) then
|
|
||||||
local matchStr = table.concat(params, ", ")
|
|
||||||
if matchStr:match("type%s*=%s*['\"]([^'\"]+)['\"]") then
|
|
||||||
v.type = matchStr:match("type%s*=%s*['\"]([^'\"]+)['\"]")
|
|
||||||
end
|
|
||||||
if matchStr:match("volume%s*=%s*([%d%.]+)") then v.volume = tonumber(matchStr:match("volume%s*=%s*([%d%.]+)")) end
|
|
||||||
if matchStr:match("streamData%s*=%s*true") then v.streamData = true end
|
|
||||||
if matchStr:match("silent%s*=%s*true") then v.silent = true end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if v.volume == nil then v.volume = 3 end
|
|
||||||
if type(v.volume) == "number" then
|
|
||||||
if v.volume < 0 then v.volume = 0 end
|
|
||||||
if v.volume > 3 then v.volume = 3 end
|
|
||||||
end
|
|
||||||
local mono = v.mono or #speakers == 1
|
|
||||||
aukit.defaultInterpolation = v.interpolation or aukit.defaultInterpolation
|
|
||||||
local data
|
|
||||||
if path:match("^https?://") then
|
|
||||||
local handle, err = http.get(path, nil, true)
|
|
||||||
if not handle then error("Could not connect to " .. path .. ": " .. err) end
|
|
||||||
local code = handle.getResponseCode()
|
|
||||||
if code ~= 200 then handle.close() error("Could not connect to " .. path .. ": HTTP " .. code) end
|
|
||||||
if v.streamData then
|
|
||||||
local closed = false
|
|
||||||
function data()
|
|
||||||
if closed then return nil end
|
|
||||||
local d = handle.read(48000)
|
|
||||||
if not d then handle.close() closed = true return nil end
|
|
||||||
return d
|
|
||||||
end
|
|
||||||
else
|
|
||||||
data = handle.readAll()
|
|
||||||
handle.close()
|
|
||||||
end
|
|
||||||
elseif path:match("^wss?://") then
|
|
||||||
local ws, err = http.websocket(path)
|
|
||||||
if not ws then error("Failed to connect to websocket: " .. tostring(err)) end
|
|
||||||
|
|
||||||
-- Buffer completely locally to avoid any network stutter
|
|
||||||
local fullBuffer = ""
|
|
||||||
local isDone = false
|
|
||||||
|
|
||||||
local function cleanup()
|
|
||||||
if not isDone then
|
|
||||||
isDone = true
|
|
||||||
if ws then ws.close() end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Initial wait for the first data chunk from the server (which sends a 1s silence chunk on connect)
|
|
||||||
local startWait = os.startTimer(1.0)
|
|
||||||
while true do
|
|
||||||
local event, p1, p2 = os.pullEvent()
|
|
||||||
if event == "websocket_message" and p1 == path then
|
|
||||||
if type(p2) == "string" then fullBuffer = fullBuffer .. p2 end
|
|
||||||
-- Once we get the first message, we can break and start playing
|
|
||||||
break
|
|
||||||
elseif event == "websocket_closed" and p1 == path then
|
|
||||||
cleanup()
|
|
||||||
break
|
|
||||||
elseif event == "timer" and p1 == startWait then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Since WebSocket connection needs to run alongside aukit stream reading,
|
|
||||||
-- we will just use a non-blocking read approach in the data function itself.
|
|
||||||
|
|
||||||
function data(n)
|
|
||||||
n = n or 6000
|
|
||||||
|
|
||||||
if isDone then
|
|
||||||
if #fullBuffer == 0 then return nil end
|
|
||||||
else
|
|
||||||
-- First, read any available messages without blocking
|
|
||||||
-- ws.receive(0) returns immediately. If nil, no message is ready.
|
|
||||||
while true do
|
|
||||||
local msg = ws.receive(0)
|
|
||||||
if msg == nil then break end
|
|
||||||
if type(msg) == "string" then
|
|
||||||
fullBuffer = fullBuffer .. msg
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If we still don't have enough data, wait for more
|
|
||||||
-- but only if we know more might be coming (not isDone)
|
|
||||||
if #fullBuffer < n then
|
|
||||||
-- We only wait briefly so we don't block the audio thread too long
|
|
||||||
-- If the server isn't sending data, it means silence or end of stream
|
|
||||||
local timeout = os.startTimer(0.1) -- shorter timeout to keep pulling events
|
|
||||||
while #fullBuffer < n do
|
|
||||||
local event, p1, p2 = os.pullEvent()
|
|
||||||
if event == "websocket_message" and p1 == path then
|
|
||||||
if type(p2) == "string" then fullBuffer = fullBuffer .. p2 end
|
|
||||||
elseif event == "websocket_closed" and p1 == path then
|
|
||||||
cleanup()
|
|
||||||
break
|
|
||||||
elseif event == "timer" and p1 == timeout then
|
|
||||||
-- We hit timeout, we must return whatever we have to keep playing
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #fullBuffer == 0 and isDone then return nil end
|
|
||||||
-- Return DFPWM silence (0xAA) if buffer is empty but stream is alive
|
|
||||||
-- This keeps the stream alive without crashing or making noise
|
|
||||||
if #fullBuffer == 0 then return string.rep("\xAA", n) end
|
|
||||||
|
|
||||||
local ret
|
|
||||||
if #fullBuffer >= n then
|
|
||||||
ret = fullBuffer:sub(1, n)
|
|
||||||
fullBuffer = fullBuffer:sub(n + 1)
|
|
||||||
else
|
|
||||||
ret = fullBuffer
|
|
||||||
-- Pad with silence if we don't have enough data to fulfill the request
|
|
||||||
ret = ret .. string.rep("\xAA", n - #ret)
|
|
||||||
fullBuffer = ""
|
|
||||||
end
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
-- Note: aukit.stream handles the reading internally
|
|
||||||
-- We can use a table with a read function for custom streaming
|
|
||||||
local obj = {
|
|
||||||
read = function(n)
|
|
||||||
local d = data(n)
|
|
||||||
if not d and isDone then cleanup() end
|
|
||||||
return d
|
|
||||||
end,
|
|
||||||
close = cleanup
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
elseif path:match("^rednet://%d+") or path:match("^rednet%+%l+://%d+") then
|
|
||||||
peripheral.find("modem", rednet.open)
|
|
||||||
local proto, id, name = path:match("^rednet%+?(%l*)://(%d+)(/?.*)$")
|
|
||||||
id = tonumber(id)
|
|
||||||
if proto == "" then proto = nil end
|
|
||||||
if path ~= "" and id >= 0 then rednet.send(id, name, proto) end
|
|
||||||
function data()
|
|
||||||
local i, msg
|
|
||||||
repeat i, msg = rednet.receive(proto) until id == -1 or i == id
|
|
||||||
return msg
|
|
||||||
end
|
|
||||||
else
|
|
||||||
path = shell.resolve(...)
|
|
||||||
local file, err = fs.open(path, "rb")
|
|
||||||
if not file then error("Could not open " .. path .. ": " .. err) end
|
|
||||||
if v.streamData then
|
|
||||||
local closed = false
|
|
||||||
function data()
|
|
||||||
if closed then return nil end
|
|
||||||
local d = file.read(48000)
|
|
||||||
if not d then file.close() closed = true return nil end
|
|
||||||
return d
|
|
||||||
end
|
|
||||||
else
|
|
||||||
data = file.readAll()
|
|
||||||
file.close()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
local iter, length
|
|
||||||
if type(v.type) == "string" then v.type = v.type:lower() end
|
|
||||||
|
|
||||||
local isDfpwm = v.type == "dfpwm" or path:lower():match("%.dfpwm$") or path:match("^wss?://")
|
|
||||||
if isDfpwm then
|
|
||||||
if type(data) == "table" and data.read then
|
|
||||||
iter, length = aukit.stream.dfpwm(data, v.sampleRate, v.channels, mono)
|
|
||||||
else
|
|
||||||
iter, length = aukit.stream.dfpwm(data, v.sampleRate, v.channels, mono)
|
|
||||||
end
|
|
||||||
elseif v.type == "mdfpwm" or path:lower():match("%.mdfpwm$") then iter, length = aukit.stream.mdfpwm(data, mono)
|
|
||||||
elseif v.type == "wav" or path:lower():match("%.wav$") then iter, length = aukit.stream.wav(data, mono, v.ignoreHeader)
|
|
||||||
elseif v.type == "aiff" or path:lower():match("%.aiff?$") then iter, length = aukit.stream.aiff(data, mono, v.ignoreHeader)
|
|
||||||
elseif v.type == "au" or path:lower():match("%.au$") then iter, length = aukit.stream.au(data, mono, v.ignoreHeader)
|
|
||||||
elseif v.type == "flac" or path:lower():match("%.flac$") then iter, length = aukit.stream.flac(data, mono)
|
|
||||||
elseif v.type == "qoa" or path:lower():match("%.qoa$") then iter, length = aukit.stream.qoa(data, mono)
|
|
||||||
elseif v.type == "pcm" or path:lower():match("%.pcm$") or path:lower():match("%.raw$") or path:match("^rednet%+?%l*://") then iter, length = aukit.stream.pcm(data, v.bitDepth, v.dataType, v.channels, v.sampleRate, v.bigEndian, mono)
|
|
||||||
else error("Unknown file type. Make sure to add the right file extension to the path/URL.") end
|
|
||||||
if length == nil then length = 0 end
|
|
||||||
|
|
||||||
if not v.silent then
|
|
||||||
print("Streaming...")
|
|
||||||
local w = term.getSize()
|
|
||||||
local y = select(2, term.getCursorPos())
|
|
||||||
local fg, bg = colors.toBlit(term.getTextColor()), colors.toBlit(term.getBackgroundColor())
|
|
||||||
term.write(("00:00 %s %02d:%02d"):format(("\127"):rep(w - 12), math.floor(length / 60), length % 60))
|
|
||||||
aukit.play(iter, function(pos)
|
|
||||||
pos = math.min(pos, 5999)
|
|
||||||
local p = pos / length
|
|
||||||
term.setCursorPos(1, y)
|
|
||||||
|
|
||||||
-- 计算基本时间字符串
|
|
||||||
local timeStr = ("%02d:%02d"):format(math.floor(pos / 60), pos % 60)
|
|
||||||
local endTimeStr = ("%02d:%02d"):format(math.floor(length / 60), length % 60)
|
|
||||||
|
|
||||||
if p > 1 then
|
|
||||||
-- 确保空格数量正确
|
|
||||||
local spaces = (" "):rep(w - 12)
|
|
||||||
local text = timeStr .. " " .. spaces .. "--:--"
|
|
||||||
local fgColor = fg:rep(#text)
|
|
||||||
local bgColor = bg:rep(6) .. fg:rep(w - 12) .. bg:rep(6)
|
|
||||||
-- 确保所有参数长度相等
|
|
||||||
if #text == #fgColor and #text == #bgColor then
|
|
||||||
term.blit(text, fgColor, bgColor)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local barWidth = w - 12
|
|
||||||
local progress = math.floor(barWidth * p)
|
|
||||||
progress = math.max(0, math.min(progress, barWidth)) -- 确保进度在有效范围内
|
|
||||||
|
|
||||||
local progressBar = (" "):rep(progress) .. ("\127"):rep(barWidth - progress)
|
|
||||||
local text = timeStr .. " " .. progressBar .. " " .. endTimeStr
|
|
||||||
local fgColor = fg:rep(#text)
|
|
||||||
local bgColor = bg:rep(6) .. fg:rep(progress) .. bg:rep(barWidth - progress + 6)
|
|
||||||
-- 确保所有参数长度相等
|
|
||||||
if #text == #fgColor and #text == #bgColor then
|
|
||||||
term.blit(text, fgColor, bgColor)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end, v.volume, table.unpack(speakers))
|
|
||||||
print()
|
|
||||||
else
|
|
||||||
aukit.play(iter, function() end, v.volume, table.unpack(speakers))
|
|
||||||
end
|
|
||||||
+43
-110
@@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 1. Configuration & Constants
|
-- 1. Configuration & Constants
|
||||||
-- ============================================================================
|
|
||||||
local APP_VERSION_TEXT = "v1.0.46"
|
local APP_VERSION_TEXT = "v1.0.46"
|
||||||
|
|
||||||
local CONFIG = {
|
local CONFIG = {
|
||||||
@@ -34,9 +32,9 @@ local COLORS = {
|
|||||||
SUCCESS = colors.green
|
SUCCESS = colors.green
|
||||||
}
|
}
|
||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 2. State Management
|
-- 2. State Management
|
||||||
-- ============================================================================
|
|
||||||
local State = {
|
local State = {
|
||||||
ws = nil,
|
ws = nil,
|
||||||
stationName = "Unknown",
|
stationName = "Unknown",
|
||||||
@@ -60,9 +58,9 @@ local State = {
|
|||||||
updateInProgress = false
|
updateInProgress = false
|
||||||
}
|
}
|
||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 3. Peripherals Setup
|
-- 3. Peripherals Setup
|
||||||
-- ============================================================================
|
|
||||||
local Peripherals = {}
|
local Peripherals = {}
|
||||||
|
|
||||||
function Peripherals.isTrainStationPeripheral(name)
|
function Peripherals.isTrainStationPeripheral(name)
|
||||||
@@ -80,9 +78,6 @@ function Peripherals.isTrainStationPeripheral(name)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create Wiki documents the station peripheral by its methods rather than a
|
|
||||||
-- guaranteed fixed type string. Match on the core method set so attached
|
|
||||||
-- stations are still detected across integration/version differences.
|
|
||||||
return type(wrapped.getStationName) == "function"
|
return type(wrapped.getStationName) == "function"
|
||||||
and type(wrapped.isTrainPresent) == "function"
|
and type(wrapped.isTrainPresent) == "function"
|
||||||
and type(wrapped.getTrainName) == "function"
|
and type(wrapped.getTrainName) == "function"
|
||||||
@@ -90,7 +85,6 @@ function Peripherals.isTrainStationPeripheral(name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Peripherals.init()
|
function Peripherals.init()
|
||||||
-- Station Peripheral: prefer direct sides, then fall back to any discovered peripheral name.
|
|
||||||
local station = nil
|
local station = nil
|
||||||
local stationSide = nil
|
local stationSide = nil
|
||||||
local checkedNames = {}
|
local checkedNames = {}
|
||||||
@@ -115,7 +109,7 @@ function Peripherals.init()
|
|||||||
Peripherals.station = station
|
Peripherals.station = station
|
||||||
State.hasStationPeripheral = station ~= nil
|
State.hasStationPeripheral = station ~= nil
|
||||||
State.stationPeripheralSide = stationSide
|
State.stationPeripheralSide = stationSide
|
||||||
-- monitors
|
|
||||||
local monitors = { peripheral.find("monitor") }
|
local monitors = { peripheral.find("monitor") }
|
||||||
if #monitors > 0 then
|
if #monitors > 0 then
|
||||||
Peripherals.primaryMonitor = monitors[1]
|
Peripherals.primaryMonitor = monitors[1]
|
||||||
@@ -125,14 +119,13 @@ function Peripherals.init()
|
|||||||
Peripherals.primaryMonitor = nil
|
Peripherals.primaryMonitor = nil
|
||||||
State.hasPrimaryMonitor = false
|
State.hasPrimaryMonitor = false
|
||||||
end
|
end
|
||||||
-- Speakers
|
|
||||||
Peripherals.speakers = { peripheral.find("speaker") }
|
|
||||||
-- Create Target Block (CRN)
|
|
||||||
Peripherals.createTarget = peripheral.find("create_target")
|
Peripherals.createTarget = peripheral.find("create_target")
|
||||||
if Peripherals.createTarget then
|
if Peripherals.createTarget then
|
||||||
pcall(Peripherals.createTarget.resize, 80, 10)
|
pcall(Peripherals.createTarget.resize, 80, 10)
|
||||||
end
|
end
|
||||||
-- Block Reader (Advanced Peripherals)
|
|
||||||
local blockReader = nil
|
local blockReader = nil
|
||||||
local typeCandidates = { "blockReader", "block_reader", "arBlockReader", "advancedperipherals:block_reader" }
|
local typeCandidates = { "blockReader", "block_reader", "arBlockReader", "advancedperipherals:block_reader" }
|
||||||
for _, t in ipairs(typeCandidates) do
|
for _, t in ipairs(typeCandidates) do
|
||||||
@@ -154,9 +147,9 @@ function Peripherals.init()
|
|||||||
Peripherals.blockReader = blockReader
|
Peripherals.blockReader = blockReader
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 4. Utility Functions
|
-- 4. Utility Functions
|
||||||
-- ============================================================================
|
|
||||||
local Utils = {}
|
local Utils = {}
|
||||||
|
|
||||||
function Utils.log(level, message)
|
function Utils.log(level, message)
|
||||||
@@ -165,8 +158,7 @@ function Utils.log(level, message)
|
|||||||
local entry = { level = level, text = entryText }
|
local entry = { level = level, text = entryText }
|
||||||
table.insert(State.logs, 1, entry)
|
table.insert(State.logs, 1, entry)
|
||||||
if #State.logs > 100 then table.remove(State.logs) end
|
if #State.logs > 100 then table.remove(State.logs) end
|
||||||
|
|
||||||
-- If user is scrolling history, increment offset to stay on same items
|
|
||||||
if State.logScrollOffset > 0 then
|
if State.logScrollOffset > 0 then
|
||||||
State.logScrollOffset = math.min(State.logScrollOffset + 1, math.max(0, #State.logs - 1))
|
State.logScrollOffset = math.min(State.logScrollOffset + 1, math.max(0, #State.logs - 1))
|
||||||
end
|
end
|
||||||
@@ -546,7 +538,7 @@ function Utils.getStationCode(name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if code then
|
if code then
|
||||||
return tostring(code):lower() -- Always return lowercase for consistent comparison
|
return tostring(code):lower()
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -570,7 +562,6 @@ function Utils.isSameTrain(id1, id2)
|
|||||||
if not id1 or not id2 then return false end
|
if not id1 or not id2 then return false end
|
||||||
if id1 == id2 then return true end
|
if id1 == id2 then return true end
|
||||||
|
|
||||||
-- Consist Car Match: "CK 1 01" vs "CK 1 02"
|
|
||||||
local function getConsist(id)
|
local function getConsist(id)
|
||||||
local clean = id:gsub("^%[.-%]%s*", "")
|
local clean = id:gsub("^%[.-%]%s*", "")
|
||||||
local p = {}
|
local p = {}
|
||||||
@@ -587,27 +578,26 @@ function Utils.isSameTrain(id1, id2)
|
|||||||
local c2 = getConsist(id2)
|
local c2 = getConsist(id2)
|
||||||
if c1 == c2 then return true end
|
if c1 == c2 then return true end
|
||||||
|
|
||||||
-- Fuzzy match: remove type prefix and compare content
|
|
||||||
local _, name1, num1 = Utils.parseTrainId(id1)
|
local _, name1, num1 = Utils.parseTrainId(id1)
|
||||||
local _, name2, num2 = Utils.parseTrainId(id2)
|
local _, name2, num2 = Utils.parseTrainId(id2)
|
||||||
|
|
||||||
-- If both have numbers, compare name and number
|
|
||||||
if num1 ~= "" and num2 ~= "" then
|
if num1 ~= "" and num2 ~= "" then
|
||||||
return name1 == name2 and num1 == num2
|
return name1 == name2 and num1 == num2
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Fallback to comparing just the main content/name
|
|
||||||
return name1 == name2
|
return name1 == name2
|
||||||
end
|
end
|
||||||
|
|
||||||
function Utils.parseTrainId(trainId)
|
function Utils.parseTrainId(trainId)
|
||||||
if not trainId then return "unknown", "" end
|
if not trainId then return "unknown", "" end
|
||||||
|
|
||||||
-- Generic match: [Type]Content
|
|
||||||
local typeStr, content = trainId:match("^%[(.-)%](.+)$")
|
local typeStr, content = trainId:match("^%[(.-)%](.+)$")
|
||||||
|
|
||||||
if typeStr then
|
if typeStr then
|
||||||
-- Check for Express types
|
|
||||||
if typeStr == "Ltd.Exp" or typeStr == "Express" then
|
if typeStr == "Ltd.Exp" or typeStr == "Express" then
|
||||||
|
|
||||||
local name, num = content:match("^(.+) No%.(%d+)$")
|
local name, num = content:match("^(.+) No%.(%d+)$")
|
||||||
@@ -628,7 +618,6 @@ end
|
|||||||
|
|
||||||
function Utils.getDestination(schedule)
|
function Utils.getDestination(schedule)
|
||||||
if not schedule then return "Unknown" end
|
if not schedule then return "Unknown" end
|
||||||
-- Handle both wrapped and unwrapped schedule structure
|
|
||||||
local entries = schedule.entries or (type(schedule) == "table" and schedule)
|
local entries = schedule.entries or (type(schedule) == "table" and schedule)
|
||||||
if not entries or type(entries) ~= "table" then return "Unknown" end
|
if not entries or type(entries) ~= "table" then return "Unknown" end
|
||||||
|
|
||||||
@@ -692,7 +681,7 @@ function Utils.isFuzzyMatch(str, pattern)
|
|||||||
|
|
||||||
if str == pattern then return true end
|
if str == pattern then return true end
|
||||||
|
|
||||||
-- 1. Handle Create Mod Wildcards (*)
|
-- 1. Handle Create Mod Wildcards
|
||||||
if pattern:find("*") then
|
if pattern:find("*") then
|
||||||
local p = pattern:gsub("([%^%$%(%)%%%.%[%]%+%-%?])", "%%%1")
|
local p = pattern:gsub("([%^%$%(%)%%%.%[%]%+%-%?])", "%%%1")
|
||||||
p = p:gsub("%%%*", ".*")
|
p = p:gsub("%%%*", ".*")
|
||||||
@@ -705,7 +694,6 @@ function Utils.isFuzzyMatch(str, pattern)
|
|||||||
local b2 = Utils.getBaseName(pattern)
|
local b2 = Utils.getBaseName(pattern)
|
||||||
if b1 ~= "" and b2 ~= "" then
|
if b1 ~= "" and b2 ~= "" then
|
||||||
if b1 == b2 then return true end
|
if b1 == b2 then return true end
|
||||||
-- Substring match for base names
|
|
||||||
if #b1 >= 4 and #b2 >= 4 then
|
if #b1 >= 4 and #b2 >= 4 then
|
||||||
if b1:find(b2, 1, true) or b2:find(b1, 1, true) then
|
if b1:find(b2, 1, true) or b2:find(b1, 1, true) then
|
||||||
return true
|
return true
|
||||||
@@ -954,9 +942,9 @@ function Utils.getNextStation(schedule, currentStationName)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 5. Audio System
|
-- 5. Audio System
|
||||||
-- ============================================================================
|
|
||||||
local Audio = {}
|
local Audio = {}
|
||||||
|
|
||||||
function Audio.createStream()
|
function Audio.createStream()
|
||||||
@@ -1573,7 +1561,6 @@ function Audio.playDfpwmItem(item)
|
|||||||
local playSeq = State.interruptSeq
|
local playSeq = State.interruptSeq
|
||||||
local source = item.audioData
|
local source = item.audioData
|
||||||
|
|
||||||
-- Support both static string data and streaming functions
|
|
||||||
if type(source) == "table" and source.read then
|
if type(source) == "table" and source.read then
|
||||||
source = source.read
|
source = source.read
|
||||||
end
|
end
|
||||||
@@ -1644,7 +1631,7 @@ function Audio.playWavStreamItem(item)
|
|||||||
|
|
||||||
local playSeq = State.interruptSeq
|
local playSeq = State.interruptSeq
|
||||||
|
|
||||||
-- Ensure all speakers are completely empty and synced before starting the new stream
|
|
||||||
if not waitForNativeSpeakersEmpty(Peripherals.speakers, playSeq) then
|
if not waitForNativeSpeakersEmpty(Peripherals.speakers, playSeq) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -1789,11 +1776,11 @@ function Utils.getChineseNumberAudio(val)
|
|||||||
end
|
end
|
||||||
return seq
|
return seq
|
||||||
else
|
else
|
||||||
-- Handle Alphanumeric (e.g., "1A", "2B")
|
-- Handle Alphanumeric
|
||||||
local digit, letter = tostring(val):match("^(%d+)([a-zA-Z])$")
|
local digit, letter = tostring(val):match("^(%d+)([a-zA-Z])$")
|
||||||
if digit and letter then
|
if digit and letter then
|
||||||
local seq = Utils.getChineseNumberAudio(digit)
|
local seq = Utils.getChineseNumberAudio(digit)
|
||||||
table.insert(seq, letter:upper()) -- e.g. "A", "B"
|
table.insert(seq, letter:upper())
|
||||||
return seq
|
return seq
|
||||||
end
|
end
|
||||||
return {tostring(val)}
|
return {tostring(val)}
|
||||||
@@ -1811,7 +1798,7 @@ function Utils.getStationAudio(code, langPrefix)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- Fallback to hardcoded pattern
|
|
||||||
return langPrefix .. code
|
return langPrefix .. code
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2012,7 +1999,6 @@ function Network.register()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Network.syncFiles()
|
function Network.syncFiles()
|
||||||
-- Sync only config file (list.txt) periodically
|
|
||||||
while true do
|
while true do
|
||||||
local resp = http.get(CONFIG.API_URL .. "/list.txt")
|
local resp = http.get(CONFIG.API_URL .. "/list.txt")
|
||||||
if not resp then
|
if not resp then
|
||||||
@@ -2194,7 +2180,7 @@ function Network.messageLoop()
|
|||||||
elseif event == "websocket_closed" then
|
elseif event == "websocket_closed" then
|
||||||
Utils.log("WARN", "WS Closed")
|
Utils.log("WARN", "WS Closed")
|
||||||
State.ws = nil
|
State.ws = nil
|
||||||
State.audioRx = nil -- Clear partial download
|
State.audioRx = nil
|
||||||
os.sleep(5)
|
os.sleep(5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2216,9 +2202,9 @@ function Network.heartbeat()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 7. Display System
|
-- 7. Display System
|
||||||
-- ============================================================================
|
|
||||||
local Display = {}
|
local Display = {}
|
||||||
|
|
||||||
function Display.render()
|
function Display.render()
|
||||||
@@ -2410,9 +2396,9 @@ function Display.renderTerminal()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 7.5 Input Handling
|
-- 7.5 Input Handling
|
||||||
-- ============================================================================
|
|
||||||
local Input = {}
|
local Input = {}
|
||||||
|
|
||||||
function Input.listener()
|
function Input.listener()
|
||||||
@@ -2443,13 +2429,13 @@ function Input.listener()
|
|||||||
Display.renderTerminal()
|
Display.renderTerminal()
|
||||||
Display.render()
|
Display.render()
|
||||||
elseif event == "monitor_touch" and param1 == monitorName then
|
elseif event == "monitor_touch" and param1 == monitorName then
|
||||||
-- Simple touch to scroll: top half of log area scrolls up, bottom half scrolls down
|
|
||||||
local _, monH = Peripherals.primaryMonitor.getSize()
|
local _, monH = Peripherals.primaryMonitor.getSize()
|
||||||
local y = param3
|
local y = param3
|
||||||
if y >= monH - 3 then
|
if y >= monH - 3 then
|
||||||
if y == monH - 3 then -- Top line of log area
|
if y == monH - 3 then
|
||||||
State.logScrollOffset = math.min(State.logScrollOffset + 1, math.max(0, #State.logs - 3))
|
State.logScrollOffset = math.min(State.logScrollOffset + 1, math.max(0, #State.logs - 3))
|
||||||
elseif y == monH then -- Bottom line of log area
|
elseif y == monH then
|
||||||
State.logScrollOffset = math.max(0, State.logScrollOffset - 1)
|
State.logScrollOffset = math.max(0, State.logScrollOffset - 1)
|
||||||
end
|
end
|
||||||
Display.renderTerminal()
|
Display.renderTerminal()
|
||||||
@@ -2484,9 +2470,9 @@ function Input.listener()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 8. Main Logic
|
-- 8. Main Logic
|
||||||
-- ============================================================================
|
|
||||||
local Main = {}
|
local Main = {}
|
||||||
State.announcedApproaching = {}
|
State.announcedApproaching = {}
|
||||||
State.announcedAlerts = {}
|
State.announcedAlerts = {}
|
||||||
@@ -2510,52 +2496,7 @@ local function isPendingAlertReady(key, now)
|
|||||||
return (now - firstSeen) >= (tonumber(CONFIG.ALERT_CONFIRM_MS) or 60000)
|
return (now - firstSeen) >= (tonumber(CONFIG.ALERT_CONFIRM_MS) or 60000)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function parseEtaText(etaStr)
|
|
||||||
etaStr = tostring(etaStr or "")
|
|
||||||
if etaStr == "" then return -1 end
|
|
||||||
|
|
||||||
if string.find(etaStr, "现在", 1, true)
|
|
||||||
or string.find(etaStr, "到达", 1, true)
|
|
||||||
or string.find(etaStr, "^0+$")
|
|
||||||
then
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
|
|
||||||
local mm, ss = string.match(etaStr, "(%d+)%s*:%s*(%d+)")
|
|
||||||
if mm and ss then
|
|
||||||
return tonumber(mm) * 60 + tonumber(ss)
|
|
||||||
end
|
|
||||||
|
|
||||||
local min, sec = string.match(etaStr, "(%d+)%s*分.-(%d+)%s*秒")
|
|
||||||
if min and sec then
|
|
||||||
return tonumber(min) * 60 + tonumber(sec)
|
|
||||||
end
|
|
||||||
|
|
||||||
local secOnly = string.match(etaStr, "(%d+)%s*秒")
|
|
||||||
if secOnly then
|
|
||||||
return tonumber(secOnly)
|
|
||||||
end
|
|
||||||
|
|
||||||
local minOnly = string.match(etaStr, "(%d+)%s*分")
|
|
||||||
if minOnly then
|
|
||||||
return tonumber(minOnly) * 60
|
|
||||||
end
|
|
||||||
|
|
||||||
local num, unit = string.match(etaStr, "(%d+)%s*(.*)")
|
|
||||||
if num then
|
|
||||||
num = tonumber(num)
|
|
||||||
unit = tostring(unit or ""):lower()
|
|
||||||
if unit == "" then
|
|
||||||
return num
|
|
||||||
end
|
|
||||||
if string.find(unit, "秒", 1, true) or string.find(unit, "s", 1, true) or string.find(unit, "sec", 1, true) or (unit == "??" and num > 10) then
|
|
||||||
return num
|
|
||||||
end
|
|
||||||
return num * 60
|
|
||||||
end
|
|
||||||
|
|
||||||
return -1
|
|
||||||
end
|
|
||||||
|
|
||||||
local function normalizeUpcomingKey(trainName, destination)
|
local function normalizeUpcomingKey(trainName, destination)
|
||||||
local function clean(value)
|
local function clean(value)
|
||||||
@@ -2624,8 +2565,7 @@ function Main.loop()
|
|||||||
local etaTextByKey = {}
|
local etaTextByKey = {}
|
||||||
local delayTextByKey = {}
|
local delayTextByKey = {}
|
||||||
|
|
||||||
-- Read CRN text ETA only for display wording.
|
-- ETA come from NBT.
|
||||||
-- Timing decisions and numeric ETA still come from NBT.
|
|
||||||
if Peripherals.createTarget then
|
if Peripherals.createTarget then
|
||||||
local okLines, lines = pcall(Peripherals.createTarget.dump)
|
local okLines, lines = pcall(Peripherals.createTarget.dump)
|
||||||
if okLines and type(lines) == "table" then
|
if okLines and type(lines) == "table" then
|
||||||
@@ -2657,11 +2597,10 @@ function Main.loop()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check Block Reader (Advanced Peripherals) first for precise ETA and Delay
|
|
||||||
if Peripherals.blockReader then
|
if Peripherals.blockReader then
|
||||||
local ok, data = pcall(Peripherals.blockReader.getBlockData)
|
local ok, data = pcall(Peripherals.blockReader.getBlockData)
|
||||||
if ok and data and data.TrainStops then
|
if ok and data and data.TrainStops then
|
||||||
-- Real-time interpolation: estimate actual tick rate (TPS) from CRN refreshes
|
|
||||||
if not State.lastRefreshed or State.lastRefreshed ~= data.LastRefreshed then
|
if not State.lastRefreshed or State.lastRefreshed ~= data.LastRefreshed then
|
||||||
if State.lastRefreshed and State.lastRefreshedEpoch then
|
if State.lastRefreshed and State.lastRefreshedEpoch then
|
||||||
local deltaTicks = (data.LastRefreshed or 0) - (State.lastRefreshed or 0)
|
local deltaTicks = (data.LastRefreshed or 0) - (State.lastRefreshed or 0)
|
||||||
@@ -2711,8 +2650,7 @@ function Main.loop()
|
|||||||
if cancelled then
|
if cancelled then
|
||||||
reason = "cancelled"
|
reason = "cancelled"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Calculate ETA in seconds using interpolated currentTick
|
|
||||||
local scheduledArrival = pickEarlierTick(st.ScheduledArrival, st.ScheduledDeparture)
|
local scheduledArrival = pickEarlierTick(st.ScheduledArrival, st.ScheduledDeparture)
|
||||||
local realArrival = pickEarlierTick(st.RealTimeArrival, st.RealTimeDeparture)
|
local realArrival = pickEarlierTick(st.RealTimeArrival, st.RealTimeDeparture)
|
||||||
local etaTicks = realArrival - currentTick
|
local etaTicks = realArrival - currentTick
|
||||||
@@ -2720,8 +2658,7 @@ function Main.loop()
|
|||||||
if etaSeconds < 0 then etaSeconds = 0 end
|
if etaSeconds < 0 then etaSeconds = 0 end
|
||||||
local announceKey = normalizeUpcomingKey(trainName, destination)
|
local announceKey = normalizeUpcomingKey(trainName, destination)
|
||||||
local etaLabel = buildEtaLabel(etaSeconds, etaTextByKey[announceKey])
|
local etaLabel = buildEtaLabel(etaSeconds, etaTextByKey[announceKey])
|
||||||
|
|
||||||
-- Calculate Delay in minutes
|
|
||||||
local delayTicks = realArrival - scheduledArrival
|
local delayTicks = realArrival - scheduledArrival
|
||||||
local delayMinutes = math.floor(delayTicks / ((State.tickRate or 20) * 60))
|
local delayMinutes = math.floor(delayTicks / ((State.tickRate or 20) * 60))
|
||||||
if delayMinutes < 0 then delayMinutes = 0 end
|
if delayMinutes < 0 then delayMinutes = 0 end
|
||||||
@@ -2811,7 +2748,6 @@ function Main.loop()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Try CC:C Bridge Display Link (Legacy Text Parsing) if Block Reader is not available
|
|
||||||
if not Peripherals.blockReader and Peripherals.createTarget then
|
if not Peripherals.blockReader and Peripherals.createTarget then
|
||||||
local lines = Peripherals.createTarget.dump()
|
local lines = Peripherals.createTarget.dump()
|
||||||
local seenAlertKeys = {}
|
local seenAlertKeys = {}
|
||||||
@@ -2825,7 +2761,6 @@ function Main.loop()
|
|||||||
local delayMatch = string.match(lineText, "%+%s*(%d+)")
|
local delayMatch = string.match(lineText, "%+%s*(%d+)")
|
||||||
local delay = tonumber(delayMatch) or 0
|
local delay = tonumber(delayMatch) or 0
|
||||||
|
|
||||||
-- 从文本中移除延误相关的字眼,防止干扰后续的正则表达式匹配
|
|
||||||
local cleanLine = string.gsub(lineText, "%s*%(?%+%s*%d+.-%)?", "")
|
local cleanLine = string.gsub(lineText, "%s*%(?%+%s*%d+.-%)?", "")
|
||||||
|
|
||||||
local platform, etaStr, tName, dest
|
local platform, etaStr, tName, dest
|
||||||
@@ -2839,8 +2774,7 @@ function Main.loop()
|
|||||||
|
|
||||||
if tName and etaStr then
|
if tName and etaStr then
|
||||||
local announceKey = tName .. "_" .. dest
|
local announceKey = tName .. "_" .. dest
|
||||||
|
|
||||||
-- 将 ETA 转换为秒数进行精准判断
|
|
||||||
local etaSeconds = -1
|
local etaSeconds = -1
|
||||||
local isNow = string.find(etaStr, "现在") or string.find(etaStr, "0")
|
local isNow = string.find(etaStr, "现在") or string.find(etaStr, "0")
|
||||||
|
|
||||||
@@ -2850,7 +2784,7 @@ function Main.loop()
|
|||||||
local num, unit = string.match(etaStr, "(%d+)%s*(.*)")
|
local num, unit = string.match(etaStr, "(%d+)%s*(.*)")
|
||||||
if num then
|
if num then
|
||||||
num = tonumber(num)
|
num = tonumber(num)
|
||||||
-- 如果单位包含秒的字眼,或者是乱码但数值>10(如 45 ??, 30 ??),则认为是秒
|
|
||||||
if string.find(unit, "秒") or string.find(unit, "s") or string.find(unit, "sec") or (unit == "??" and num > 10) then
|
if string.find(unit, "秒") or string.find(unit, "s") or string.find(unit, "sec") or (unit == "??" and num > 10) then
|
||||||
etaSeconds = num
|
etaSeconds = num
|
||||||
else
|
else
|
||||||
@@ -2893,7 +2827,6 @@ function Main.loop()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 触发逻辑:当 ETA 已经明确不大于45秒,且之前没有播报过
|
|
||||||
if etaSeconds >= 0 and etaSeconds <= 45 then
|
if etaSeconds >= 0 and etaSeconds <= 45 then
|
||||||
seenApproachKeys[announceKey] = true
|
seenApproachKeys[announceKey] = true
|
||||||
local pending = State.pendingApproaching[announceKey]
|
local pending = State.pendingApproaching[announceKey]
|
||||||
@@ -2942,7 +2875,7 @@ function Main.loop()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Update Upcoming Trains globally for the monitor
|
|
||||||
State.upcomingTrains = newUpcoming
|
State.upcomingTrains = newUpcoming
|
||||||
|
|
||||||
if not Peripherals.station then
|
if not Peripherals.station then
|
||||||
@@ -3016,9 +2949,9 @@ function Main.loop()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
|
||||||
-- 9. Bootstrap
|
-- 9. Bootstrap
|
||||||
-- ============================================================================
|
|
||||||
AutoUpdate.checkOnBoot()
|
AutoUpdate.checkOnBoot()
|
||||||
Peripherals.init()
|
Peripherals.init()
|
||||||
Utils.ensureSlaveIdentityConfigured()
|
Utils.ensureSlaveIdentityConfigured()
|
||||||
|
|||||||
Reference in New Issue
Block a user