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
+1 -1
View File
@@ -3,7 +3,7 @@
"debug": "disable",
"os": "disable",
"jit": "disable",
"ffi":"disable",
"ffi":"disable"
},
"workspace.library": ["context.lua"],
"diagnostics.disable": [
+26 -1
View File
@@ -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 \"\")
@@ -63,10 +65,33 @@ install(CODE
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()
+8 -77
View File
@@ -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
@@ -273,78 +279,3 @@ function userdata.get() end
---@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
+13 -7
View File
@@ -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
@@ -115,3 +119,5 @@ table.insert(hooks.on_state, function ()
load_af_from_userdata()
if _Af_Oda_len ~= 0 then init_af_oda() end
end)
return af
+16 -10
View File
@@ -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
@@ -83,3 +87,5 @@ 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
+10 -4
View File
@@ -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
@@ -125,3 +129,5 @@ table.insert(hooks.on_state, function ()
_RDS_ODAs = {}
_RDS_ODA_pointer = 1
end)
return oda
+26 -20
View File
@@ -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
if rtp.get_rtp_meta(false) then init_rtp() end
if rtp.get_rtp_meta(true) then init_ertp() end
end)
return rtp
+52 -11
View File
@@ -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
return uecp
-6
View File
@@ -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
+16 -11
View File
@@ -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
+69
View File
@@ -0,0 +1,69 @@
#include "ipc_client.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
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;
}
+11
View File
@@ -0,0 +1,11 @@
#include <stdint.h>
#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);
+2 -6
View File
@@ -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");
-51
View File
@@ -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;
}
-25
View File
@@ -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);
+3 -2
View File
@@ -1,6 +1,5 @@
#include "rds.h"
#include "fs.h"
#include "modulator.h"
#include "lib.h"
#include "lua_rds.h"
#include <time.h>
@@ -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;
+10 -24
View File
@@ -2,13 +2,11 @@
#include <signal.h>
#include <getopt.h>
#include <pthread.h>
#include <pulse/simple.h>
#include <pulse/error.h>
#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];