ipc thing, and some more stuff i guess

This commit is contained in:
2026-07-11 13:37:41 +02:00
parent 564ea1c20e
commit 5bad2b8200
17 changed files with 271 additions and 264 deletions
+123
View File
@@ -0,0 +1,123 @@
local oda = require("oda")
---@class AfModule
local af = {}
local _Af_Oda_id = nil
local _Af_Oda_state = 0
local _Af_Oda_len = 0
local _Af_Oda_afs = {}
local USERDATA_ODA_OFFSET = 274
---@param freq number
---@return table
local function encode_af(freq)
local out = {}
if(freq >= 87.6 and freq <= 107.9) then
table.insert(out, math.tointeger((freq * 10) - 875))
elseif freq >= 64.1 and freq <= 88.0 then
table.insert(out, math.tointeger((freq * 10) - 384))
elseif freq >= 153 and freq <= 279 then
table.insert(out, 250) -- LFMF incoming
table.insert(out, math.tointeger((freq - 153) / 9 + 1))
elseif freq >= 531 and freq <= 1602 then
table.insert(out, 250) -- LFMF incoming
table.insert(out, math.tointeger((freq - 531) / 9 + 16))
else error(string.format("Invalid AF frequency: %.1f", freq), 2) end
return out
end
local function get_next_af_oda_group()
local af = {0, 0, 0, 0}
local num_entries = #_Af_Oda_afs
local i = 1
if _Af_Oda_state == 0 then
af[1] = 224 + math.min(_Af_Oda_len, 25)
_Af_Oda_state = 1
i = 2
end
while i <= 4 do
if _Af_Oda_state <= num_entries then
af[i] = _Af_Oda_afs[_Af_Oda_state]
_Af_Oda_state = _Af_Oda_state + 1
else af[i] = 205 end
i = i + 1
end
if _Af_Oda_state > num_entries then _Af_Oda_state = 0 end
local b = (7 << 12)
for j = 1, 4 do
local bit8 = (math.tointeger(af[j]) >> 8) & 0x01
b = b | (bit8 << (j - 1))
end
return b, ((math.tointeger(af[1]) & 0xFF) << 8) | (math.tointeger(af[2]) & 0xFF), ((math.tointeger(af[3]) & 0xFF) << 8) | (math.tointeger(af[4]) & 0xFF)
end
local function init_af_oda()
if _Af_Oda_id == nil then
_Af_Oda_id = oda.register_oda(7, false, 0x6365, 0, false)
oda.set_oda_handler(_Af_Oda_id, function()
local b, c, d = get_next_af_oda_group()
return true, b, c, d
end)
end
end
local function save_af_to_userdata(afs)
local count = #afs
if count > 25 then count = 25 end
local payload = string.pack("B", count)
for i = 1, count do payload = payload .. string.pack("f", afs[i]) end
userdata.set_offset(USERDATA_ODA_OFFSET, #payload, payload)
end
local function _process_af_list(afs)
_Af_Oda_afs = {}
_Af_Oda_len = #afs
for _, f in ipairs(afs) do
local codes = encode_af(f)
for _, code in ipairs(codes) do
table.insert(_Af_Oda_afs, code)
end
end
_Af_Oda_state = 0
init_af_oda()
end
local function load_af_from_userdata()
local header = userdata.get_offset(USERDATA_ODA_OFFSET, 1)
if header == "" or header == nil then return end
local count = string.unpack("B", header)
if count == 0 or count > 25 then return end
local data = userdata.get_offset(USERDATA_ODA_OFFSET + 1, count * 4)
if #data < (count * 4) then return end
local afs = {}
for i = 1, count do
local val = string.unpack("f", data, (i-1)*4 + 1)
table.insert(afs, val)
end
_process_af_list(afs)
end
---Sets the AFs included in the ODA and saves them
---@param afs table List of numbers (e.g., {98.1, 102.5})
function af.set_af_oda(afs)
_process_af_list(afs)
save_af_to_userdata(afs)
end
table.insert(hooks.on_state, function ()
load_af_from_userdata()
if _Af_Oda_len ~= 0 then init_af_oda() end
end)
return af
+91
View File
@@ -0,0 +1,91 @@
local oda = require("oda")
---@class ErtModule
local ert = {}
local _Ert_state = 0
local _Ert_oda_id = nil
--- Size: 259 bytes
local USERDATA_ERT_OFFSET = 0
local function init_ert()
if _Ert_oda_id == nil then
_Ert_oda_id = oda.register_oda(13, false, 0x6552, 1, false)
oda.set_oda_handler(_Ert_oda_id, function ()
if string.byte(userdata.get_offset(USERDATA_ERT_OFFSET+258, 1)) == 1 then
local new_data = userdata.get_offset(USERDATA_ERT_OFFSET, 128)
local new_segments = string.byte(userdata.get_offset(USERDATA_ERT_OFFSET+128, 1))
userdata.set_offset(USERDATA_ERT_OFFSET+129, 128, new_data)
userdata.set_offset(USERDATA_ERT_OFFSET+257, 1, string.char(new_segments))
userdata.set_offset(USERDATA_ERT_OFFSET+258, 1, string.char(0))
_Ert_state = 0
end
local segments = string.byte(userdata.get_offset(USERDATA_ERT_OFFSET+257, 1))
if segments == 0 then return false, 0, 0, 0 end
if _Ert_state >= segments then _Ert_state = 0 end
local b = _Ert_state & 31
local chunk = userdata.get_offset(USERDATA_ERT_OFFSET + 129 + _Ert_state * 4, 4)
local c = (string.byte(chunk, 1) << 8) | string.byte(chunk, 2)
local d = (string.byte(chunk, 3) << 8) | string.byte(chunk, 4)
_Ert_state = (_Ert_state + 1) % segments
return true, b, c, d
end)
end
end
function ert.unregister_ert()
if _Ert_oda_id ~= nil then
oda.unregister_oda(_Ert_oda_id)
_Ert_oda_id = nil
end
end
function ert.set_ert(text)
if #text == 0 then
userdata.set_offset(USERDATA_ERT_OFFSET, 128, "")
userdata.set_offset(USERDATA_ERT_OFFSET+128, 1, string.char(0))
userdata.set_offset(USERDATA_ERT_OFFSET+258, 1, string.char(1))
return
end
local data = text .. "\r"
data = string.sub(data, 1, 128)
local padding = (4 - (#data % 4)) % 4
data = data .. string.rep("\0", padding)
userdata.set_offset(USERDATA_ERT_OFFSET, 128, data)
local segments = #data // 4
if segments > 32 then segments = 32 end
userdata.set_offset(USERDATA_ERT_OFFSET+128, 1, string.char(segments))
if string.byte(userdata.get_offset(USERDATA_ERT_OFFSET+257, 1)) == 0 then
init_ert()
userdata.set_offset(USERDATA_ERT_OFFSET+129, 128, data)
userdata.set_offset(USERDATA_ERT_OFFSET+257, 1, string.char(segments))
_Ert_state = 0
else userdata.set_offset(USERDATA_ERT_OFFSET+258, 1, string.char(1)) end
if _Ert_oda_id == nil then init_ert() end
end
function ert.get_ert()
local segments = string.byte(userdata.get_offset(USERDATA_ERT_OFFSET+128, 1))
if segments == 0 then return "" end
local data = userdata.get_offset(USERDATA_ERT_OFFSET, 128)
return data:match("^(.-)[\r%z]*") or ""
end
table.insert(hooks.on_state, function ()
if string.byte(userdata.get_offset(USERDATA_ERT_OFFSET+257, 1)) ~= 0 then init_ert() end
end)
return ert
+133
View File
@@ -0,0 +1,133 @@
---@class OdaModule
local oda = {}
local _ODA = { group = 0, group_version = false, aid = 0, data = 0, handler = false, temp = false }
function _ODA.new(group, group_version, aid, data, handler, temp)
local instance = { group = group or 0, group_version = group_version or false, aid = aid or 0, data = data or 0, handler = handler or false, temp = temp or false }
setmetatable(instance, { __index = _ODA })
return instance
end
local _RDS_ODAs = {}
local _RDS_ODA_pointer = 1
---Registers an ODA to be used in the '\x06' of the group sequence. ODAs are stored as state data, thus running reset_rds will clear it
---Groups 14, 15, 2, 0 cannot be registered either version, groups 10, 4, 1 can be only registered as B, any other is free to take
---Group 3A will mean that there will be no group handler for this ODA, meaning it can only be interacted with via the 3A AID group, handler set is not possible with such groups
---@param group integer
---@param group_version boolean
---@param aid integer
---@param data integer
---@param temp boolean
---@return integer oda_id
function oda.register_oda(group, group_version, aid, data, temp)
if group == 14 or group == 15 or group == 2 or group == 0 then error("Group is incorrect", 2) end
if (group == 10 or group == 4 or group == 1) and group_version then error("Group is incorrect", 2) end
local oda = _ODA.new(group, group_version, aid, data, false, temp)
for i = 1, #_RDS_ODAs do
if _RDS_ODAs[i] == false then
_RDS_ODAs[i] = oda
return i
end
end
table.insert(_RDS_ODAs, oda)
return #_RDS_ODAs
end
---Unregisters an ODA, this stops the handler or AID being called/sent
---@param oda_id integer
function oda.unregister_oda(oda_id)
if oda_id < 1 or oda_id > #_RDS_ODAs or _RDS_ODAs[oda_id] == false then error("Invalid ODA ID: " .. tostring(oda_id), 2) end
_RDS_ODAs[oda_id] = false
if _RDS_ODA_pointer == oda_id then _RDS_ODA_pointer = (_RDS_ODA_pointer % #_RDS_ODAs) + 1 end
end
---Sets the id_data for a existing ODA group
---@param oda_id integer
---@param data integer
function oda.set_oda_id_data(oda_id, data)
if oda_id < 1 or oda_id > #_RDS_ODAs or _RDS_ODAs[oda_id] == false then error("Invalid ODA ID: " .. tostring(oda_id), 2) end
_RDS_ODAs[oda_id].data = data
end
---@alias ODAHandler fun(): (boolean, integer, integer, integer)
---Sets a function to handle the ODA data generation.
---The handler is called when the group sequence '\xff' slot is processed.
---The function must return 3 integers representing RDS Blocks B, C, and D.
---Please note that you do not need to compute the block A to indentify the group and group version, that will be done for you and EVERY SINGLE group has PTY and TP inserted (and also PI if its a B)
---You are asked to set groups B last 5 bits, leave rest 0
---@param oda_id integer The ID returned by register_oda
---@param fun ODAHandler
function oda.set_oda_handler(oda_id, fun)
if oda_id < 1 or oda_id > #_RDS_ODAs or _RDS_ODAs[oda_id] == false then error("Invalid ODA ID: " .. tostring(oda_id), 2) end
if _RDS_ODAs[oda_id].group == 3 then error("3A ODAs cannot have handlers.", 2) end
_RDS_ODAs[oda_id].handler = fun
end
local function get_aid()
local checked = 0
while checked < #_RDS_ODAs do
local oda = _RDS_ODAs[_RDS_ODA_pointer]
if oda ~= false then
local b = 3 << 12 | oda.group << 1 | (oda.group_version and 1 or 0)
local data, aid = oda.data, oda.aid
if oda.temp then _RDS_ODAs[_RDS_ODA_pointer] = false end
_RDS_ODA_pointer = (_RDS_ODA_pointer % #_RDS_ODAs) + 1
return true, b, data, aid
end
_RDS_ODA_pointer = (_RDS_ODA_pointer % #_RDS_ODAs) + 1
checked = checked + 1
end
return false, 0, 0, 0
end
local function get_data()
local checked_count = 0
while checked_count < #_RDS_ODAs do
local oda = _RDS_ODAs[_RDS_ODA_pointer]
if oda ~= false and type(oda.handler) == "function" and not oda.temp then
local ok, generated, b, c, d = pcall(oda.handler)
if ok and generated then
_RDS_ODA_pointer = (_RDS_ODA_pointer % #_RDS_ODAs) + 1
b = b | oda.group << 12
b = b | (oda.group_version and 1 or 0) << 11
return generated, b, c, d
end
end
_RDS_ODA_pointer = (_RDS_ODA_pointer % #_RDS_ODAs) + 1
checked_count = checked_count + 1
end
return false, 0, 0, 0
end
local function group_handler(group_type)
if #_RDS_ODAs == 0 then return false, 0, 0, 0 end
if _RDS_ODA_pointer > #_RDS_ODAs or _RDS_ODA_pointer < 1 then _RDS_ODA_pointer = 1 end
if group_type == "\x06" then return get_aid()
elseif group_type == "\xff" then return get_data()
else return false, 0, 0, 0 end
end
require("group").register_group("\x06\xff", group_handler)
table.insert(hooks.on_state, function ()
_RDS_ODAs = {}
_RDS_ODA_pointer = 1
end)
return oda
+101
View File
@@ -0,0 +1,101 @@
local oda = require("oda")
---@class RtpModule
local rtp = {}
local _Rtp_oda_id = nil
local _Ertp_oda_id = nil
local _Rtp_toggle = false
local _Ertp_toggle = false
--- Size: 15 bytes
local USERDATA_RTP_OFFSET = 259
local function init_rtp()
if _Rtp_oda_id == nil then
_Rtp_oda_id = oda.register_oda(11, false, 0x4BD7, 0, false)
oda.set_oda_handler(_Rtp_oda_id, function ()
local b = (_Rtp_toggle and 1 or 0) << 4 | string.byte(userdata.get_offset(USERDATA_RTP_OFFSET, 1)) << 3
local data_0 = userdata.get_offset(USERDATA_RTP_OFFSET+1, 3)
local data_1 = userdata.get_offset(USERDATA_RTP_OFFSET+4, 3)
b = b | (string.byte(data_0, 1) & 0xf8) >> 3
local c = (string.byte(data_0, 1) & 0x7) << 13
c = c | (string.byte(data_0, 2) & 0x3f) << 7
c = c | (string.byte(data_0, 3) & 0x3f) << 1
c = c | (string.byte(data_1, 1) & 0xe0) >> 5
local d = (string.byte(data_1, 1) & 0x1f) << 11
d = d | (string.byte(data_1, 2) & 0x3f) << 5
d = d | (string.byte(data_1, 3) & 0x1f)
return true, b, c, d
end)
end
end
local function init_ertp()
if _Ertp_oda_id == nil then
_Ertp_oda_id = oda.register_oda(12, false, 0x4BD8, 0, false)
oda.set_oda_handler(_Ertp_oda_id, function ()
local b = (_Ertp_toggle and 1 or 0) << 4 | string.byte(userdata.get_offset(USERDATA_RTP_OFFSET+7, 1)) << 3
local data_0 = userdata.get_offset(USERDATA_RTP_OFFSET+8, 3)
local data_1 = userdata.get_offset(USERDATA_RTP_OFFSET+11, 3)
b = b | (string.byte(data_0, 1) & 0xf8) >> 3
local c = (string.byte(data_0, 1) & 0x7) << 13
c = c | (string.byte(data_0, 2) & 0x3f) << 7
c = c | (string.byte(data_0, 3) & 0x3f) << 1
c = c | (string.byte(data_1, 1) & 0xe0) >> 5
local d = (string.byte(data_1, 1) & 0x1f) << 11
d = d | (string.byte(data_1, 2) & 0x3f) << 5
d = d | (string.byte(data_1, 3) & 0x1f)
return true, b, c, d
end)
end
end
function rtp.set_rtp_meta(ertp, running)
if ertp then
if running and _Ertp_oda_id == nil then init_ertp() end
userdata.set_offset(USERDATA_RTP_OFFSET+7, 1, string.char(running and 1 or 0))
else
if running and _Rtp_oda_id == nil then init_rtp() end
userdata.set_offset(USERDATA_RTP_OFFSET, 1, string.char(running and 1 or 0))
end
end
function rtp.get_rtp_meta(ertp)
local offset = ertp and (USERDATA_RTP_OFFSET+7) or USERDATA_RTP_OFFSET
return string.byte(userdata.get_offset(offset, 1)) ~= 0
end
function rtp.toggle_rtp(ertp)
if ertp then _Ertp_toggle = not _Ertp_toggle
else _Rtp_toggle = not _Rtp_toggle end
end
function rtp.set_rtplus_tags(ertp, t1, s1, l1, t2, s2, l2)
rtp.set_rtp_meta(ertp, true)
rtp.toggle_rtp(ertp)
userdata.set_offset(ertp and (USERDATA_RTP_OFFSET+8) or (USERDATA_RTP_OFFSET+1), 6, string.char(t1, s1, l1, t2, s2, l2))
end
function rtp.get_rtplus_tags(ertp)
return string.byte(userdata.get_offset(ertp and (USERDATA_RTP_OFFSET+8) or (USERDATA_RTP_OFFSET+1), 6), 1, 6)
end
function rtp.unregister_rtp(ertp)
if ertp and _Ertp_oda_id ~= nil then
oda.unregister_oda(_Ertp_oda_id)
_Ertp_oda_id = nil
elseif _Rtp_oda_id ~= nil then
oda.unregister_oda(_Rtp_oda_id)
_Rtp_oda_id = nil
end
end
table.insert(hooks.on_state, function ()
if rtp.get_rtp_meta(false) then init_rtp() end
if rtp.get_rtp_meta(true) then init_ertp() end
end)
return rtp
+432
View File
@@ -0,0 +1,432 @@
---@class UecpModule
local uecp = {}
uecp.site_addreses = {}
uecp.encoder_addreses = {}
uecp.rt_buffer = {}
uecp.rt_buffer_index = 1
uecp.rt_tx_remaining = 0
uecp.freedata_buffer = {}
uecp.freedata_buffer_index = 1
hooks.rt_transmission[#hooks.rt_transmission + 1] = function ()
if #uecp.rt_buffer == 0 then return end
if #uecp.rt_buffer == 1 then return end
if uecp.rt_tx_remaining > 1 then
uecp.rt_tx_remaining = uecp.rt_tx_remaining - 1
return
end
uecp.rt_buffer_index = (uecp.rt_buffer_index % #uecp.rt_buffer) + 1
local entry = uecp.rt_buffer[uecp.rt_buffer_index]
uecp.rt_tx_remaining = entry.number_tx
RDS.set_rt_raw(entry.text)
if entry.toggle_ab then RDS.toggle_rt_ab() end
end
---@param dsn integer
---@param write function
local function dsn_helper(dsn, write)
if dsn == 0 then write()
elseif dsn == 254 then
local start = Data.get_writing_program()
for i = 1, Data.max_programs do
local p = i - 1
if p ~= start then
Data.set_writing_program(p)
write()
end
end
elseif dsn == 255 then
for i = 1, Data.max_programs do
Data.set_writing_program(i - 1)
write()
end
else
Data.set_writing_program(dsn - 1)
write()
end
end
local mec_handlers = {}
mec_handlers[0x01] = function(data)
-- PI
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local pi_msb = string.byte(data, 4)
local pi_lsb = string.byte(data, 5)
dsn_helper(dsn, function()
RDS.pi = (pi_msb << 8) | pi_lsb
end)
return 5
end
mec_handlers[0x02] = function(data)
-- PS
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local ps = string.sub(data, 4, 11) -- Static len
dsn_helper(dsn, function()
RDS.set_ps(ps)
end)
return 11
end
mec_handlers[0x21] = function(data)
-- LPS
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local mel = string.byte(data, 4)
local lps = string.sub(data, 5, 5+mel)
local cut = string.find(lps, "\r", 1, true)
if cut then lps = string.sub(lps, 1, cut - 1) end
dsn_helper(dsn, function()
RDS.set_lps(lps)
end)
return 4 + mel
end
mec_handlers[0x04] = function(data)
-- PTYI
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local data = string.byte(data, 4)
dsn_helper(dsn, function ()
RDS.dpty = (data & 8) ~= 0
end)
return 4
end
mec_handlers[0x03] = function(data)
-- TP/TA
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local data = string.byte(data, 4)
dsn_helper(dsn, function ()
RDS.ta = (data & 1) ~= 0
RDS.tp = (data & 2) ~= 0
end)
return 4
end
mec_handlers[0x07] = function(data)
-- PTY
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local data = string.byte(data, 4)
dsn_helper(dsn, function ()
RDS.pty = data
end)
return 4
end
mec_handlers[0x3E] = function(data)
-- PTYN
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local ptyn = string.sub(data, 4, 11) -- Static
dsn_helper(dsn, function ()
RDS.set_ptyn(ptyn)
end)
return 11
end
mec_handlers[0x0A] = function(data)
-- RT
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local mel = string.byte(data, 4)
if mel == 0 then
uecp.rt_buffer = {}
uecp.rt_buffer_index = 1
uecp.rt_tx_remaining = 0
dsn_helper(dsn, function ()
RDS.rt_enabled = false
end)
return 4
end
local config = string.byte(data, 5)
local toggle_ab = (config & 1) ~= 0
local number_tx = (config >> 1) & 0xF
local buffer_config = (config >> 5) & 3
if mel == 1 and buffer_config == 0 then
uecp.rt_buffer = {}
uecp.rt_buffer_index = 1
uecp.rt_tx_remaining = 0
dsn_helper(dsn, function ()
RDS.rt_enabled = false
end)
return 5
end
local rt_text = string.sub(data, 6, 4 + mel)
local cr = string.find(rt_text, "\r", 1, true)
if cr then rt_text = string.sub(rt_text, 1, cr - 1) end
if buffer_config == 0 then
uecp.rt_buffer = {}
uecp.rt_buffer_index = 1
uecp.rt_buffer[1] = { text = rt_text, number_tx = number_tx, toggle_ab = toggle_ab }
uecp.rt_tx_remaining = number_tx
-- Seed the encoder immediately with the first entry
dsn_helper(dsn, function ()
RDS.set_rt_raw(rt_text)
if toggle_ab then RDS.toggle_rt_ab() end
RDS.rt_enabled = true
end)
elseif buffer_config == 2 then
uecp.rt_buffer[#uecp.rt_buffer + 1] = { text = rt_text, number_tx = number_tx, toggle_ab = toggle_ab }
end
return 4 + mel
end
mec_handlers[0x13] = function (data)
-- AF
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local mel = string.sub(data, 4)
-- TODO: decode - i have zero clue what the hell is the meaning of the docs, it might as well be in chinese (traditional)
return 4+mel
end
mec_handlers[0x14] = function (data)
-- EON-AF
local dsn = string.byte(data, 2)
local psn = string.byte(data, 3)
local mel = string.sub(data, 4)
-- TODO: decode - haven't read this one
return 4+mel
end
mec_handlers[0x1A] = function (data)
-- ECC / SLS
local dsn = string.byte(data, 2)
-- No PSN
local data = string.sub(data, 3)
local data_lsb = string.sub(data, 4)
local full_data = ((data & 15) << 8) | data_lsb
local variant = (data >> 4) & 3
if variant == 0 then
-- TODO: maybe make it more than just ecc? idk why but would be cool
dsn_helper(dsn, function ()
RDS.ecc = full_data
end)
end
return 4
end
mec_handlers[0x2E] = function (data)
-- Linkage Information
-- What? What is the main PSN? Does the server tell me that or am i to fucking guess?
-- (yes it does tell me that :) mec 0x28)
return 5
end
hooks.pre_tx[#hooks.pre_tx+1] = function()
if #uecp.freedata_buffer == 0 then return end
local entry = uecp.freedata_buffer[uecp.freedata_buffer_index]
uecp.freedata_buffer_index = uecp.freedata_buffer_index + 1
if uecp.freedata_buffer_index > #uecp.freedata_buffer then
uecp.freedata_buffer_index = 1
end
RDS.put_custom_group(entry.b, entry.c, entry.d)
end
mec_handlers[0x24] = function (data)
-- Free-format data in type A or B group
local group = string.byte(data, 2)
local bufferdata = string.byte(data, 3)
local buffer = (bufferdata >> 5) & 3
local blockb = bufferdata & 31
local blockc_msb = string.byte(data, 4)
local blockc_lsb = string.byte(data, 5)
local blockd_msb = string.byte(data, 6)
local blockd_lsb = string.byte(data, 7)
local b, c, d = (group << 11) | blockb, (blockc_msb << 8) | blockc_lsb, (blockd_msb << 8) | blockd_lsb
if buffer == 0 then
RDS.put_custom_group(b, c, d)
elseif buffer == 2 then
uecp.freedata_buffer[#uecp.freedata_buffer + 1] = { b = b, c = c, d = d }
elseif buffer == 3 then
uecp.freedata_buffer = {}
uecp.freedata_buffer_index = 1
end
return 7
end
mec_handlers[0x40] = function(data)
local group = string.byte(data, 2)
local aid_msb = string.byte(data, 3)
local aid_lsb = string.byte(data, 4)
local buffer = string.byte(data, 5) & 3
local msg_msb = string.byte(data, 6) -- 6
local msg_lsb = string.byte(data, 7) -- 7
local timeout = string.byte(data, 8)
-- TODO:
return 8
end
-- Fuck you mean, i have to implement some fucking "spinning wHELLs"? may the lord have mercy
-- Also no time setting via UECP - this is linux, just install fucking chrony
mec_handlers[0x19] = function (data)
-- CT
local data = string.byte(data, 2)
dsn_helper(255, function ()
RDS.ct = data ~= 0
end)
return 2
end
mec_handlers[0x16] = function (data)
-- Group sequence for stream 0
local dsn = string.byte(data, 2)
local mel = string.byte(data, 3)
local group_sequence = string.sub(data, 4, 3+mel)
dsn_helper(dsn, function ()
RDS.set_grpseq(group_sequence)
end)
return 4 + mel
end
mec_handlers[0x23] = function (data)
-- Site address
local control_bits = string.byte(data, 2)
local high = string.byte(data, 3)
local low = string.byte(data, 4)
local data = high << 8 | low
if control_bits == 0 then
for i, v in ipairs(uecp.site_addreses) do
if v == data then table.remove(uecp.site_addreses, i) end
end
elseif control_bits == 1 then uecp.site_addreses[#uecp.site_addreses+1] = data
elseif control_bits == 2 then uecp.site_addreses = {} end
return 4
end
mec_handlers[0x27] = function (data)
-- Encoder address
local control_bits = string.byte(data, 2)
local data = string.byte(data, 3)
if control_bits == 0 then
for i, v in ipairs(uecp.encoder_addreses) do
if v == data then table.remove(uecp.encoder_addreses, i) end
end
elseif control_bits == 1 then uecp.encoder_addreses[#uecp.encoder_addreses+1] = data
elseif control_bits == 2 then uecp.encoder_addreses = {} end
return 3
end
mec_handlers[0x1C] = function (data)
-- DS select
local dsn = string.byte(data, 2)
Data.set_output_program(dsn)
Data.set_writing_program(dsn)
return 2
end
mec_handlers[0x2D] = function (data)
-- Manufacturer
local mel = string.byte(data, 2)
local designation = string.sub(data, 3, 4)
if string.byte(designation, 1) == 0x39 and string.byte(designation, 2) == 0x35 and mel > 2 then
pcall(hooks.parse_ascii, string.sub(data, 5, 4+mel))
end
return 2+mel
end
---@param data string
local function undo_byte_stuff(data)
local output = {}
local i = 1
while i <= #data do
local d0 = string.byte(data, i)
if d0 == 0xFD then
local d1 = string.byte(data, i + 1)
if not d1 then error("truncated escape sequence") end
output[#output + 1] = string.char(0xFD + d1)
i = i + 2
else
output[#output + 1] = string.char(d0)
i = i + 1
end
end
return table.concat(output)
end
---@param site integer
---@param encoder integer
---@return boolean
local function check_address(site, encoder)
if site == 0 and encoder == 0 then return true end
local site_allowed = false
if site ~= 0 then
for i, v in ipairs(uecp.site_addreses) do
if v == site then
site_allowed = true
break
end
end
else site_allowed = true end
local encoder_allowed = false
if encoder ~= 0 then
for i, v in ipairs(uecp.encoder_addreses) do
if v == encoder then
encoder_allowed = true
break
end
end
else encoder_allowed = true end
return site_allowed and encoder_allowed
end
---@param packet string
function uecp.parse_uecp(packet)
if not packet or #packet == 0 then return end
if string.byte(packet, 1) ~= 0xfe or string.byte(packet, #packet) ~= 0xff then return end
local unstuffed = undo_byte_stuff(string.sub(packet, 2, #packet - 1))
local addr1 = string.byte(unstuffed, 1)
local addr2 = string.byte(unstuffed, 2)
local site_addr = (addr1 << 2) | (addr2 >> 6)
if not check_address(site_addr, addr2 & 0x3F) then return end
local sequence_count = string.byte(unstuffed, 3)
local mfl = string.byte(unstuffed, 4)
local data = string.sub(unstuffed, 5, 4 + mfl)
if mfl ~= #data then
print("data len not right")
return
end
local crc_calculated = Data.crc16(string.sub(unstuffed, 1, 4 + mfl))
local crc_hi = string.byte(unstuffed, 4 + mfl + 1)
local crc_stored = (crc_hi << 8) | string.byte(unstuffed, 4 + mfl + 2)
if crc_calculated ~= crc_stored then
print("bad crc")
return
end
local consumed = 1
while consumed <= #data do
local mec = string.byte(data, consumed)
local handler = mec_handlers[mec]
if not handler then
print(string.format("unknown MEC 0x%02X, cannot continue", mec))
break
end
local advance = handler(string.sub(data, consumed))
Data.set_writing_program(Data.get_output_program())
consumed = consumed + advance
end
end
return uecp