-
-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extract core state machine to lua module (#362)
- Loading branch information
1 parent
fd87614
commit 989589d
Showing
12 changed files
with
442 additions
and
293 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.