Skip to content

Commit

Permalink
Extract core state machine to lua module (#362)
Browse files Browse the repository at this point in the history
  • Loading branch information
charliefoxtwo authored Oct 9, 2023
1 parent fd87614 commit 989589d
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 293 deletions.
52 changes: 31 additions & 21 deletions Scripts/DCS-BIOS/BIOS.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ package.path = lfs.writedir() .. [[Scripts/DCS-BIOS/lib/modules/documentation/?.
package.path = lfs.writedir() .. [[Scripts/DCS-BIOS/lib/modules/memory_map/?.lua;]] .. package.path

-- all requires must come after updates to package.path
local ProtocolIO = require("ProtocolIO")

local BIOSConfig = require("BIOSConfig")
local BIOSStateMachine = require("BIOSStateMachine")
local ConnectionManager= require("ConnectionManager")
local TCPServer = require("TCPServer")
local UDPServer = require("UDPServer")
local socket = require("socket") --[[@as Socket]]

local json = loadfile([[Scripts/JSON.lua]]) -- try to load json from dcs
BIOS.json = json and json() or require "JSON" -- if that fails, fall back to module that we can define
Expand Down Expand Up @@ -175,8 +179,6 @@ BIOS.protocol.writeNewModule(VNAO_T_45)
local Yak_52 = require "Yak-52"
BIOS.protocol.writeNewModule(Yak_52)
----------------------------------------------------------------------------Modules End--------------------------------------
dofile(lfs.writedir()..[[Scripts/DCS-BIOS/BIOSConfig.lua]])

--Saves aliases for each aircraft for external programs
local function saveAliases()
local JSON = BIOS.json
Expand All @@ -188,6 +190,8 @@ local function saveAliases()
end
end
pcall(saveAliases)
-- save constants for arduino devs to a header file
pcall(BIOS.protocol.saveAddresses)

-- Prev Export functions.
local PrevExport = {}
Expand All @@ -196,36 +200,44 @@ PrevExport.LuaExportStop = LuaExportStop
PrevExport.LuaExportBeforeNextFrame = LuaExportBeforeNextFrame
PrevExport.LuaExportAfterNextFrame = LuaExportAfterNextFrame

local connection_manager = ConnectionManager:new({})

local state_machine = BIOSStateMachine:new(BIOS.dbg.aircraftNameToModules, MetadataStart, MetadataEnd, 11000, connection_manager)

local function process_input_line(line)
state_machine:processInputLine(line)
end

for _, udp in ipairs(BIOSConfig.udp_config) do
connection_manager:addConnection(UDPServer:new(udp.send_address, udp.send_port, udp.receive_address, udp.receive_port, socket, process_input_line))
end

for _, tcp in ipairs(BIOSConfig.tcp_config) do
connection_manager:addConnection(TCPServer:new(tcp.address, tcp.port, socket, process_input_line))
end

-- Lua Export Functions
LuaExportStart = function()

for _, v in pairs(ProtocolIO.connections) do v:init() end
BIOS.protocol.init()

state_machine:init()

-- Chain previously-included export as necessary
if PrevExport.LuaExportStart then
PrevExport.LuaExportStart()
end
end

LuaExportStop = function()

BIOS.protocol.shutdown()
ProtocolIO.flush()
for _, v in pairs(ProtocolIO.connections) do v:close() end

state_machine:shutdown()

-- Chain previously-included export as necessary
if PrevExport.LuaExportStop then
PrevExport.LuaExportStop()
end
end

function LuaExportBeforeNextFrame()

for _, v in pairs(ProtocolIO.connections) do
if v.step then v:step() end
end

state_machine:receive()

-- Chain previously-included export as necessary
if PrevExport.LuaExportBeforeNextFrame then
PrevExport.LuaExportBeforeNextFrame()
Expand All @@ -234,9 +246,7 @@ function LuaExportBeforeNextFrame()
end

function LuaExportAfterNextFrame()

BIOS.protocol.step()
ProtocolIO.flush()
state_machine:step()

-- Chain previously-included export as necessary
if PrevExport.LuaExportAfterNextFrame then
Expand Down
50 changes: 33 additions & 17 deletions Scripts/DCS-BIOS/BIOSConfig.lua
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
local ProtocolIO = require("ProtocolIO")
local TCPServer = require("TCPServer")
local UDPServer = require("UDPServer")
local socket = require("socket") --[[@as Socket]]

local udp_send_address = "239.255.50.10"
local udp_send_port = 5010
local udp_receive_address = "*"
local udp_receive_port = 7778

local tcp_address = "*"
local tcp_port = 7778

ProtocolIO.connections = {
UDPServer:new(udp_send_address, udp_send_port, udp_receive_address, udp_receive_port, socket, BIOS.protocol.processInputLine),
TCPServer:new(tcp_address, tcp_port, socket, BIOS.protocol.processInputLine),
}
module("BIOSConfig", package.seeall)

--- @class TCPConnectionConfig
--- @field address string
--- @field port integer

--- @class UDPConnectionConfig
--- @field send_address string
--- @field send_port integer
--- @field receive_address string
--- @field receive_port integer

--- @class BIOSConfig
--- @field tcp_config TCPConnectionConfig[]
--- @field udp_config UDPConnectionConfig[]
local BIOSConfig = {
tcp_config = {
{
address = "*",
port = 7778
},
},
udp_config = {
{
send_address = "239.255.50.10",
send_port = 5010,
receive_address = "*",
receive_port = 7778
},
},
}

return BIOSConfig
174 changes: 174 additions & 0 deletions Scripts/DCS-BIOS/lib/BIOSStateMachine.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
local Log = require "Log"
module("BIOSStateMachine", package.seeall)

--- @class BIOSStateMachine
--- @field private modules_by_name {[string]: Module[]} a map of module names to the modules to send data for
--- @field private metadata_start MetadataStart the MetadataStart module
--- @field private metadata_end MetadataEnd the MetadataEnd module
--- @field private max_bytes_per_second integer the maximum amount of data per second to send
--- @field private connection_manager ConnectionManager a connection manager with all active connections
--- @field private active_aircraft_name string? the name of the current aircraft
--- @field private bytes_in_transit integer the number of bytes currently being sent over the wire
--- @field private active_modules Module[] modules which are currently being exported (i.e. modules associated with the active aircraft)
--- @field private update_counter integer a frame counter which ticks with every frame
--- @field private update_skip_counter integer a counter which increments for every skipped frame due to too much data being sent
--- @field private next_step_time number the time at which the next frame tick should occur
--- @field private last_frame_time number the time at which the last frame tick occurred
local BIOSStateMachine = {}

--- Constructs a new BIOS state machine
--- @param modules_by_name {[string]: Module[]} a map of module names to the modules to send data for
--- @param metadata_start MetadataStart the MetadataStart module
--- @param metadata_end MetadataEnd the MetadataEnd module
--- @param max_bytes_per_second integer the maximum amount of data per second to send
--- @param connection_manager ConnectionManager a connection manager with all active connections
--- @return BIOSStateMachine
function BIOSStateMachine:new(modules_by_name, metadata_start, metadata_end, max_bytes_per_second, connection_manager)
--- @type BIOSStateMachine
local o = {
modules_by_name = modules_by_name,
metadata_start = metadata_start,
metadata_end = metadata_end,
max_bytes_per_second = max_bytes_per_second,
connection_manager = connection_manager,
bytes_in_transit = 0,
active_modules = {},
update_counter = 0,
update_skip_counter = 0,
next_step_time = 0,
last_frame_time = LoGetModelTime(),
}
setmetatable(o, self)
self.__index = self
return o
end

function BIOSStateMachine:processInputLine(line)
local cmd, args = line:match("^([^ ]+) (.*)")

if cmd then
for _, module in ipairs(self.active_modules) do
local processor = module.inputProcessors[cmd]
if processor then
processor(args)
end
end
end
end

--- @private
--- @param module Module
--- @param dev0 CockpitDevice
function BIOSStateMachine:queue_module_data(module, dev0)
for _, hook in ipairs(module.exportHooks) do
hook(dev0)
end
-- legacy behavior - for some reason, we seem to typically call this twice. Is this because modules are getting too big?
module.memoryMap:autosyncStep()
module.memoryMap:autosyncStep()
local data = module.memoryMap:flushData()
self.bytes_in_transit = self.bytes_in_transit + data:len()
self.connection_manager:queue(data)
end

function BIOSStateMachine:init()
for _, connection in ipairs(self.connection_manager.connections) do
connection:init()
end
end

function BIOSStateMachine:receive()
for _, connection in ipairs(self.connection_manager.connections) do
if connection.step then
connection:step()
end
end
end

local frame_sync_sequence = string.char(0x55, 0x55, 0x55, 0x55)

function BIOSStateMachine:step()
-- rate limiting
local curTime = LoGetModelTime()
self.bytes_in_transit = self.bytes_in_transit - ((curTime - self.last_frame_time) * self.max_bytes_per_second)
self.last_frame_time = curTime
if self.bytes_in_transit < 0 then self.bytes_in_transit = 0 end

-- determine active aircraft
local self_data = LoGetSelfData()
local current_aircraft_name = self_data and self_data["Name"] or "NONE"

self.metadata_start:setAircraftName(current_aircraft_name)

self.active_modules = self.modules_by_name[current_aircraft_name] or {}
if self.active_aircraft_name ~= current_aircraft_name then
for _, acftModule in ipairs(self.active_modules) do
acftModule.memoryMap:clearValues()
end
self.active_aircraft_name = current_aircraft_name
end

-- export data
if curTime < self.next_step_time then
return -- runs 30 times per second
end

self.update_counter = (self.update_counter + 1) % 256
self.metadata_end:setUpdateCounter(self.update_counter)

-- if the last frame update has not been completely transmitted, skip a frame
if self.bytes_in_transit > 0 then
-- TODO: increase a frame skip counter for logging purposes
self.update_skip_counter = (self.update_skip_counter + 1) % 256
return
end
self.metadata_end:setUpdateSkipCounter(self.update_skip_counter)
self.next_step_time = curTime + .033

-- send frame sync sequence
self.bytes_in_transit = self.bytes_in_transit + 4
self.connection_manager:queue(frame_sync_sequence)

local dev0 = GetDevice(0)
if dev0 and type(dev0) ~= "number" then -- this type check is legacy code - unclear if this is still possible
dev0:update_arguments()
end

-- export aircraft-independent data
self:queue_module_data(self.metadata_start, dev0)

-- Export aircraft data
for _, module in ipairs(self.active_modules) do
self:queue_module_data(module, dev0)
end

self:queue_module_data(self.metadata_end, dev0)

self.connection_manager:send_queue()
end

function BIOSStateMachine:shutdown()
local dev0 = GetDevice(0)

-- Nullify the aircraft name and publish one last frame to identify end of mission.
self.metadata_start:setAircraftName("")

-- send frame sync sequence
self.connection_manager:queue(frame_sync_sequence)

-- export aircraft-independent data: MetadataStart
self:queue_module_data(self.metadata_start, dev0)

-- export aircraft-independent data: MetadataEnd
self:queue_module_data(self.metadata_end, dev0)

self.connection_manager:send_queue()

-- close any open connections
for _, connection in ipairs(self.connection_manager.connections) do
connection:close()
end
end


return BIOSStateMachine
68 changes: 68 additions & 0 deletions Scripts/DCS-BIOS/lib/ConnectionManager.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module("ConnectionManager", package.seeall)

--- @class ConnectionManager
--- @field connections Server[] the connections to send messages to
--- @field private msg_buf string[] the buffer of messages to send
--- @field private MAX_PAYLOAD_SIZE integer the maximum payload that can be accepted and sent
local ConnectionManager = {
}

--- Constructs a new connection handler
--- @param connections Server[] the connections to send messages to
--- @return ConnectionManager
function ConnectionManager:new(connections)
--- @type ConnectionManager
local o = {
connections = connections,
msg_buf = {},
MAX_PAYLOAD_SIZE = 2048
}
setmetatable(o, self)
self.__index = self
return o
end

--- Adds a new connection
--- @param server Server
function ConnectionManager:addConnection(server)
table.insert(self.connections, server)
end

--- Queues a message to be sent to any connections
---@param msg string the message to send
function ConnectionManager:queue(msg)
if (msg:len() > self.MAX_PAYLOAD_SIZE) then
error("Message exceeded max buffer size! " + msg)
end

table.insert(self.msg_buf, msg)
end

--- Flushes the message buffer, sending any queued messages
function ConnectionManager:send_queue()
local packet = ""
while #self.msg_buf > 0 do
local msg = table.remove(self.msg_buf, 1)
if packet:len() + msg:len() > self.MAX_PAYLOAD_SIZE then
-- packet would be too big, so send what we have now
self:send_packet(packet)
packet = ""
end
packet = packet .. msg
end

if packet:len() > 0 then
self:send_packet(packet)
end
end

--- @private
--- Sends a packet to all open connections
--- @param packet string
function ConnectionManager:send_packet(packet)
for _, conn in ipairs(self.connections) do
if conn.send then conn:send(packet) end
end
end

return ConnectionManager
Loading

0 comments on commit 989589d

Please sign in to comment.