mirror of
https://github.com/radio95-rnt/rds95.git
synced 2026-07-29 08:19:17 +02:00
move some scripts as modules and also change rds1 group hook
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user