move some scripts as modules and also change rds1 group hook

This commit is contained in:
2026-06-20 14:42:10 +02:00
parent a189c56cdf
commit 90d082a845
9 changed files with 107 additions and 102 deletions
+25
View File
@@ -0,0 +1,25 @@
local md = {}
md.registered_groups = {}
hooks.group[#hooks.group+1] = function (group)
if md.registered_groups[group] then
local ok, generated, b, c, d = pcall(md.registered_groups[group], group)
if ok then return generated, b, c, d
else
print(generated)
return false, 0, 0, 0
end
end
return false, 0, 0, 0
end
---@param designator string
---@param handler function
function md.register_group(designator, handler)
for i = 1, #designator do
local char = designator:sub(i, i)
md.registered_groups[char] = handler
end
end
return md
+150
View File
@@ -0,0 +1,150 @@
local rds2_oda = {}
local _RDS2_ODA = { aid = 0, data = 0, handler = false, file_related = false, channel = 0 }
local _RDS2_next_channel = { normal = 16, file = 0 }
function _RDS2_ODA.new(aid, data, handler, file_related, channel)
local instance = { aid = aid or 0, data = data or 0, handler = handler or false, file_related = file_related or false, channel = channel or 0 }
setmetatable(instance, { __index = _RDS2_ODA })
return instance
end
local _RDS2_ODAs = {}
local _RDS2_ODA_aid = 0
local _RDS2_ODA_pointer = 1
---@param aid integer
---@param data integer
---@param file_related boolean
---@return integer oda_id
function rds2_oda.register_oda_rds2(aid, data, file_related)
local channel
if file_related then
channel = _RDS2_next_channel.file
_RDS2_next_channel.file = _RDS2_next_channel.file + 1
else
channel = _RDS2_next_channel.normal
_RDS2_next_channel.normal = _RDS2_next_channel.normal + 1
end
local oda = _RDS2_ODA.new(aid, data, false, file_related, channel)
for i = 1, #_RDS2_ODAs do
if _RDS2_ODAs[i] == false then
_RDS2_ODAs[i] = oda
return i
end
end
table.insert(_RDS2_ODAs, oda)
return #_RDS2_ODAs
end
---Unregisters an RDS 2 ODA, this stops the handler or AID being called/sent
---@param oda_id integer
function rds2_oda.unregister_oda_rds2(oda_id)
if oda_id < 1 or oda_id > #_RDS2_ODAs or _RDS2_ODAs[oda_id] == false then error("Invalid ODA ID: " .. tostring(oda_id), 2) end
_RDS2_ODAs[oda_id] = false
if _RDS2_ODA_pointer == oda_id then _RDS2_ODA_pointer = _RDS2_ODA_pointer + 1 end
end
---@param oda_id integer
---@param data integer
function rds2_oda.set_oda_id_data_rds2(oda_id, data)
if oda_id < 1 or oda_id > #_RDS2_ODAs or _RDS2_ODAs[oda_id] == false then error("Invalid ODA ID: " .. tostring(oda_id), 2) end
_RDS2_ODAs[oda_id].data = data
end
---The callback function for an ODA handler
---@alias RDS2_ODAHandler fun(integer): (boolean, integer, integer, integer, integer)
---You are asked to not fill in the channel id in block A, however you are asked to fill in the function number (if you do not know what is that, just OR block A with (1 << 14))
---@param oda_id integer
---@param func RDS2_ODAHandler
function rds2_oda.set_oda_handler_rds2(oda_id, func)
if oda_id < 1 or oda_id > #_RDS2_ODAs or _RDS2_ODAs[oda_id] == false then error("Invalid ODA ID: " .. tostring(oda_id), 2) end
_RDS2_ODAs[oda_id].handler = func
end
function rds2_oda.rds2_group(stream)
if #_RDS2_ODAs == 0 then return false, 0, 0, 0, 0 end
if _RDS2_ODA_pointer > #_RDS2_ODAs then _RDS2_ODA_pointer = 1 end
local checked = 0
while checked < #_RDS2_ODAs and _RDS2_ODAs[_RDS2_ODA_pointer] == false do
_RDS2_ODA_pointer = _RDS2_ODA_pointer + 1
if _RDS2_ODA_pointer > #_RDS2_ODAs then _RDS2_ODA_pointer = 1 end
checked = checked + 1
end
if checked == #_RDS2_ODAs then return false, 0, 0, 0, 0 end
local oda = _RDS2_ODAs[_RDS2_ODA_pointer]
local channel = oda.channel
if oda.file_related then channel = channel & 0xF end
_RDS2_ODA_pointer = _RDS2_ODA_pointer + 1
local next_oda = nil
if _RDS2_ODA_pointer <= #_RDS2_ODAs then next_oda = _RDS2_ODAs[_RDS2_ODA_pointer] end
if _RDS2_ODA_aid == 0 and stream == 1 then
_RDS2_ODA_aid = 1
local block1_base = (2 << 14) | channel
if next_oda and oda.data > 0 and oda.data <= 0xFFFF and next_oda.data == 0 then
_RDS2_ODA_pointer = _RDS2_ODA_pointer + 1
return true, block1_base | (1 << 6), oda.aid, oda.data, next_oda.aid
elseif next_oda and oda.data == 0 and next_oda.data > 0 and next_oda.data <= 0xFFFF then
_RDS2_ODA_pointer = _RDS2_ODA_pointer + 1
return true, block1_base | (2 << 6), oda.aid, next_oda.aid, next_oda.data
elseif next_oda ~= nil and oda.data == 0 and next_oda.data == 0 then
_RDS2_ODA_pointer = _RDS2_ODA_pointer + 1
if _RDS2_ODA_pointer <= #_RDS2_ODAs then
local third_oda = _RDS2_ODAs[_RDS2_ODA_pointer]
_RDS2_ODA_pointer = _RDS2_ODA_pointer + 1
return true, block1_base | (3 << 6), oda.aid, next_oda.aid, third_oda.aid
else return true, block1_base, oda.aid, (oda.data >> 16) & 0xffff, oda.data & 0xffff end
end
return true, block1_base, oda.aid, (oda.data >> 16) & 0xffff, oda.data & 0xffff
else
_RDS2_ODA_aid = _RDS2_ODA_aid + 1
if _RDS2_ODA_aid > 8 then _RDS2_ODA_aid = 0 end
if oda.handler then
local generated = false
checked = 0
while generated == false and checked < #_RDS2_ODAs do
local ok, a, b, c, d
ok, generated, a, b, c, d = pcall(oda.handler, stream)
if not (generated and ok) then
_RDS2_ODA_pointer = _RDS2_ODA_pointer + 1
if _RDS2_ODA_pointer > #_RDS2_ODAs then _RDS2_ODA_pointer = 1 end
oda = _RDS2_ODAs[_RDS2_ODA_pointer]
checked = checked + 1
else
local channel_bitshift = 8
local fid = (a & 0xC000) >> 14
local fn_msb = (a >> 13) & 1
if fid == 0 and fn_msb == 0 then return true, 0, b, c, d -- Tunnel, not sure why but sure
-- FID == 0 and FN_MSB = 1 means RFT data, file data
-- FID == 1 is the standard ODA group
-- FID == 2 is AID
-- FID == 3 is reserved
elseif fid == 2 and fn_msb == 0 then channel_bitshift = 0 end -- This is AID
return generated, (channel << channel_bitshift) | a, b, c, d
end
end
if checked == #_RDS2_ODAs then return false, 0, 0, 0, 0 end
end
return true, (2 << 14) | channel, oda.aid, (oda.data >> 16) & 0xffff, oda.data & 0xffff
end
end
table.insert(hooks.on_state, function ()
_RDS2_ODAs = {}
_RDS2_ODA_aid = 0
_RDS2_ODA_pointer = 1
end)
return rds2_oda
+172
View File
@@ -0,0 +1,172 @@
local scheduler = require("rds2_oda")
---@class RftInstance
---@field oda_id integer|nil The internal ODA registration ID
---@field file_data string The raw binary content of the file
---@field crc_data string Calculated CRC chunks for Variant 1 groups
---@field file_segment integer Current segment index being sent
---@field crc_segment integer Current CRC byte index being sent
---@field toggle boolean The RDS2 toggle bit (flips when file content changes)
---@field last_id integer The file ID used in the previous transmission
---@field version integer The file version (0-7)
---@field crc_enabled boolean Whether CRC protection is active
---@field crc_full_file integer The CRC16 of the entire file (for mode 0)
---@field crc_mode integer The RDS2 RFT CRC mode (0-5)
---@field crc_sent_this_cycle boolean Internal flag to interleave CRCs with data
---@field aid integer The Application Identification (e.g., 0xFF7F for Logo)
---@field send_once boolean If true, unregisters after one full transmission
---@field paused boolean If true, the ODA handler will return empty groups
RftInstance = {}
RftInstance.__index = RftInstance
--- Creates a new RFT Manager instance.
---@return RftInstance
function RftInstance.new()
local self = setmetatable({}, RftInstance)
self.oda_id = nil
self.file_data = ""
self.crc_data = ""
self.file_segment = 0
self.crc_segment = 1
self.toggle = false
self.last_id = -1
self.version = 0
self.crc_enabled = false
self.crc_full_file = 0
self.crc_mode = 0
self.crc_sent_this_cycle = false
self.aid = 0
self.send_once = false
self.paused = false
return self
end
function RftInstance:stop()
if self.oda_id ~= nil and self.aid ~= 0 then
scheduler.unregister_oda_rds2(self.oda_id)
self.oda_id = nil
end
self.file_data = ""
self.crc_data = ""
self.file_segment = 0
self.crc_segment = 1
self.paused = false
end
--- Internal method to start the ODA handler logic.
--- Processes RDS2 RFT Variants 0, 1 and Data Groups.
---@private
function RftInstance:start()
if self.oda_id == nil and self.aid ~= 0 then
self.oda_id = scheduler.register_oda_rds2(self.aid, 0, true)
scheduler.set_oda_handler_rds2(self.oda_id, function(stream)
if #self.file_data == 0 or self.paused then
return false, 0, 0, 0, 0
end
local total_segments = math.ceil(#self.file_data / 5)
if not self.crc_sent_this_cycle and self.crc_enabled and (self.file_segment % 16 == 0) then
self.crc_sent_this_cycle = true
local chunk_address = math.floor((self.crc_segment - 1) / 2)
local c = (1 << 12) | (self.crc_mode & 7) << 9 | (chunk_address & 0x1ff)
local high_byte, low_byte
if self.crc_mode ~= 0 then
high_byte = string.byte(self.crc_data, self.crc_segment) or 0
low_byte = string.byte(self.crc_data, self.crc_segment + 1) or 0
else
high_byte = self.crc_full_file >> 8
low_byte = self.crc_full_file & 0xff
end
self.crc_segment = self.crc_segment + 2
if self.crc_segment > #self.crc_data then self.crc_segment = 1 end
return true, (2 << 14), self.aid, c, (high_byte << 8) | low_byte
elseif self.crc_sent_this_cycle then self.crc_sent_this_cycle = false end
local base = self.file_segment * 5 + 1
local function b(i) return string.byte(self.file_data, base + i) or 0 end
local word1 = (((self.toggle and 1 or 0) << 7) | ((self.file_segment >> 8) & 0x7F))
local word2 = ((self.file_segment & 0xFF) << 8) | b(0)
local word3 = (b(1) << 8) | b(2)
local word4 = (b(3) << 8) | b(4)
self.file_segment = self.file_segment + 1
if self.file_segment >= total_segments then
self.file_segment = 0
if self.send_once then self:stop() end
end
return true, (2 << 12) | word1, word2, word3, word4
end)
end
end
--- Loads a file and begins RDS2 transmission.
---@param aid integer Application ID (e.g. 0xFF7F for Station Logo)
---@param path string System path to the file
---@param id integer File ID (0-63). Use the same ID to update a file, different to trigger a reset.
---@param crc integer|boolean CRC Mode (0: Full, 1-5: Chunks, true/7: Auto)
---@param once boolean If true, file is sent once and the stream is closed
---@return boolean interrupted Returns true if a previous file was already being sent
function RftInstance:sendFile(aid, path, id, crc, once)
local interrupted = (#self.file_data ~= 0)
if self.aid ~= aid then self:stop() end
self.aid = aid
local file = io.open(path, "rb")
if not file then error("Could not open file: " .. path) end
self.file_data = file:read("*a")
file:close()
self.send_once = once
if id == self.last_id then
self.toggle = not self.toggle
self.version = (self.version + 1) % 8
end
self.crc_data = ""
self.crc_enabled = (crc ~= false)
local f_size = #self.file_data
if crc == 0 then
self.crc_mode = 0
self.crc_full_file = Data.crc16(self.file_data)
elseif crc == true or crc == 7 then
if f_size <= 40960 then self.crc_mode = 1
elseif f_size > 40960 and f_size <= 81920 then self.crc_mode = 2
else self.crc_mode = 3 end
else
self.crc_mode = crc and 1 or 0
end
local chunk_multipliers = {16, 32, 64, 128, 256}
local multiplier = chunk_multipliers[self.crc_mode]
if self.crc_enabled and multiplier then
local chunk_size = 5 * multiplier
for i = 1, f_size, chunk_size do
local chunk = string.sub(self.file_data, i, i + chunk_size - 1)
local v = Data.crc16(chunk)
self.crc_data = self.crc_data .. string.char(v >> 8, v & 0xff)
end
end
if f_size > (0x3ffff * 5) then error("File too large") end
if self.oda_id == nil then self:start() end
scheduler.set_oda_id_data_rds2(self.oda_id, f_size | (id & 63) << 18 | (self.version & 7) << 24 | (self.crc_enabled and 1 or 0) << 27)
self.last_id = id
self.paused = false
return interrupted
end
return RftInstance