1234 lines
42 KiB
Lua
1234 lines
42 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(peripheral.wrap(thruster.transmission))
|
|
if transmissionType == "analog_transmission" then
|
|
thruster.power = (peripheral.wrap(thruster.transmission).getSignal())/15
|
|
elseif transmissionType == "Create_RotationSpeedController" then
|
|
thruster.power = (peripheral.wrap(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 = 32.0,
|
|
update = function(self, setpoint, pv, dt)
|
|
if dt <= 0 then return 0.0 end
|
|
|
|
local error = setpoint - pv
|
|
|
|
local P = self.kp * error
|
|
|
|
local D = self.kd * ((error - self.lastError) / dt)
|
|
|
|
local potential_i = self.integral + (self.ki * error * dt)
|
|
|
|
local raw_output = P + potential_i + D
|
|
|
|
if Config.Debug then print("DEBUG: PID raw output: "..raw_output) end
|
|
|
|
local clampedOutput = 0
|
|
|
|
if raw_output >= self.minOutput and raw_output <= 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
|
|
|
|
return clampedOutput
|
|
end
|
|
}
|
|
end
|
|
|
|
-- tune these for the best values for each vessel
|
|
PIDs = {
|
|
-- angular
|
|
-- attitude PIDs
|
|
RollAttitudePID = CreatePID(3.0, 0.01, 0.05),
|
|
PitchAttitudePID = CreatePID(3.0, 0.01, 0.05),
|
|
YawAttitudePID = CreatePID(0.1, 0.01, 0.05),
|
|
|
|
-- rate PIDs
|
|
RollRatePID = CreatePID(0.8, 0.01, 0.05),
|
|
PitchRatePID = CreatePID(0.8, 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(0.8, 0.01, 0.05),
|
|
PortStarRatePID = CreatePID(0.1, 0.01, 0.05),
|
|
ForeAftRatePID = CreatePID(0.1, 0.01, 0.05)
|
|
}
|
|
|
|
-- 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
|
|
|
|
if thruster.x_config_equivalent ~= nil then
|
|
|
|
-- 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)
|
|
else
|
|
if Config.Debug then print("DEBUG: Thruster has x_config_equivalent set") end
|
|
|
|
local prop1 = throttleOutput + pitchRateOutput + rollRateOutput -- front-left
|
|
local prop2 = throttleOutput + pitchRateOutput - rollRateOutput -- front-right
|
|
local prop3 = throttleOutput - pitchRateOutput - rollRateOutput -- back-right
|
|
local prop4 = throttleOutput - pitchRateOutput + rollRateOutput -- back-left
|
|
|
|
if thruster.x_config_equivalent == "1" then SetThrusterPower(thruster, prop1)
|
|
elseif thruster.x_config_equivalent == "2" then SetThrusterPower(thruster, prop2)
|
|
elseif thruster.x_config_equivalent == "3" then SetThrusterPower(thruster, prop3)
|
|
elseif thruster.x_config_equivalent == "4" then SetThrusterPower(thruster, prop4)
|
|
else print("ERROR: Thruster "..thruster.name.." has invalid x_config_equivalent value")
|
|
end
|
|
end
|
|
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()
|