Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract core state machine to lua module #362

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -169,8 +173,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 @@ -182,6 +184,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 @@ -190,36 +194,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 @@ -228,9 +240,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
Loading