|
/** |
|
* CIWS "Sentinel" weapon controller software. |
|
* |
|
* @see LogicExtensions Besiege mod by Lambda & fanzhuyifan |
|
* @author BSoDium |
|
*/ |
|
|
|
|
|
// system variables |
|
const Verbose = true; |
|
const SysVar = { |
|
"calibrate" : true, // calibrate on startup |
|
"autotest" : false, // autotest on startup |
|
"calibration_delay_x" : 200, // frames - this should vary between computers |
|
"calibration_delay_z" : 20, |
|
"update_delay" : 0.9, // time between mainloop calls |
|
"X_incert" : 0.005, // positionning uncertainty on X axis |
|
"Z_incert" : 0.05, // positionning uncertainty on Z axis |
|
"max_decel_time" : 10 |
|
}; |
|
const Sources = { |
|
100 : "startup debugger", |
|
101 : "runtime debugger", |
|
102 : "shutdown debugger", |
|
103 : "runtime notifier", |
|
104 : "error stream", |
|
105 : "auto test" |
|
}; |
|
const PIOdict = { |
|
"X_anglo" : 0, // PIO in |
|
"Z_anglo" : 1, // PIO in |
|
"entity_sensor" : 2, // PIO in |
|
"weapon" : 3, // PIO in |
|
"X_hinge_l" : 4, // PIO out |
|
"X_hinge_r" : 5, // PIO out |
|
"Z_wheel_fslow" : 6, // PIO out |
|
"Z_wheel_rslow" : 7, // PIO out |
|
"Z_wheel_ffast" : 8, // PIO out |
|
"Z_wheel_rfast" : 9, // PIO out |
|
}; |
|
|
|
// Active PIO list : |
|
// X anglometer -> 5001 |
|
// Z anglometer -> 5000 |
|
// entity sensor -> 6024 |
|
// fire weapon -> E |
|
// X hinge -> 3 left 4 right |
|
// Z wheel slow -> 1 forwards 2 reverse |
|
// Z wheel fast -> 5 forwards 6 reverse |
|
|
|
let AngloDict = { |
|
"X_anglo_l" : 0.24675, // left threshold |
|
"X_anglo_r" : 0.75217, // right threshold |
|
"Z_anglo_start" : 0, |
|
"Z_decel_dist" : 0, |
|
}; |
|
|
|
let targets = []; |
|
|
|
/** |
|
* Send a message along with its associated source/emitter |
|
* basically an upgraded print function |
|
* @param {number} F_SourceId Message emitter Id |
|
* @param {string} F_Message Message content |
|
* @param {boolean} O_Verbose Toggle print, omitting argument always displays message |
|
*/ |
|
function ConsoleOutput(F_SourceId, F_Message, O_Verbose = 1) { |
|
if (O_Verbose) { |
|
message = `[${Sources[F_SourceId]}] ${F_Message}`; |
|
print(message); |
|
} |
|
} |
|
|
|
function GetVal(F_PioId) { |
|
return in(PIOdict[F_PioId]); |
|
} |
|
|
|
function SetVal(F_PioId, F_Value) { |
|
out(PIOdict[F_PioId], F_Value); |
|
} |
|
|
|
function SleepFrames(F_Frames = 1) { // if no argument is provided SleepFrames() equals to asleep(), it skips a frame |
|
for (let i = 0; i < F_Frames; i++) { |
|
UpdatePos(); |
|
asleep(); |
|
} |
|
return; |
|
} |
|
|
|
function wait(F_Event, O_Event_args = []) { |
|
let FrameCounter = 0; |
|
while (!(F_Event.apply(null, O_Event_args))) { |
|
// ConsoleOutput(103, F_Event.apply(null, F_Event_args)); |
|
UpdatePos(); |
|
FrameCounter++; |
|
asleep(); |
|
} |
|
return FrameCounter; |
|
} |
|
|
|
function UpdatePos() { |
|
LastPos = [GetVal("X_anglo"), GetVal("Z_anglo")]; |
|
} |
|
|
|
function KeepPressed(F_PioId, O_Frames = 10, O_Action = SleepFrames, O_Action_args) { |
|
SetVal(F_PioId, true); // start pressing |
|
if (O_Action == SleepFrames) { |
|
O_Action(O_Frames); // wait |
|
} else { |
|
O_Action.apply(null, O_Action_args); |
|
} |
|
SetVal(F_PioId, false); // stop pressing |
|
return; |
|
} |
|
|
|
function min(F_a, F_b) { |
|
if (F_a > F_b) { |
|
return F_b; |
|
} else { |
|
return F_a; |
|
} |
|
} |
|
|
|
function max(F_a, F_b) { |
|
if (F_a > F_b) { |
|
return F_a; |
|
} else { |
|
return F_b; |
|
} |
|
} |
|
|
|
function avg(table) { |
|
let sum = 0; |
|
for (let i = 0; i < table.length; i++) { |
|
sum += table[i]; |
|
} |
|
sum /= table.length; |
|
return sum; |
|
} |
|
|
|
function Calibrate(F_AngloDict) { |
|
// X axis calibration |
|
ConsoleOutput(103, "Calibrating X axis...", Verbose); |
|
KeepPressed("X_hinge_r", SysVar["calibration_delay_x"]); |
|
F_AngloDict["X_anglo_r"] = GetVal("X_anglo"); |
|
|
|
KeepPressed("X_hinge_l", 2*SysVar["calibration_delay_x"]); |
|
F_AngloDict["X_anglo_l"] = GetVal("X_anglo"); |
|
ConsoleOutput(103, "X axis successfully calibrated", Verbose); |
|
|
|
// Z axis calibration |
|
ConsoleOutput(103, "Calibrating Z axis...", Verbose); |
|
KeepPressed("Z_wheel_ffast", SysVar["calibration_delay_z"]); |
|
let start = GetVal("Z_anglo"); |
|
SleepFrames(SysVar["calibration_delay_z"]); |
|
let end = GetVal("Z_anglo"); |
|
if (end >= start) { |
|
let Delta = end - start; |
|
F_AngloDict["Z_decel_dist"] = Delta; |
|
} else { |
|
ConsoleOutput(104, "Deceleration time too long. Failed to calibrate Z axis", Verbose) |
|
return; |
|
} |
|
ConsoleOutput(103, F_AngloDict["Z_decel_dist"]); |
|
ConsoleOutput(103, "Z axis successfully calibrated", Verbose); |
|
SleepFrames(100); // remove when not debugging |
|
return; |
|
} |
|
|
|
// events |
|
function ReachedEventRight(F_Angle, F_PioId) { |
|
let Current = GetVal(F_PioId); |
|
return (F_Angle > Current || Current > 0.25); |
|
} |
|
|
|
function ReachedEventLeft(F_Angle, F_PioId) { |
|
let Current = GetVal(F_PioId); |
|
return (F_Angle < Current && Current <= 0.25); |
|
} |
|
|
|
function ReachedEventForwards(F_Angle, F_PioId) { |
|
let Current = GetVal(F_PioId); |
|
let Delta = Math.abs(F_Angle - Current); |
|
|
|
if (Delta < AngloDict["Z_decel_dist"]) { |
|
return true; // stop |
|
} |
|
|
|
if (Delta < 0.5) { // the 0|1 threshold isn't in the way |
|
return (F_Angle < Current); |
|
} else if (Delta > 0.5) { // 0|1 threshold not crossed yet |
|
return (F_Angle > Current); |
|
} else { |
|
return true; // Delta = 0.5 |
|
} |
|
} |
|
|
|
function ReachedEventBackwards(F_Angle, F_PioId) { |
|
let Current = GetVal(F_PioId); |
|
let Delta = Math.abs(F_Angle - Current); |
|
|
|
if (Delta < AngloDict["Z_decel_dist"]) { |
|
return true; |
|
} |
|
|
|
if (Delta < 0.5) { // cf ReachedEventBackwards |
|
return (F_Angle > GetVal(F_PioId)); |
|
} else if (Delta > 0.5) { |
|
return (F_Angle < GetVal(F_PioId)); |
|
} else { |
|
return true; |
|
} |
|
|
|
} |
|
|
|
function ReachedFullRotation(F_PioId) { |
|
let Current = GetVal(F_PioId); |
|
let Sensor = GetVal("entity_sensor"); |
|
|
|
if (Sensor) { |
|
targets = targets.concat([[GetVal("X_anglo"), GetVal("Z_anglo")]]); |
|
} |
|
|
|
return Current > (1 - SysVar["Z_incert"]); |
|
} |
|
|
|
// actions |
|
/** |
|
* |
|
* @param {number} F_Angle Target angle |
|
*/ |
|
function GoToPosX(F_Angle) { |
|
let target_left = (0 <= F_Angle && F_Angle <= (AngloDict["X_anglo_l"]) + SysVar["X_incert"]); // target valid |
|
let hinge_left = (0 <= GetVal("X_anglo") && GetVal("X_anglo") <= (AngloDict["X_anglo_l"]) + SysVar["X_incert"]); // hinge position normal |
|
let hinge_left_of_target = (GetVal("X_anglo") > F_Angle); // hinge position relative to target |
|
|
|
if (target_left) { // target angle on the left side (valid) |
|
if (hinge_left_of_target && hinge_left) { // go right |
|
KeepPressed("X_hinge_r", null, wait, [ReachedEventRight, [F_Angle, "X_anglo"]]); |
|
} else if (!hinge_left_of_target && hinge_left) { |
|
KeepPressed("X_hinge_l", null, wait, [ReachedEventLeft, [F_Angle, "X_anglo"]]); |
|
} else if (!hinge_left) { |
|
KeepPressed("X_hinge_l", null, wait, [ReachedEventLeft, [0, "X_anglo"]]); |
|
ConsoleOutput(101, "Hinge out of working area, resetting", Verbose); |
|
} |
|
} else { // target angle on the right side (invalid) |
|
ConsoleOutput(104, "Hinge target angle in dead zone", Verbose); |
|
} |
|
} |
|
|
|
/** |
|
* |
|
* @param {number} F_Angle Target angle |
|
* @param {string} O_Path Path to target ("shortest" | "longest"), not implemented yet |
|
*/ |
|
function GoToPosZ(F_Angle, O_Path = "shortest") { |
|
let Current = GetVal("Z_anglo"); // Current angle |
|
//let Delta = F_Angle - Current; |
|
|
|
let Delta0 = max(F_Angle, Current) - min(F_Angle, Current); |
|
let Delta1 = 1 - max(F_Angle, Current) + min(F_Angle, Current); |
|
|
|
// choose speed |
|
if (min(Delta0, Delta1) <= 2*AngloDict["Z_decel_dist"]) { |
|
var speedstr = "slow"; |
|
} else { |
|
var speedstr = "fast"; |
|
} |
|
|
|
if (max(F_Angle, Current) == F_Angle) { |
|
if (Delta0 < 0.5) { |
|
KeepPressed(`Z_wheel_f${speedstr}`, null, wait, [ReachedEventForwards, [F_Angle, "Z_anglo"]]); |
|
} else if (Delta1 < 0.5) { |
|
KeepPressed(`Z_wheel_r${speedstr}`, null, wait, [ReachedEventBackwards, [F_Angle, "Z_anglo"]]); |
|
} else if (Delta0 == 0.5) { // no shortest path, choose forwards |
|
KeepPressed(`Z_wheel_f${speedstr}`, null, wait, [ReachedEventForwards, [F_Angle, "Z_anglo"]]); |
|
} |
|
} else { |
|
if (Delta0 < 0.5) { |
|
KeepPressed(`Z_wheel_r${speedstr}`, null, wait, [ReachedEventBackwards, [F_Angle, "Z_anglo"]]); |
|
} else if (Delta1 < 0.5) { |
|
KeepPressed(`Z_wheel_f${speedstr}`, null, wait, [ReachedEventForwards, [F_Angle, "Z_anglo"]]); |
|
} else if (Delta0 == 0.5) { // no shortest path, choose forwards |
|
KeepPressed(`Z_wheel_r${speedstr}`, null, wait, [ReachedEventBackwards, [F_Angle, "Z_anglo"]]); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Set the angle of the Hinge and the wheel to F_AngleX and F_AngleZ respectively |
|
* |
|
* @param {number} F_AngleX Target angle - X axis |
|
* @param {number} F_AngleZ Target angle - Z axis |
|
* @param {number} O_Loops Number of GoToPosX and GoToPosZ calls in the loop (a high value means a higher precision, and obviously a slower process) |
|
*/ |
|
function setPos(F_AngleX, F_AngleZ, O_Loops = 1) { |
|
for (let i = 0; i < O_Loops; i++) { |
|
GoToPosX(F_AngleX); |
|
GoToPosZ(F_AngleZ); |
|
} |
|
} |
|
|
|
function AutoTest() { |
|
let positions = [ |
|
[0.125, 0.5, 1], // x, z, loop_count |
|
[0, 0, 1], |
|
[0.125, 0.5, 1] |
|
] |
|
let error = Array(positions.length).fill(0); |
|
for (let i = 0; i < positions.length; i++) { |
|
setPos.apply(null, positions[i]); |
|
SleepFrames(150); |
|
ConsoleOutput(105, `${GetVal("X_anglo")} ${GetVal("Z_anglo")} final`, Verbose); |
|
let X_current = GetVal("X_anglo"), Z_current = GetVal("Z_anglo"); |
|
let errorX = min(Math.abs(X_current - positions[i][0]), Math.abs(1 - max(X_current, positions[i][0]) + min(X_current, positions[i][0]))); |
|
let errorZ = min(Math.abs(Z_current - positions[i][1]), Math.abs(1 - max(Z_current, positions[i][1]) + min(Z_current, positions[i][1]))); |
|
error[i] = 360*(errorX + errorZ)/2; |
|
} |
|
ConsoleOutput(103, `avg positionning error : ${avg(error)} °`); |
|
} |
|
|
|
function scan(F_PassCount) { |
|
targets = []; |
|
let res = 0.25/F_PassCount; |
|
setPos(0,0); |
|
for (let i = 0; i < F_PassCount; i++) { |
|
// do a full rotation |
|
GoToPosX(res*i); |
|
KeepPressed("Z_wheel_ffast", null, wait, [ReachedFullRotation, ["Z_anglo"]]); |
|
SleepFrames(10); // allow the wheel to rotate enough for the ReachedFullRotation Event to turn off |
|
} |
|
} |
|
|
|
function fire() { |
|
SetVal("weapon", true); |
|
SleepFrames(1); |
|
SetVal("weapon", false); |
|
} |
|
|
|
function engage(F_Targets) { |
|
for (let i = 0; i < F_Targets.length; i++) { |
|
let target = F_Targets[i]; |
|
setPos.apply(null, target); |
|
while (GetVal("entity_sensor")) { |
|
fire(); |
|
print("firing"); |
|
} |
|
print(`target ${i+1} of ${F_Targets.length} lost`) |
|
} |
|
} |
|
|
|
// main loop |
|
function MainLoop() { |
|
scan(10); |
|
if (targets.length > 0) { |
|
ConsoleOutput(103, `${targets.length} targets located, engaging`, Verbose); |
|
engage(targets); |
|
} |
|
|
|
// program next execution of MainLoop |
|
setTimeout(SysVar["update_delay"], MainLoop); |
|
} |
|
|
|
// program start |
|
print('\n'); |
|
ConsoleOutput(100, "booting up...", Verbose); |
|
SysVar["calibrate"] && Calibrate(AngloDict); |
|
SysVar["autotest"] && AutoTest(); |
|
|
|
|
|
MainLoop(); |
|
|