---@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