Files
caero-attitude-control/main.lua

1239 lines
41 KiB
Lua

Thrusters = {} -- Table of thruster tables; see docs.md####Thrusters for the details of the thruster table structure
ThrustDirections = {
Angular = {
Negative = {
x = "pitchdown",
y = "yawport",
z = "rollport"
},
Positive = {
x = "pitchup",
y = "yawstar",
z = "rollstar"
}
},
Lateral = {
Negative = {
x = "star",
y = "down",
z = "aft"
},
Positive = {
x = "port",
y = "up",
z = "fore"
}
}
}
SensorData = {}
ThrusterTypes = {
"thruster",
"solid_fuel_thruster",
"ion_thruster",
"vector_thruster",
"liquid_vector_thruster"
}
PropellerTypes = {
"gyroscopic_propeller_bearing",
"propeller",
"propeller_bearing"
}
TransmissionTypes = {
"analog_transmission",
"Create_RotationSpeedController"
}
Config = {}
Config.ConfigPath = "/caero-attitude-control/config/config.txt"
Config.Debug = false -- this controls debug prints
-- File serializatino and deserialization
-- taken from https://gist.github.com/yuhanz/6688d474a3c391daa6d6
function tableToString(table)
return "return"..serializeTable(table)
end
-- loadstring is deprecated, so modified to run without it
function stringToTable(str)
local f = {}
local chunk, err = load(str)
if chunk then
f = chunk()
else
print("Error loading config string: "..err)
end
return f
end
function serializeTable(val, name, skipnewlines, depth)
skipnewlines = skipnewlines or false
depth = depth or 0
local tmp = string.rep(" ", depth)
if name then
if not string.match(name, '^[a-zA-Z_][a-zA-Z0-9_]*$') then
name = string.gsub(name, "'", "\\'")
name = "['".. name .. "']"
end
tmp = tmp .. name .. " = "
end
if type(val) == "table" then
tmp = tmp .. "{" .. (not skipnewlines and "\n" or "")
for k, v in pairs(val) do
tmp = tmp .. serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "")
end
tmp = tmp .. string.rep(" ", depth) .. "}"
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
else
tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
end
return tmp
end
-- Monitor Configuration
Config.Monitors = {}
Config.Monitors.InstrumentPanelMonitor = nil
Config.Monitors.AutopilotControlMonitor = nil
local function identifyMonitor(monitorType)
-- Find all available monitors
local monitors = { peripheral.find("monitor") }
if not monitors or #monitors == 0 then
print("No monitors found on the network.")
return nil
end
print("Available monitors:")
for i, monitor in ipairs(monitors) do
if monitor then
local width, height = monitor.getSize()
print(i .. ". " .. peripheral.getName(monitor) .. " (" .. width .. "x" .. height .. ")")
end
end
print("\nPlease select the monitor you want to use for " .. monitorType .. " (enter number):")
-- Simple input handling for demonstration
local input = read()
local choice = tonumber(input)
if not choice or choice < 1 or choice > #monitors then
print("Invalid selection.")
return nil
end
local selectedMonitor = monitors[choice]
local selectedMonitorName = peripheral.getName(selectedMonitor)
print("Selected monitor: " .. selectedMonitorName)
-- Return monitor details as a table
return {
name = selectedMonitorName,
peripheral = selectedMonitor
}
end
-- Autopilot
Config.Autopilot = {
AutopilotEngaged = false,
AutoForeAft = false,
AutopilotDesiredAltitude = nil,
AutopilotDesiredSpeed = nil,
AutopilotDesiredHeading = nil
}
-- Correctional values for when some sensor values are offset
Config.SensorCorrection = {
Heading = 0 -- this should be +/- 180
}
Config.Throttles = nil
function ThrottleInit()
if Config.Throttles ~= nil then
return
end
Config.Throttles = {}
local sides = {
"top",
"bottom",
"left",
"right",
"front",
"back"
}
local function getRelayStates()
local states = {}
local sides = {
"top",
"bottom",
"left",
"right",
"front",
"back"
}
local names = peripheral.getNames()
for _, name in ipairs(names) do
if peripheral.getType(name) == "redstone_relay" then
local wrapper = peripheral.wrap(name)
for _, side in ipairs(sides) do
states[name] = states[name] or {}
states[name][side] = wrapper.getAnalogInput(side)
end
end
end
return states
end
local function identifyThrottle(throttleType)
print("Identifying "..throttleType.." throttle.")
print("\nPlease change the input signal for the throttle.")
local initialStates = getRelayStates()
os.pullEvent("redstone")
local currentStates = getRelayStates()
Config.Throttles[throttleType] = {}
for pname, currentState in pairs(currentStates) do
local initialState = initialStates[pname]
for k, v in pairs(currentState) do
if initialState[k] ~= v then
Config.Throttles[throttleType].name = pname
Config.Throttles[throttleType].side = k
return
end
end
end
end
identifyThrottle("fore")
identifyThrottle("aft")
identifyThrottle("down")
end
function PropellerInit()
local transmissions = {}
local propellers = {}
for _, v in ipairs(TransmissionTypes) do
local vlist = { peripheral.find(v) }
if vlist ~= nil and vlist[1] ~= nil then
table.insert(transmissions, vlist)
end
end
for _, v in ipairs(PropellerTypes) do
local vlist = peripheral.find(v)
if vlist ~= nil and vlist[1] ~= nil then
table.insert(propellers, vlist)
end
end
for pi, pv in ipairs(propellers) do
if Thrusters[peripheral.getName(pv)] == nil then
for _, tv in pairs(transmissions) do
if pv.getSourceId() == tv.getSelfId() then
if Thrusters[peripheral.getName(pv)] == nil then
Thrusters[peripheral.getName(pv)] = {
type = "rotator",
thruster = pv,
transmission = peripheral.wrap(tv),
--[[
NOTE:
affectVectors will have values depending on how the thruster's
thrust vector interacts with the vessel's attitude
angular:
possible directions for angular.yaw: "port"|"star"
possible directions for angular.roll: "port"|"star"
possible directions for angular.pitch: "up"|"down"
lateral:
possible directions for lateral.x: "port"|"star"
possible directions for lateral.y: "up"|"down"
possible directions for lateral.z: "fore"|"aft"
]]
affectVectors = {
angular = {
yaw = nil,
pitch = nil,
roll = nil
},
lateral = {
x = nil,
y = nil,
z = nil
}
},
power = (tv.getSignal())/15
}
end
end
end
elseif Config.Debug then
print("DEBUG: thruster of name "..peripheral.getName(pv).." already indexed, skipping (called by PropellerInit)")
end
end
end
function ThrusterInit()
local thrusters = {}
for _, v in ipairs(ThrusterTypes) do
local vlist = peripheral.find(v)
if vlist ~= nil and vlist[1] ~= nil then
table.insert(thrusters, vlist)
end
end
for ti, tv in ipairs(thrusters) do
if Thrusters[peripheral.getName(tv)] == nil then
Thrusters[peripheral.getName(tv)] = {
thruster = tv,
transmission = nil,
type = "thruster",
--[[
NOTE:
affectVectors will have values depending on how the thruster's
thrust vector interacts with the vessel's attitude
angular:
possible directions for angular.yaw: "port"|"star"
possible directions for angular.roll: "port"|"star"
possible directions for angular.pitch: "up"|"down"
lateral:
possible directions for lateral.x: "port"|"star"
possible directions for lateral.y: "up"|"down"
possible directions for lateral.z: "fore"|"aft"
]]
affectVectors = {
angular = {
yaw = nil,
pitch = nil,
roll = nil
},
lateral = {
x = nil,
y = nil,
z = nil
}
},
power = tv.getPower()
}
elseif Config.Debug then
print("DEBUG: thruster of name "..peripheral.getName(tv).." already indexed, skipping (called by ThrusterInit)")
end
end
end
-- this is for populating nil values from thruster config files
local function populateThrusterValues()
for _, thruster in pairs(Thrusters) do
if thruster.name == nil and thruster.thruster == nil then -- as long as we have one of thruster or name, we can fetch other data
print("ERROR: Thruster ".._.." has no name or thruster object. Removing entry in thruster list")
Thrusters[_] = nil
return
end
if thruster.name == nil and thruster.thruster ~= nil then
thruster.name = peripheral.getName(thruster.thruster)
end
if thruster.thruster == nil and thruster.name ~= nil then
thruster.thruster = peripheral.wrap(thruster.name)
end
if thruster.type == nil then
if Config.Debug then print("DEBUG: Thruster "..thruster.name.." does not have a type.") end
for _, tt in ipairs(ThrusterTypes) do
if peripheral.getType(thruster.thruster) == tt then
if Config.Debug then print("DEBUG: Detected thruster "..thruster.name.." is of type 'thruster'.") end
thruster.type = "thruster"
break
end
end
for _, pt in ipairs(PropellerTypes) do
if peripheral.getType(thruster.thruster) == pt then
if Config.Debug then print("DEBUG: Detected thruster "..thruster.name.." is of type 'rotator'.") end
thruster.type = "rotator"
break
end
end
end
if thruster.type == "rotator" and thruster.transmission == nil then
if Config.Debug then print("DEBUG: Thruster "..thruster.name.." does not have an associated transmission.") end
local subnetworkId = peripheral.wrap(thruster.name).getSourceId()
for _, transmissionType in ipairs(TransmissionTypes) do
local transmissions = { peripheral.find(transmissionType) }
if transmissions ~= nil and transmissions[1] ~= nil then
if Config.Debug then print("DEBUG: transmission of type "..transmissionType.." found.") end
for _, transmission in ipairs(transmissions) do
local thisTransmissionId = transmission.getSelfId()
if Config.Debug then print("DEBUG: Comparing subnetwork IDs values: "..thisTransmissionId.." == "..subnetworkId) end
if transmission.getSelfId() == subnetworkId then
if Config.Debug then print("DEBUG: Detected that thruster "..thruster.name.." is connected to transmission "..peripheral.getName(transmission)) end
thruster.transmission = peripheral.getName(transmission)
break
elseif Config.Debug then
print("DEBUG: Comparison inequal")
end
end
elseif Config.Debug then
print("DEBUG: No transmissions of type "..transmissionType.." (print from populateThrusterValues)")
end
end
end
if thruster.power == nil then
if Config.Debug then print("DEBUG: Thruster "..thruster.name..": thruster power is not populated") end
if thruster.type == "thruster" then
thruster.power = thruster.thruster.getPower()
elseif thruster.type == "rotator" then
if thruster.transmission ~= nil then
local transmissionType = peripheral.getType(thruster.transmission)
if transmissionType == "analog_transmission" then
thruster.power = (thruster.transmission.getSignal())/15
elseif transmissionType == "Create_RotationSpeedController" then
thruster.power = (thruster.transmission.getTargetSpeed())/256
end
else
print("ERROR: Thruster "..peripheral.getName(thruster.thruster).." has no transmission.")
end
end
end
end
end
function Update()
populateThrusterValues()
PropellerInit()
ThrusterInit()
end
local function checkIfThrusterIsIndexed()
local thrusters = {}
local unindexedThrusters = {}
for _, v in ipairs(ThrusterTypes) do
local thrustersList = peripheral.find(v)
if thrustersList then
table.insert(thrusters, thrustersList)
end
end
for _, v in ipairs(PropellerTypes) do
local thrustersList = peripheral.find(v)
if thrustersList then
table.insert(thrusters, thrustersList)
end
end
for _, v in ipairs(thrusters) do
if Thrusters[peripheral.getName(v)] == nil then
table.insert(unindexedThrusters, v)
end
end
if #unindexedThrusters == 0 then
return nil
end
return unindexedThrusters
end
local function partiallyUpdateThrusters(thrusterList)
local thrusterTypes = {
"thruster",
"ion_thruster",
"vector_thruster",
"liquid_vector_thruster"
}
local propellerTypes = {
"gyroscopic_propeller_bearing",
"propeller",
"propeller_bearing"
}
-- for every thruster in the input array
-- check if they are one of thrusterTypes
for _, tv in ipairs(thrusterList) do
for _, tt in ipairs(ThrusterTypes) do
if peripheral.getType(tv) == tt then
Thrusters[peripheral.getName(tv)] = {
thruster = tv,
transmission = nil,
type = "thruster",
affectVectors = {
angular = {
yaw = nil,
roll = nil,
pitch = nil
},
lateral = {
x = nil,
y = nil,
z = nil
}
},
power = tv.getPower()
}
end
end
for _, pt in ipairs(PropellerTypes) do
if peripheral.getType(tv) == pt then
local thisThruster = {
thruster = tv,
transmission = nil,
type = "rotator",
affectVectors = {
angular = {
yaw = nil,
roll = nil,
pitch = nil
},
lateral = {
x = nil,
y = nil,
z = nil
}
},
power = tv.getPower()
}
for _, tt in ipairs(TransmissionTypes) do
local transmissions = peripheral.find(tt)
for _, t in ipairs(transmissions) do
if tv.getSourceId() == t.getSelfId() then
thisThruster.transmission = peripheral.getName(t)
end
end
end
Thrusters[peripheral.getName(tv)] = thisThruster
end
end
end
end
function PollThrottle()
for _, v in pairs(Config.Throttles) do
if v.name and v.side then
v.input = peripheral.wrap(v.name).getAnalogInput(v.side)
end
end
end
function PollSensors()
PollNavTable()
PollAltitude()
PollGimbal()
PollVelocity()
end
function PollVelocity()
local velSensors = { peripheral.find("velocity_sensor") }
-- Velocity Sensors
VelocityVectors = {}
if velSensors[1] ~= nil then -- verify array is not empty
SensorData.Velocity = {}
for _, velsensor in pairs(velSensors) do
local vsAxis = velsensor.getAxis()
local vsVelocity = velsensor.getVelocity()
-- correct for heading correction value
if Config.SensorCorrection.Heading == -90 or Config.SensorCorrection.Heading == 270 then
if vsAxis == "x" then vsAxis = "z" end
if vsAxis == "z" then vsAxis = "x" end
elseif Config.SensorCorrection.Heading == 180 or Config.SensorCorrection.Heading == -180 then
vsVelocity = -vsVelocity
elseif Config.SensorCorrection.Heading == 90 or Config.SensorCorrection.Heading == -270 then
if vsAxis == "x" then vsAxis = "z" end
if vsAxis == "z" then vsAxis = "x" end
vsVelocity = -vsVelocity
end
VelocityVectors[vsAxis] = vsVelocity
SensorData.Velocity.Raw = VelocityVectors
end
if Config.Debug then
print("DEBUG: PollVelocity fetched sensor data: "..tableToString(SensorData.Velocity))
end
else
if Config.Debug then
print("DEBUG: No velocity sensors found.")
end
end
end
function PollAltitude()
local altSensor = peripheral.find("altitude_sensor") -- enforce one alt sensor
-- Altitude Sensor
if altSensor then
SensorData.Altitude = {}
Altitude = altSensor.getHeight()
AirPressure = altSensor.getAirPressure()
VerticalSpeed = altSensor.getVerticalSpeed()
SensorData.Altitude.Altitude = Altitude
SensorData.Altitude.AirPressure = AirPressure
SensorData.Altitude.VerticalSpeed = VerticalSpeed
if Config.Debug then
print("DEBUG: PollAltitude fetched sensor data: "..tableToString(SensorData.Altitude))
end
end
end
function PollNavTable()
local navTable = peripheral.find("navigation_table") -- enforce one nav table
-- Navigation Table
if navTable then
SensorData.NavTable = {}
Heading = navTable.getHeading()
if Config.SensorCorrection.Heading ~= 0 then
local CorrectedHeading = Heading + Config.SensorCorrection.Heading
print("DEBUG: Heading = "..Heading.." | Correction = "..Config.SensorCorrection.Heading )
print("DEBUG: Corrected Heading = "..CorrectedHeading)
if CorrectedHeading > 360 then
CorrectedHeading = CorrectedHeading - 360
elseif CorrectedHeading < 0 then
CorrectedHeading = CorrectedHeading + 360
end
Heading = CorrectedHeading
end
SensorData.NavTable.Heading = Heading
NavTableHasTarget = navTable.hasTarget()
SensorData.NavTable.HasTarget = NavTableHasTarget
if NavTableHasTarget then
BearingToTarget = navTable.getBearing()
TargetClosureRate = navTable.getClosureRate()
TargetVerticalOffset = navTable.getVerticalOffsetToTarget()
TargetRelativeAngle = navTable.getRelativeAngle()
end
if not NavTableHasTarget then
BearingToTarget = 0
TargetClosureRate = 0
TargetVerticalOffset = 0
TargetRelativeAngle = 0
end
SensorData.NavTable.TargetBearing = BearingToTarget
SensorData.NavTable.TargetClosureRate = TargetClosureRate
SensorData.NavTable.TargetVerticalOffset = TargetVerticalOffset
SensorData.NavTable.TargetRelativeAngle = TargetRelativeAngle
end
end
function PollGimbal()
local gimbalSensor = peripheral.find("gimbal_sensor") -- enforce one gimbal sensor
-- Gimbal Sensor
if gimbalSensor then
SensorData.Gimbal = {}
Angles = {
xAngle = 0, -- pitch angle
zAngle = 0 -- roll angle
}
for i, v in ipairs(gimbalSensor.getAngles()) do
if i == 1 then
Angles.xAngle = v
elseif i == 2 then
Angles.zAngle = v
end
end
-- correct for heading correction value
if Config.SensorCorrection.Heading == -90 or Config.SensorCorrection.Heading == 270 then
local temp = Angles.xAngle
Angles.xAngle = Angles.zAngle
Angles.zAngle = temp
elseif Config.SensorCorrection.Heading == 180 or Config.SensorCorrection.Heading == -180 then
Angles.xAngle = -Angles.xAngle
Angles.zAngle = -Angles.zAngle
elseif Config.SensorCorrection.Heading == 90 or Config.SensorCorrection.Heading == -270 then
local temp = Angles.xAngle
Angles.xAngle = -Angles.zAngle
Angles.zAngle = -temp
end
AngularRates = {
wx = 0, -- pitch rate
wy = 0, -- yaw rate
wz = 0 -- roll rate
}
for i, v in ipairs(gimbalSensor.getAngularRates()) do
if i == 0 then
AngularRates.wx = v
elseif i == 1 then
AngularRates.wy = v
elseif i == 2 then
AngularRates.wz = v
end
end
-- correct for heading correction value
if Config.SensorCorrection.Heading == -90 or Config.SensorCorrection.Heading == 270 then
local temp = AngularRates.wx
AngularRates.wx = AngularRates.wz
AngularRates.wz = temp
elseif Config.SensorCorrection.Heading == 180 or Config.SensorCorrection.Heading == -180 then
AngularRates.wx = -AngularRates.wx
AngularRates.wz = -AngularRates.wz
elseif Config.SensorCorrection.Heading == 90 or Config.SensorCorrection.Heading == -270 then
local temp = AngularRates.wx
AngularRates.wx = -AngularRates.wz
AngularRates.wz = -temp
end
LinearAcceleration = {
ax = 0, -- port-starboard axis
ay = 0, -- up-down axis
az = 0 -- fore-aft axis
}
for i, v in ipairs(gimbalSensor.getLinearAcceleration()) do
if i == 0 then
LinearAcceleration.ax = v
elseif i == 1 then
LinearAcceleration.ay = v
elseif i == 2 then
LinearAcceleration.az = v
end
end
-- correct for heading correction value
if Config.SensorCorrection.Heading == -90 or Config.SensorCorrection.Heading == 270 then
local temp = LinearAcceleration.ax
LinearAcceleration.ax = LinearAcceleration.az
LinearAcceleration.az = temp
elseif Config.SensorCorrection.Heading == 180 or Config.SensorCorrection.Heading == -180 then
LinearAcceleration.ax = -LinearAcceleration.ax
LinearAcceleration.az = -LinearAcceleration.az
elseif Config.SensorCorrection.Heading == 90 or Config.SensorCorrection.Heading == -270 then
local temp = LinearAcceleration.ax
LinearAcceleration.ax = -LinearAcceleration.az
LinearAcceleration.az = -temp
end
SensorData.Gimbal.Angles = Angles
SensorData.Gimbal.AngularRates = AngularRates
SensorData.Gimbal.LinearAcceleration = LinearAcceleration
if Config.Debug then
print("DEBUG: PollGimbal fetched sensor data: "..tableToString(SensorData.Gimbal))
end
end
end
-- the sigmoid is probably the best function to use for determining how much thrust to apply based on the current velocity or angular rate
function Sigmoid(x)
local e = 2.718281828459045
return 1 / (1 + e^(-x))
end
-- this sigmoid is modified to return a value between -1 and 1, and inverted so that the output is negative when the input is positive, and vice versa
-- the idea is to take the velocity or angular rate, and return a value that can be used to determine how much thrust to apply in the opposite direction to counteract it
-- then, the output can be given to the update thrust function which will propogate the value to the thrusters
function CustomSigmoid(x)
return ((-2 * Sigmoid(x)) + 1)
end
-- thruster is a thruster type, power is a vector from 0.0 to 1.0
function SetThrusterPower(thruster, power)
if thruster == nil then
print("ERROR: SetThrusterPower called with nil thruster.")
return
end
if power > 1.0 then power = 1.0 elseif power < 0.0 then power = 0.0 end
if Config.Debug then print("DEBUG: thruster "..thruster.name..": power "..power) end
if thruster.type == "rotator" then
thruster.power = power
local actualPower = 0
if thruster.transmission == nil then
print("ERROR: SetThrusterPower called with nil transmission for rotator thruster.")
print("Thruster: "..thruster.name)
return
end
if peripheral.getType(peripheral.wrap(thruster.transmission)) == "Create_RotationSpeedController" then
if (math.ceil(power*256) - power*256 <= 0.5) then
actualPower = math.ceil(power*256)
end
if (math.ceil(power*256) - power*256) > 0.5 then
actualPower = math.floor(power*256)
end
thruster.transmission.setTargetSpeed(actualPower)
end
if peripheral.getType(peripheral.wrap(thruster.transmission)) == "analog_transmission" then
if (math.ceil(power*15) - power*15) <= 0.5 then
actualPower = math.ceil(power*15)
end
if (math.ceil(power*15) - power*15) > 0.5 then
actualPower = math.floor(power*15)
end
if actualPower < 1 then
actualPower = 15 -- signal 15 will decouple the speed and stop the motion
elseif actualPower <= 15 then
actualPower = actualPower - 1
end
peripheral.wrap(thruster.transmission).setSignal(actualPower)
-- if Config.Debug then print("DEBUG: actualPower: "..actualPower) end
end
elseif thruster.type == "thruster" then
thruster.power = power
thruster.thruster.setPowerNormalized(power)
end
end
function GetThrusterPower(thruster)
if thruster.type == "rotator" then
return thruster.power
end
if thruster.type == "thruster" then
return thruster.thruster.getPower()
end
end
-- PID systems for stabilization and autopilot
LastTime = os.epoch("utc")
function GetDeltaTime()
local currentTime = os.epoch("utc")
local dt = (currentTime - LastTime) / 1000 -- convert milliseconds to seconds
LastTime = currentTime
if dt <= 0 then dt = 0.001 end -- prevent division by zero
return dt
end
function CreatePID(kp, ki, kd)
return {
kp = kp,
ki = ki,
kd = kd,
integral = 0, lastError = 0,
minOutput = 0.0,
maxOutput = 64.0,
update = function(self, setpoint, pv, dt)
local error = setpoint - pv
local P = self.kp * error
local D = 0
if dt > 0 then D = self.kd * ((error - self.lastError) / dt) else D = 0 end
local potential_i = self.integral + (self.ki * error * dt)
local raw_output = P + potential_i + D
local clampedOutput = 0
if self.minOutput <= potential_i and potential_i <= self.maxOutput then
self.integral = potential_i
clampedOutput = raw_output
else
clampedOutput = math.max(self.minOutput, math.min(raw_output, self.maxOutput))
end
self.lastError = error
local normalizedOutput = (clampedOutput - self.minOutput) / (self.maxOutput - self.minOutput)
return normalizedOutput
end
}
end
-- tune these for the best values for each vessel
PIDs = {
-- angular
-- attitude PIDs
RollAttitudePID = CreatePID(4.5, 0.01, 0.05),
PitchAttitudePID = CreatePID(4.5, 0.01, 0.05),
YawAttitudePID = CreatePID(0.1, 0.01, 0.05),
-- rate PIDs
RollRatePID = CreatePID(1.2, 0.01, 0.05),
PitchRatePID = CreatePID(1.2, 0.01, 0.05),
YawRatePID = CreatePID(0.5, 0.01, 0.05),
-- lateral
-- value PIDs
AltitudePID = CreatePID(0.6, 0.01, 0.05),
PortStarPID = CreatePID(0.1, 0.01, 0.05),
ForeAftPID = CreatePID(0.1, 0.01, 0.05),
-- rate PIDs
VerticalRatePID = CreatePID(1.2, 0.01, 0.05),
PortStarRatePID = CreatePID(0.1, 0.01, 0.05),
ForeAftRatePID = CreatePID(0.1, 0.01, 0.05)
}
local function createRollingAverage(window)
local buffer = {}
local index = 0
local count = 0
local sum = 0
return function(new_value)
index = (index % window) + 1
if buffer[index] then
sum = sum - buffer[index]
else
count = count + 1
end
buffer[index] = new_value
sum = sum + new_value
return sum / count
end
end
-- min and max thrust for thrust normalization
local minThrust = math.huge
local maxThrust = -math.huge
local function normalize(val, min, max)
if min == max then return 0.5 end
return (val - min) / (max - min)
end
function UpdateGlobalThrust()
-- for each thruster, there is a table of affectVectors that determine in what directions the thruster can apply thrust
-- each thruster has a power value from 0 to 1, which is the amount of thrust the thruster is currently applying
-- depending on the desired thrust vectors, some counteractive desired thrust vectors will be ignored
local dt = GetDeltaTime() / 1000
if SensorData.Gimbal == nil or SensorData.Altitude == nil or SensorData.Velocity == nil then
print("ERROR: One or more of SensorData is nil. Cannot update thrust.")
return
end
local AltitudeRateOutput = PIDs.VerticalRatePID:update(Config.Autopilot.AutopilotDesiredAltitude, SensorData.Altitude.Altitude, dt)
local throttleOutput = PIDs.AltitudePID:update(AltitudeRateOutput, SensorData.Altitude.VerticalSpeed, dt)
if Config.Debug then print("DEBUG: Throttle Output: "..throttleOutput) end
local pitchOutput = PIDs.PitchAttitudePID:update(0, SensorData.Gimbal.Angles.xAngle, dt)
local pitchRateOutput = PIDs.PitchRatePID:update(pitchOutput, SensorData.Gimbal.AngularRates.wx or 0, dt)
if Config.Debug then print("DEBUG: Pitch Output: "..pitchRateOutput) end
local rollOutput = PIDs.RollAttitudePID:update(0, SensorData.Gimbal.Angles.zAngle, dt)
local rollRateOutput = PIDs.RollRatePID:update(rollOutput, SensorData.Gimbal.AngularRates.wz or 0, dt)
if Config.Debug then print("DEBUG: Roll Output: "..rollRateOutput) end
local yawOutput = 5
if Config.Autopilot.AutopilotDesiredHeading ~= nil then yawOutput = PIDs.YawAttitudePID:update(Config.Autopilot.AutopilotDesiredHeading, SensorData.NavTable.Heading, dt) end
local yawRateOutput = PIDs.YawRatePID:update(Config.Autopilot.AutopilotDesiredYawRate or 0, SensorData.Gimbal.AngularRates.wy or 0, dt)
for _, thruster in pairs(Thrusters) do
local desiredThrust = 0
-- Calculate desired thrust based on affectVectors and PID outputs
if thruster.affectVectors.angular.pitch ~= nil and thruster.primary_pitch_thruster then
if thruster.affectVectors.angular.pitch == "up" then
desiredThrust = desiredThrust + pitchRateOutput
else
desiredThrust = desiredThrust - pitchRateOutput
end
end
if thruster.affectVectors.angular.roll ~= nil and thruster.primary_roll_thruster then
if thruster.affectVectors.angular.roll == "port" then
desiredThrust = desiredThrust + rollRateOutput
else
desiredThrust = desiredThrust - rollRateOutput
end
end
if thruster.affectVectors.angular.yaw ~= nil and thruster.primary_yaw_thruster then
if thruster.affectVectors.angular.yaw == "port" then
desiredThrust = desiredThrust + yawRateOutput
else
desiredThrust = desiredThrust - yawRateOutput
end
end
if thruster.affectVectors.lateral.y == "up" then
desiredThrust = desiredThrust + throttleOutput
end
if Config.Debug then print("DEBUG: "..thruster.name..": desiredThrust "..desiredThrust.." (from UpdateGlobalThrust)") end
SetThrusterPower(thruster, desiredThrust)
end
end
function Init()
local configFile = io.open(Config.ConfigPath, "r")
if configFile then
local configContent = configFile:read("*all")
Config = stringToTable(configContent)
configFile:close()
else
print("Could not open the config file")
end
-- Initialize monitors - identify which monitor is for which type
if Config.Monitors.InstrumentPanelMonitor == nil then
print("Identifying instrument panel monitor...")
Config.Monitors.InstrumentPanelMonitor = identifyMonitor("instrument panel")
else
Config.Monitors.InstrumentPanelMonitor.peripheral = peripheral.wrap(Config.Monitors.InstrumentPanelMonitor.name)
end
if Config.Monitors.AutopilotControlMonitor == nil then
print("Identifying autopilot control monitor...")
Config.Monitors.AutopilotControlMonitor = identifyMonitor("autopilot control")
else
Config.Monitors.AutopilotControlMonitor.peripheral = peripheral.wrap(Config.Monitors.AutopilotControlMonitor.name)
end
ThrottleInit()
Thrusters = {}
if Config.thrusterConfigPath ~= nil then
local thrusterConfigFile = io.open(Config.thrusterConfigPath, "r")
if thrusterConfigFile then
local configContent = thrusterConfigFile:read("*all")
local configChunk, err = load(configContent)
if configChunk then
Thrusters = configChunk()
else
print("ERROR: Error loading thruster config string: "..err)
end
thrusterConfigFile:close()
else
print("Could not open the thruster config file")
end
end
-- If we're here, the config loaded successfully.
populateThrusterValues()
Update()
end
-- Display functions for monitors
function displayInstrumentPanel(monitor, sensorData)
if not monitor or not monitor.peripheral then return end
local m = monitor.peripheral
m.clear()
m.setCursorPos(1, 1)
-- Simple instrument panel display
m.write("Caero Attitude Control")
m.setCursorPos(1, 2)
m.write("----------------------")
local next = next
if next(sensorData) ~= nil then
if sensorData then
m.setCursorPos(1, 3)
m.write("Pitch: " .. string.format("%.2f", SensorData.Gimbal.Angles.xAngle or 0))
m.setCursorPos(1, 4)
m.write("Roll: " .. string.format("%.2f", SensorData.Gimbal.Angles.zAngle or 0))
m.setCursorPos(1, 5)
m.write("Yaw: " .. string.format("%.2f", SensorData.NavTable.Heading or 0))
m.setCursorPos(1, 6)
m.write("Velocity: " .. string.format("%.2f", sensorData.velocity or 0))
m.setCursorPos(1, 7)
m.write("Altitude: " .. string.format("%.2f", SensorData.Altitude.Altitude or 0))
else
m.setCursorPos(1, 3)
m.write("Pitch: 0.00")
m.setCursorPos(1, 4)
m.write("Roll: 0.00")
m.setCursorPos(1, 5)
m.write("Yaw: 0.00")
m.setCursorPos(1, 6)
m.write("Velocity: 0.00")
m.setCursorPos(1, 7)
m.write("Altitude: 0.00")
end
-- Display autopilot status
local autopilotStatus = Config.Autopilot.AutopilotEngaged and "ON" or "OFF"
m.setCursorPos(1, 9)
m.write("Autopilot: " .. autopilotStatus)
end
end
function displayAutopilotControls(monitor, sensorData)
if not monitor or not monitor.peripheral then return end
local m = monitor.peripheral
m.clear()
m.setCursorPos(1, 1)
-- Simple autopilot control panel
m.write("Autopilot Controls")
m.setCursorPos(1, 2)
m.write("------------------")
m.setCursorPos(1, 3)
m.write("Engage: [ ]")
m.setCursorPos(1, 4)
m.write("Speed: [0.0]")
m.setCursorPos(1, 5)
m.write("Heading: [0.0]")
m.setCursorPos(1, 6)
m.write("Manual: [ ]")
m.setCursorPos(1, 7)
m.setCursorPos(1, 8)
m.write("Mode: Auto")
end
-- Sensor data collection - returns a table with all relevant sensor readings
function collectSensorData()
local data = {}
-- Collect gimbal data
data.gimbal = {
pitch = 0,
roll = 0
}
-- Collect navigation table data
data.navTable = {
heading = 0,
hasTarget = false
}
-- Collect altitude data
data.altitude = 0
-- Collect velocity data
data.velocity = 0
return data
end
function WriteConfigFiles()
-- since the thruster object will change every time, set each thruster's thruster object to nil before writing to the config file, so that the config file only contains the thruster names and not the thruster objects
for _, thruster in pairs(Thrusters) do
thruster.thruster = nil
thruster.transmission = nil
end
print("Writing to thruster config file.")
local thrusterConfigFile = fs.open(Config.thrusterConfigPath, "w+")
thrusterConfigFile.write(tableToString(Thrusters))
thrusterConfigFile.close()
print("Writing to config file")
local configFile = fs.open(Config.ConfigPath, "w+")
configFile.write(tableToString(Config))
configFile.close()
end
function Main()
Init()
os.sleep(5) -- pause for any initialization errors to display
local sentinel = true
print("Mainloop starting. Press 'q' to stop the loop and save configuration changes.")
while sentinel do
PollSensors()
UpdateGlobalThrust()
-- Update monitor displays
if Config.Monitors.InstrumentPanelMonitor then
displayInstrumentPanel(Config.Monitors.InstrumentPanelMonitor, SensorData)
end
if Config.Monitors.AutopilotControlMonitor then
displayAutopilotControls(Config.Monitors.AutopilotControlMonitor, SensorData)
end
local function getTerminateEvent()
local event, key, is_held = os.pullEventRaw("key")
if keys.getName(key) == "q" then
print("Quit Input Received")
sentinel = false
else
return
end
end
local function do_sleep() os.sleep(0.1) end
parallel.waitForAny(do_sleep, getTerminateEvent)
end
for _, t in pairs(Thrusters) do
if t.type == "rotator" then
SetThrusterPower(t, 0.0)
peripheral.wrap(t.transmission).releaseSignal() -- release control of transmissions on quit
else
SetThrusterPower(t, 0.0)
end
end
WriteConfigFiles()
end
-- Run the main function when the script starts
Main()