From 5bad2b82009a2d7d9b4636417d431b4a98c41159 Mon Sep 17 00:00:00 2001 From: KubaPro010 Date: Sat, 11 Jul 2026 13:37:41 +0200 Subject: [PATCH] ipc thing, and some more stuff i guess --- .luarc.json | 2 +- CMakeLists.txt | 29 ++++++++- context.lua | 87 +++---------------------- scripts/2-af.lua => modules/af.lua | 22 ++++--- scripts/2-ert.lua => modules/ert.lua | 28 ++++---- scripts/1-oda.lua => modules/oda.lua | 16 +++-- scripts/2-rtp.lua => modules/rtp.lua | 48 ++++++++------ scripts/98-uecp.lua => modules/uecp.lua | 65 ++++++++++++++---- readme.md | 6 -- scripts/{99-data.lua => main.lua} | 29 +++++---- src/ipc_client.c | 69 ++++++++++++++++++++ src/ipc_client.h | 11 ++++ src/lua_rds.c | 8 +-- src/modulator.c | 51 --------------- src/modulator.h | 25 ------- src/rds.c | 5 +- src/rds95.c | 34 +++------- 17 files changed, 271 insertions(+), 264 deletions(-) rename scripts/2-af.lua => modules/af.lua (91%) rename scripts/2-ert.lua => modules/ert.lua (86%) rename scripts/1-oda.lua => modules/oda.lua (94%) rename scripts/2-rtp.lua => modules/rtp.lua (75%) rename scripts/98-uecp.lua => modules/uecp.lua (87%) rename scripts/{99-data.lua => main.lua} (95%) create mode 100644 src/ipc_client.c create mode 100644 src/ipc_client.h delete mode 100644 src/modulator.c delete mode 100644 src/modulator.h diff --git a/.luarc.json b/.luarc.json index 148e2a7..bcdc915 100644 --- a/.luarc.json +++ b/.luarc.json @@ -3,7 +3,7 @@ "debug": "disable", "os": "disable", "jit": "disable", - "ffi":"disable", + "ffi":"disable" }, "workspace.library": ["context.lua"], "diagnostics.disable": [ diff --git a/CMakeLists.txt b/CMakeLists.txt index 451d165..f964127 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ add_executable(rds95 ${SOURCES}) find_package(Lua REQUIRED) target_include_directories(rds95 PRIVATE ${LUA_INCLUDE_DIR}) -target_link_libraries(rds95 PRIVATE m pthread pulse pulse-simple inih ${LUA_LIBRARIES}) +target_link_libraries(rds95 PRIVATE m pthread inih ${LUA_LIBRARIES}) install(TARGETS rds95 DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) @@ -23,7 +23,9 @@ install(CODE # Define the paths for the source and destination files set(PREFIX_FILE \"${CMAKE_CURRENT_SOURCE_DIR}/.script.lua\") set(SCRIPTS_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/scripts\") + set(MODULES_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/modules\") set(DEST_FILE \"/etc/rds95.lua\") + set(DEST_MODULES_DIR \"/etc/rds95\") # Initialize content variable set(FINAL_CONTENT \"\") @@ -62,13 +64,36 @@ install(CODE # Write the resulting content to the destination file message(STATUS \"Installing script file to \${DEST_FILE}\") file(WRITE \${DEST_FILE} \"\${FINAL_CONTENT}\") + + if(EXISTS \${MODULES_DIR} AND IS_DIRECTORY \${MODULES_DIR}) + file(MAKE_DIRECTORY \${DEST_MODULES_DIR}) + file(GLOB LUA_MODULES \"\${MODULES_DIR}/*.lua\") + list(LENGTH LUA_MODULES MODULE_COUNT) + + if(MODULE_COUNT GREATER 0) + message(STATUS \"Installing \${MODULE_COUNT} Lua module(s) to \${DEST_MODULES_DIR}.\") + foreach(LUA_MODULE \${LUA_MODULES}) + get_filename_component(MODULE_NAME \${LUA_MODULE} NAME) + file(COPY \${LUA_MODULE} DESTINATION \${DEST_MODULES_DIR}) + message(STATUS \"Installed module: \${MODULE_NAME}\") + endforeach() + else() + message(STATUS \"No Lua modules found in modules directory.\") + endif() + else() + message(STATUS \"Modules directory not found.\") + endif() # Change ownership to the user who invoked sudo (if applicable) if(DEFINED ENV{SUDO_USER}) message(STATUS \"Changing ownership of \${DEST_FILE} to \$ENV{SUDO_USER}\") execute_process(COMMAND chown \$ENV{SUDO_USER}:\$ENV{SUDO_USER} \${DEST_FILE}) + if(EXISTS \${DEST_MODULES_DIR}) + message(STATUS \"Changing ownership of \${DEST_MODULES_DIR} to \$ENV{SUDO_USER}\") + execute_process(COMMAND chown -R \$ENV{SUDO_USER}:\$ENV{SUDO_USER} \${DEST_MODULES_DIR}) + endif() else() message(STATUS \"No SUDO_USER detected, skipping chown\") endif() " -) \ No newline at end of file +) diff --git a/context.lua b/context.lua index b08ecfe..03b45cb 100644 --- a/context.lua +++ b/context.lua @@ -63,7 +63,7 @@ hooks.on_start = {} ---@type function[] hooks.on_state = {} ----This function is called every second (or rather if the second has changed since the last stream 0 group) +---This function is called every second ---It should be defined by the user in the script. ---This is a table of functions. Each will be called ---@type function[] @@ -81,12 +81,18 @@ hooks.minute_tick = {} ---@type function[] hooks.rt_transmission = {} ----This function is calld every time a PS transmits start to end +---This function is called every time a PS transmits start to end ---It should be defined by the user in the script. ---This is a table of functions. Each will be called ---@type function[] hooks.ps_transmission = {} +---This function is called before the next group is determined +---It should be defined by the user in the script. +---This is a table of functions. Each will be called +---@type function[] +hooks.pre_tx = {} + ---This function is called in order to handle UDP data. The string returned is sent back to the UDP peer as a response ---It should be defined by the user in the script. ---@param data string @@ -272,79 +278,4 @@ function userdata.get() end ---@param offset integer ---@param size integer ---@return string -function userdata.get_offset(offset, size) end - ----@class ext -ext = {} ----@class RDSext -RDS.ext = {} - --- RT Plus Tags ----Sets RT+ tags: type1, start1, len1, type2, start2, len2 ----@param ertp boolean ----@param t1 integer ----@param s1 integer ----@param l1 integer ----@param t2 integer ----@param s2 integer ----@param l2 integer -function RDS.ext.set_rtplus_tags(ertp, t1, s1, l1, t2, s2, l2) end - ----Gets RT+ tags: type1, start1, len1, type2, start2, len2 ----@param ertp boolean ----@return integer type1, integer start1, integer len1, integer type2, integer start2, integer len2 -function RDS.ext.get_rtplus_tags(ertp) end - ----Toggles RTP or ERTP's toggle switch ----@param ertp boolean -function RDS.ext.toggle_rtp(ertp) end - ----Sets the metadata of RTP or ERTP ----@param ertp boolean ----@param running boolean -function RDS.ext.set_rtp_meta(ertp, running) end ----Gets the metadata of RTP and ERTP ----@param ertp boolean ----@return boolean running -function RDS.ext.get_rtp_meta(ertp) end - ----Sets the AFs included in the ODA ----@param afs table -function RDS.ext.set_af_oda(afs) end - ----Registers an ODA to be used in the 0x6 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 Send AID once (no data) and destroy ----@return integer oda_id -function ext.register_oda(group, group_version, aid, data, temp) end - ----Unregisters an ODA, this stops the handler or AID being called/sent ----@param oda_id integer -function ext.unregister_oda(oda_id) end - ----Sets the data for a existing ODA group ----@param oda_id integer ----@param data integer -function ext.set_oda_id_data(oda_id, data) end - ----The callback function for an ODA handler ----@alias ODAHandler fun(): (boolean, integer, integer, integer) - ----Sets a function to handle the ODA data generation. ----The handler is called when the group sequence '0xff' 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 B 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 inside block C) ----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 ext.set_oda_handler(oda_id, fun) end - ----@param ert string -function RDS.ext.set_ert(ert) end ----@return string -function RDS.ext.get_ert() end \ No newline at end of file +function userdata.get_offset(offset, size) end \ No newline at end of file diff --git a/scripts/2-af.lua b/modules/af.lua similarity index 91% rename from scripts/2-af.lua rename to modules/af.lua index 0f3d025..ffa35ea 100644 --- a/scripts/2-af.lua +++ b/modules/af.lua @@ -1,7 +1,11 @@ -_Af_Oda_id = nil -_Af_Oda_state = 0 -_Af_Oda_len = 0 -_Af_Oda_afs = {} +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 @@ -55,8 +59,8 @@ end local function init_af_oda() if _Af_Oda_id == nil then - _Af_Oda_id = ext.register_oda(7, false, 0x6365, 0, false) - ext.set_oda_handler(_Af_Oda_id, function() + _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) @@ -106,7 +110,7 @@ end ---Sets the AFs included in the ODA and saves them ---@param afs table List of numbers (e.g., {98.1, 102.5}) -function RDS.ext.set_af_oda(afs) +function af.set_af_oda(afs) _process_af_list(afs) save_af_to_userdata(afs) end @@ -114,4 +118,6 @@ end table.insert(hooks.on_state, function () load_af_from_userdata() if _Af_Oda_len ~= 0 then init_af_oda() end -end) \ No newline at end of file +end) + +return af diff --git a/scripts/2-ert.lua b/modules/ert.lua similarity index 86% rename from scripts/2-ert.lua rename to modules/ert.lua index 0813995..fee356b 100644 --- a/scripts/2-ert.lua +++ b/modules/ert.lua @@ -1,13 +1,17 @@ -_Ert_state = 0 -_Ert_oda_id = nil +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 = ext.register_oda(13, false, 0x6552, 1, false) - ext.set_oda_handler(_Ert_oda_id, function () + _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)) @@ -34,22 +38,22 @@ local function init_ert() end end -function unregister_ert() +function ert.unregister_ert() if _Ert_oda_id ~= nil then - ext.unregister_oda(_Ert_oda_id) + oda.unregister_oda(_Ert_oda_id) _Ert_oda_id = nil end end -function RDS.ext.set_ert(ert) - if #ert == 0 then +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 = ert .. "\r" + local data = text .. "\r" data = string.sub(data, 1, 128) local padding = (4 - (#data % 4)) % 4 @@ -72,7 +76,7 @@ function RDS.ext.set_ert(ert) if _Ert_oda_id == nil then init_ert() end end -function RDS.ext.get_ert() +function ert.get_ert() local segments = string.byte(userdata.get_offset(USERDATA_ERT_OFFSET+128, 1)) if segments == 0 then return "" end @@ -82,4 +86,6 @@ end table.insert(hooks.on_state, function () if string.byte(userdata.get_offset(USERDATA_ERT_OFFSET+257, 1)) ~= 0 then init_ert() end -end) \ No newline at end of file +end) + +return ert diff --git a/scripts/1-oda.lua b/modules/oda.lua similarity index 94% rename from scripts/1-oda.lua rename to modules/oda.lua index 405f53a..8969e8f 100644 --- a/scripts/1-oda.lua +++ b/modules/oda.lua @@ -1,3 +1,5 @@ +---@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) @@ -18,7 +20,7 @@ local _RDS_ODA_pointer = 1 ---@param data integer ---@param temp boolean ---@return integer oda_id -function ext.register_oda(group, group_version, aid, data, temp) +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) @@ -34,7 +36,7 @@ end ---Unregisters an ODA, this stops the handler or AID being called/sent ---@param oda_id integer -function ext.unregister_oda(oda_id) +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 @@ -46,11 +48,13 @@ end ---Sets the id_data for a existing ODA group ---@param oda_id integer ---@param data integer -function ext.set_oda_id_data(oda_id, data) +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. @@ -58,7 +62,7 @@ end ---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 ext.set_oda_handler(oda_id, fun) +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 @@ -124,4 +128,6 @@ require("group").register_group("\x06\xff", group_handler) table.insert(hooks.on_state, function () _RDS_ODAs = {} _RDS_ODA_pointer = 1 -end) \ No newline at end of file +end) + +return oda diff --git a/scripts/2-rtp.lua b/modules/rtp.lua similarity index 75% rename from scripts/2-rtp.lua rename to modules/rtp.lua index 0156a1c..622b5ce 100644 --- a/scripts/2-rtp.lua +++ b/modules/rtp.lua @@ -1,15 +1,19 @@ -_Rtp_oda_id = nil -_Ertp_oda_id = nil -_Rtp_toggle = false -_Ertp_toggle = false +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 = ext.register_oda(11, false, 0x4BD7, 0, false) - ext.set_oda_handler(_Rtp_oda_id, function () + _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) @@ -31,8 +35,8 @@ end local function init_ertp() if _Ertp_oda_id == nil then - _Ertp_oda_id = ext.register_oda(12, false, 0x4BD8, 0, false) - ext.set_oda_handler(_Ertp_oda_id, function () + _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) @@ -52,7 +56,7 @@ local function init_ertp() end end -function RDS.ext.set_rtp_meta(ertp, running) +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)) @@ -61,35 +65,37 @@ function RDS.ext.set_rtp_meta(ertp, running) userdata.set_offset(USERDATA_RTP_OFFSET, 1, string.char(running and 1 or 0)) end end -function RDS.ext.get_rtp_meta(ertp) +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 RDS.ext.toggle_rtp(ertp) +function rtp.toggle_rtp(ertp) if ertp then _Ertp_toggle = not _Ertp_toggle else _Rtp_toggle = not _Rtp_toggle end end -function RDS.ext.set_rtplus_tags(ertp, t1, s1, l1, t2, s2, l2) - RDS.ext.set_rtp_meta(ertp, true) - RDS.ext.toggle_rtp(ertp) +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 RDS.ext.get_rtplus_tags(ertp) +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 unregister_rtp(ertp) +function rtp.unregister_rtp(ertp) if ertp and _Ertp_oda_id ~= nil then - ext.unregister_oda(_Ertp_oda_id) + oda.unregister_oda(_Ertp_oda_id) _Ertp_oda_id = nil elseif _Rtp_oda_id ~= nil then - ext.unregister_oda(_Rtp_oda_id) + oda.unregister_oda(_Rtp_oda_id) _Rtp_oda_id = nil end end table.insert(hooks.on_state, function () - if RDS.ext.get_rtp_meta(false) then init_rtp() end - if RDS.ext.get_rtp_meta(true) then init_ertp() end -end) \ No newline at end of file + if rtp.get_rtp_meta(false) then init_rtp() end + if rtp.get_rtp_meta(true) then init_ertp() end +end) + +return rtp diff --git a/scripts/98-uecp.lua b/modules/uecp.lua similarity index 87% rename from scripts/98-uecp.lua rename to modules/uecp.lua index ad89ba7..c60d6ec 100644 --- a/scripts/98-uecp.lua +++ b/modules/uecp.lua @@ -1,13 +1,15 @@ -uecp = {} +---@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 @@ -23,7 +25,8 @@ hooks.rt_transmission[#hooks.rt_transmission + 1] = function () 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 @@ -47,7 +50,7 @@ local function dsn_helper(dsn, write) end local mec_handlers = {} -mec_handlers[1] = function(data) +mec_handlers[0x01] = function(data) -- PI local dsn = string.byte(data, 2) local psn = string.byte(data, 3) @@ -59,7 +62,7 @@ mec_handlers[1] = function(data) end) return 5 end -mec_handlers[2] = function(data) +mec_handlers[0x02] = function(data) -- PS local dsn = string.byte(data, 2) local psn = string.byte(data, 3) @@ -84,7 +87,7 @@ mec_handlers[0x21] = function(data) end) return 4 + mel end -mec_handlers[4] = function(data) +mec_handlers[0x04] = function(data) -- PTYI local dsn = string.byte(data, 2) local psn = string.byte(data, 3) @@ -94,7 +97,7 @@ mec_handlers[4] = function(data) end) return 4 end -mec_handlers[3] = function(data) +mec_handlers[0x03] = function(data) -- TP/TA local dsn = string.byte(data, 2) local psn = string.byte(data, 3) @@ -105,7 +108,7 @@ mec_handlers[3] = function(data) end) return 4 end -mec_handlers[7] = function(data) +mec_handlers[0x07] = function(data) -- PTY local dsn = string.byte(data, 2) local psn = string.byte(data, 3) @@ -218,21 +221,57 @@ 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 -- No clue + 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) - RDS.put_custom_group((group << 11) | blockb, (blockc_msb << 8) | blockc_lsb, (blockd_msb << 8) | blockd_lsb) + 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) @@ -385,7 +424,9 @@ function uecp.parse_uecp(packet) break end local advance = handler(string.sub(data, consumed)) - consumed = consumed + advance Data.set_writing_program(Data.get_output_program()) + consumed = consumed + advance end -end \ No newline at end of file +end + +return uecp diff --git a/readme.md b/readme.md index 38dc960..a807547 100644 --- a/readme.md +++ b/readme.md @@ -33,12 +33,6 @@ Yet another unique feature, the Lua engine allows you to control the encoder, al Every RDS2 subcarrier is in full control of Lua (except tunneling - core fills in PTY and the rest) -### RFT - -For a long time there was no RFT in RDS95, but on 28th December 2025 that changed when i implemented it in lua, just call the load function with the path and id 0, and see the your station's logo in the decoder - -And yes, it is implemented in only Lua and the ODA engine which is also in Lua - ## Disclaimer RDS95 is "based" on [Anthony96922](https://github.com/Anthony96922/)'s [MiniRDS](https://github.com/Anthony96922/MiniRDS) licensed under GPLv3 diff --git a/scripts/99-data.lua b/scripts/main.lua similarity index 95% rename from scripts/99-data.lua rename to scripts/main.lua index 2c035f7..080a57f 100644 --- a/scripts/99-data.lua +++ b/scripts/main.lua @@ -1,3 +1,8 @@ +local af = require("af") +local ert = require("ert") +local rtp = require("rtp") +local uecp = require("uecp") + ---@param data string function hooks.parse_ascii(data) if string.sub(data, 1, 4):lower() == "lua=" then @@ -59,21 +64,21 @@ function hooks.parse_ascii(data) elseif data == "rdsgen" then return string.format("RDSGEN=%s\r\n", string.format("%d", RDS.get_streams())) elseif data == "link" then return string.format("LINK=%s\r\n", string.format("%d", (RDS.get_link() and 1 or 0))) elseif data == "rtp" then - local t1, s1, l1, t2, s2, l2 = RDS.ext.get_rtplus_tags(false) + local t1, s1, l1, t2, s2, l2 = rtp.get_rtplus_tags(false) return string.format("RTP=%d,%d,%d,%d,%d,%d\r\n", t1, s1, l1, t2, s2, l2) elseif data == "ertp" then - local t1, s1, l1, t2, s2, l2 = RDS.ext.get_rtplus_tags(true) + local t1, s1, l1, t2, s2, l2 = rtp.get_rtplus_tags(true) return string.format("ERTP=%d,%d,%d,%d,%d,%d\r\n", t1, s1, l1, t2, s2, l2) elseif data == "rtprun" then - local running = RDS.ext.get_rtp_meta(false) + local running = rtp.get_rtp_meta(false) local f1 = 2 or (running and 1 or 0) return string.format("RTPRUN=%d\r\n", f1) elseif data == "ertprun" then - local running = RDS.ext.get_rtp_meta(true) + local running = rtp.get_rtp_meta(true) local f1 = 2 or (running and 1 or 0) return string.format("ERTPRUN=%d\r\n", f1) elseif data == "lps" then return string.format("LPS=%s\r\n", RDS.get_lps()) - elseif data == "ert" then return string.format("ERT=%s\r\n", RDS.ext.get_ert()) + elseif data == "ert" then return string.format("ERT=%s\r\n", ert.get_ert()) else local eon_cmd, eon_num = data:match("^eon(%d+)([a-z]+)$") if eon_cmd then @@ -232,7 +237,7 @@ function hooks.parse_ascii(data) RDS.set_lps(value) return "+\r\n" elseif cmd == "ert" then - RDS.ext.set_ert(value) + ert.set_ert(value) return "+\r\n" elseif cmd == "link" then local link = tonumber(value) @@ -252,7 +257,7 @@ function hooks.parse_ascii(data) local t1, s1, l1, t2, s2, l2 = value:match("(%d+),(%d+),(%d+),(%d+),(%d+),(%d+)") if not l2 then return "-\r\n" end - RDS.ext.set_rtplus_tags( + rtp.set_rtplus_tags( is_ertp, ---@diagnostic disable-next-line: param-type-mismatch tonumber(t1), tonumber(s1), tonumber(l1), tonumber(t2), tonumber(s2), tonumber(l2) @@ -282,8 +287,8 @@ function hooks.parse_ascii(data) local f2 = tonumber(f2_str) or 0 local running = (f1 & 1) ~= 0 - RDS.ext.set_rtp_meta(is_ertp, running) - if f2 ~= 0 then RDS.ext.toggle_rtp(is_ertp) end + rtp.set_rtp_meta(is_ertp, running) + if f2 ~= 0 then rtp.toggle_rtp(is_ertp) end return "+\r\n" elseif cmd == "af" then local af_table = {} @@ -307,7 +312,7 @@ function hooks.parse_ascii(data) local af_table = {} if value == "" or value == "0" then - RDS.ext.set_af_oda({}) + af.set_af_oda({}) return "+\r\n" end @@ -319,7 +324,7 @@ function hooks.parse_ascii(data) if #af_table > 25 then return "-\r\n" end - RDS.ext.set_af_oda(af_table) + af.set_af_oda(af_table) return "+\r\n" else return "?\r\n" end end @@ -328,4 +333,4 @@ end function hooks.data_handle(data) if uecp.parse_uecp and string.byte(data, 1) == 0xfe then return uecp.parse_uecp(data) else return hooks.parse_ascii(data) end -end \ No newline at end of file +end diff --git a/src/ipc_client.c b/src/ipc_client.c new file mode 100644 index 0000000..5fb9a20 --- /dev/null +++ b/src/ipc_client.c @@ -0,0 +1,69 @@ +#include "ipc_client.h" +#include +#include +#include +#include +#include + +int ipc_connect(IPC_Client *client, const char *socket_path) { + client->fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (client->fd < 0) { perror("fm95 ipc: socket"); return -1; } + + struct sockaddr_un addr = { .sun_family = AF_UNIX }; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (connect(client->fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("fm95 ipc: connect"); + close(client->fd); + client->fd = -1; + return -1; + } + memset(client->prev_output, 0, sizeof(client->prev_output)); + return 0; +} + +void ipc_close(IPC_Client *client) { + if (client->fd >= 0) close(client->fd); + client->fd = -1; +} + +// Pulls one group's worth of bits (BITS_PER_GROUP), differentially encodes, +// packs 8-to-a-byte, sends as opcode 112 with the stream index. +int ipc_send_bits(IPC_Client *client, RDSEncoder *enc, uint8_t stream) { + if (client->fd < 0) return -1; + + uint8_t raw_bits[BITS_PER_GROUP]; + get_rds_bits(enc, raw_bits, stream); + + uint8_t packed[(BITS_PER_GROUP + 7) / 8]; + memset(packed, 0, sizeof(packed)); + + uint8_t prev = client->prev_output[stream]; + for (int i = 0; i < BITS_PER_GROUP; i++) { + uint8_t cur = prev ^ raw_bits[i]; // the differential step, moved here + prev = cur; + if (cur) packed[i / 8] |= (0x80 >> (i % 8)); + } + client->prev_output[stream] = prev; + + uint8_t msg[2 + sizeof(packed)]; + msg[0] = 112; + msg[1] = stream; + memcpy(msg + 2, packed, sizeof(packed)); + + ssize_t sent = send(client->fd, msg, sizeof(msg), 0); + if (sent != (ssize_t)sizeof(msg)) { + fprintf(stderr, "fm95 ipc: short/failed send\n"); + return -1; + } + + uint8_t reply; + if (recv(client->fd, &reply, 1, 0) <= 0) { + fprintf(stderr, "fm95 ipc: no reply / disconnected\n"); + return -1; + } + if (reply == 2) fprintf(stderr, "fm95 reported bitring overrun on stream %d\n", stream); + else if (reply == 1) fprintf(stderr, "fm95 rejected message (unknown opcode?)\n"); + + return 0; +} \ No newline at end of file diff --git a/src/ipc_client.h b/src/ipc_client.h new file mode 100644 index 0000000..ae2953f --- /dev/null +++ b/src/ipc_client.h @@ -0,0 +1,11 @@ +#include +#include "rds.h" // for RDSEncoder, get_rds_bits, BITS_PER_GROUP + +typedef struct { + int fd; + uint8_t prev_output[4]; // per-stream differential state +} IPC_Client; + +int ipc_connect(IPC_Client *client, const char *socket_path); +void ipc_close(IPC_Client *client); +int ipc_send_bits(IPC_Client *client, RDSEncoder *enc, uint8_t stream); \ No newline at end of file diff --git a/src/lua_rds.c b/src/lua_rds.c index 2791043..cad6658 100644 --- a/src/lua_rds.c +++ b/src/lua_rds.c @@ -118,18 +118,14 @@ uint8_t init_lua(RDSEncoder* _enc) { lua_newtable(globalL); lua_setfield(globalL, -2, "ps_transmission"); lua_newtable(globalL); + lua_setfield(globalL, -2, "pre_tx"); + lua_newtable(globalL); lua_setfield(globalL, -2, "group"); lua_pushvalue(globalL, -1); hooks_ref = luaL_ref(globalL, LUA_REGISTRYINDEX); lua_setglobal(globalL, "hooks"); lua_newtable(globalL); - lua_setglobal(globalL, "ext"); - - lua_newtable(globalL); - - lua_newtable(globalL); - lua_setfield(globalL, -2, "ext"); lua_pushinteger(globalL, EONs); lua_setfield(globalL, -2, "eon_count"); diff --git a/src/modulator.c b/src/modulator.c deleted file mode 100644 index b265121..0000000 --- a/src/modulator.c +++ /dev/null @@ -1,51 +0,0 @@ -#include "modulator.h" -#include "fs.h" - -void init_rds_modulator(RDSModulator* rdsMod, RDSEncoder* enc, uint8_t num_streams) { - memset(rdsMod, 0, sizeof(RDSModulator)); - rdsMod->num_streams = num_streams; - - memset(enc, 0, sizeof(RDSEncoder)); - rdsMod->enc = enc; - - rdsMod->data = (RDSModulatorModulationData*)calloc(num_streams, sizeof(RDSModulatorModulationData)); - if (rdsMod->data == NULL) { - fprintf(stderr, "Error: Could not allocate memory for RDS modulation data\n"); - return; - } - - for (uint8_t i = 0; i < num_streams; i++) { - rdsMod->data[i].symbol_shift = i * M_PI / 2.0f; - rdsMod->data[i].bit_pos = BITS_PER_GROUP; - } -} - -void cleanup_rds_modulator(RDSModulator* rdsMod) { - if (rdsMod->data) { - free(rdsMod->data); - rdsMod->data = NULL; - } -} - -float get_rds_sample(RDSModulator* rdsMod, uint8_t stream) { - if(stream >= rdsMod->num_streams) return 0.0f; - - rdsMod->data[stream].phase += 1187.5 / RDS_SAMPLE_RATE; - - if (rdsMod->data[stream].phase >= 1.0f) { - rdsMod->data[stream].phase -= 1.0f; - if (rdsMod->data[stream].bit_pos == BITS_PER_GROUP) { - get_rds_bits(rdsMod->enc, rdsMod->data[stream].bit_buffer, stream); - rdsMod->data[stream].bit_pos = 0; - } - - rdsMod->data[stream].cur_bit = rdsMod->data[stream].bit_buffer[rdsMod->data[stream].bit_pos++]; - rdsMod->data[stream].prev_output = rdsMod->data[stream].cur_output; - rdsMod->data[stream].cur_output = rdsMod->data[stream].prev_output ^ rdsMod->data[stream].cur_bit; // Something with differential encoding? Instead of encoding the data, encode if the bit of data changes - } - - float sample = sinf(M_2PI * rdsMod->data[stream].phase + rdsMod->data[stream].symbol_shift); - if(rdsMod->data[stream].cur_output == 0) sample = -sample; // do bpsk, if you comment this part out, nothing will be decoded - - return sample; -} \ No newline at end of file diff --git a/src/modulator.h b/src/modulator.h deleted file mode 100644 index 6479d2f..0000000 --- a/src/modulator.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once -#include "common.h" -#include "lib.h" -#include "rds.h" - -typedef struct -{ - uint8_t bit_buffer[BITS_PER_GROUP]; - uint8_t bit_pos : 7; - uint8_t prev_output : 1; - uint8_t cur_output : 1; - uint8_t cur_bit : 1; - float symbol_shift; - float phase; -} RDSModulatorModulationData; - -typedef struct { - RDSModulatorModulationData *data; - RDSEncoder* enc; - uint8_t num_streams; -} RDSModulator; - -void init_rds_modulator(RDSModulator* rdsMod, RDSEncoder* enc, uint8_t num_streams); -void cleanup_rds_modulator(RDSModulator* rdsMod); -float get_rds_sample(RDSModulator* rdsMod, uint8_t stream); \ No newline at end of file diff --git a/src/rds.c b/src/rds.c index 6cb3dc4..e8900b6 100644 --- a/src/rds.c +++ b/src/rds.c @@ -1,6 +1,5 @@ #include "rds.h" #include "fs.h" -#include "modulator.h" #include "lib.h" #include "lua_rds.h" #include @@ -97,7 +96,7 @@ void get_rds_group(RDSEncoder* enc, RDSGroup *group, uint8_t stream) { time(&now); utc = gmtime(&now); - if(utc->tm_sec != enc->state[enc->program].last_second && stream == 0) { + if(utc->tm_sec != enc->state[enc->program].last_second) { enc->state[enc->program].last_second = utc->tm_sec; lua_call_tfunction("tick"); } @@ -112,6 +111,8 @@ void get_rds_group(RDSEncoder* enc, RDSGroup *group, uint8_t stream) { } } + lua_call_tfunction("pre_tx"); + uint8_t good_group = 0; uint8_t grp_sqc_idx = 0; char grp; diff --git a/src/rds95.c b/src/rds95.c index baf06eb..f13b60a 100644 --- a/src/rds95.c +++ b/src/rds95.c @@ -2,13 +2,11 @@ #include #include #include -#include -#include #include "../inih/ini.h" #include "rds.h" +#include "ipc_client.h" #include "fs.h" -#include "modulator.h" #include "udp_server.h" #include "tcp_server.h" #include "lib.h" @@ -49,8 +47,7 @@ static inline void show_help(char *name) { ); } -typedef struct -{ +typedef struct { uint16_t udp_port; uint16_t tcp_port; char rds_device_name[48]; @@ -201,28 +198,17 @@ int main(int argc, char **argv) { } if(config.asciig == 0) { - int pulse_error; + IPC_Client client; + if (ipc_connect(&client, "/etc/fm95/ctl.socket") < 0) goto exit; - float *rds_buffer = (float*)malloc(NUM_MPX_FRAMES * config.num_streams * sizeof(float)); - if (rds_buffer == NULL) { - fprintf(stderr, "Error: Could not allocate memory for RDS buffer\n"); - goto exit; + while (!stop_rds) { + for (uint8_t s = 0; s < config.num_streams; s++) { + ipc_send_bits(&client, &rdsEncoder, s); + } + msleep((int)(1000.0 * BITS_PER_GROUP / 1187.5)); } - while(!stop_rds) { - for (uint16_t i = 0; i < NUM_MPX_FRAMES * config.num_streams; i++) { - uint8_t stream = i % config.num_streams; - if((rdsEncoder.enabled_streams > stream ? 1 : 0) == 0) rds_buffer[i] = 0.0f; - rds_buffer[i] = get_rds_sample(&rdsModulator, stream); - } - - if (pa_simple_write(rds_device, rds_buffer, NUM_MPX_FRAMES * config.num_streams * sizeof(float), &pulse_error) != 0) { - fprintf(stderr, "Error: could not play audio. (%s : %d)\n", pa_strerror(pulse_error), pulse_error); - break; - } - } - - free(rds_buffer); + ipc_close(&client); } else { RDSGroup group; char output_buffer[1024];