إرساء ISS باستخدام JavaScript والبوصلة

أصدرت شركة SpaceX ، التي أسسها قناع Ilon سيئ السمعة ، جهاز محاكاة لرسو السفن اليدوي لمركبة Crew Dragon الفضائية مع ISS. إذا سار كل شيء وفقًا للخطة ، فسيتم الإرساء في 27 مايو 2020. سيتم ذلك في الوضع التلقائي بالكامل ، ولكن سيتمكن الطاقم من التبديل إلى التحكم اليدوي. في الواقع ، هو الوضع اليدوي الذي يتم إعادة إنتاجه في المحاكي.

يقع جهاز المحاكاة نفسه في الموقع وهو لعبة إشكالية إلى حد ما من التلال الأولى ... يحاول

مكوك الفضاء الطيران بالطريقة الخاطئة ... والدقة التي تحتاجها للوصول إلى البوابة هي 20 سم ... على طول ثلاثة محاور ، بالإضافة إلى السرعة الزاوية وسرعة الإزاحة إلخ

بدأت المشاعر الوطنية تلعب بي وبطريقة ما أصبحت عارًا لقوة الفضاء السابقة ، وأخذت هذه المحاكاة كتحدي. نظرًا لأن Musk قرر إظهار مدى تعقيد الإرساء ، والصعوبات التي واجهها مهندسوهم لإنشاء برنامج إرساء تلقائي ، فقد قررت كتابة ، في وقت فراغي ، برنامج JavaScript يربط Dragon و ISS بسهولة في هذا المحاكي.

كيف تحب ذلك ، إيلون ماسك؟

صورة
التدخين عادة سيئة لصحتك

انتباه! هذه الخوارزمية هي "مزاح" ، ولم تكن مخصصة للاستخدام في الظروف الحقيقية. المؤلف غير مسؤول عن أي خسائر مباشرة أو غير مباشرة تتكبدها المركبة الفضائية أو أي أشياء أخرى باستخدام هذه الخوارزمية.




أولاً ، القليل من التاريخ.

من المعروف جيدًا أن مكوكنا الفضائي القابل لإعادة الاستخدام Buran كان مشابهًا جدًا للمكوك الأمريكي. ومن المعروف أيضًا أنه طار مرة واحدة فقط ، على عكس "نظرائه" الأمريكيين. لكن قلة من الناس يعرفون أن رحلته الوحيدة كانت بدون طيار. انطلق هو نفسه ، وهبط وفعل كل هذا في ظروف جوية سيئة للغاية.

دائمًا ما تهبط المكوكات الأمريكية فقط في الوضع اليدوي. كان هذا مفاجئًا نظرًا لأن أجهزة الكمبيوتر لم تكن أقوى من الآلة الحاسبة. هذا ، من الناحية النظرية ، لا ينبغي أن يكون صعبا ، فكر الليلة الماضية.



لكن دعنا نصل إلى النقطة. ما هو جهاز محاكاة على موقع SpaceX .

في البداية ، نرى معلومات عامة بأن الانحراف في جميع المعلمات يجب أن يكون في حدود 0.2 متر (20 سم). بالنظر إلى حجم المحطة والسفينة ، يعد هذا قيدًا خطيرًا للغاية.

نبدأ المحاكاة ونرى.



أعلاه ، إلى اليمين وأسفل الدائرة المركزية يوجد الانحراف الزاوي للسفينة على ثلاثة محاور.
الأخضر - القيمة الحالية.

الأزرق - السرعة التي تتغير بها في الثانية.

الإزاحة اليسرى بالنسبة إلى البوابة بالأمتار. لا توجد سرعة إزاحة ...

وحدات التحكم في الجزء السفلي من الشاشة هي أزرار مع تكرارها على لوحة المفاتيح.

لذلك سنبدأ معهم تحليل البرنامج باعتباره الأقل إثارة للاهتمام.

تخطيط أزرار لوحة المفاتيح.



الكتلة اليسرى مسؤولة عن الإزاحة المتعلقة بالبوابة ، لكن الكتلة اليمنى للإزاحة بالنسبة إلى المحاور.

نكتب ، أو نجد على الشبكة ، رمزًا يمكنه محاكاة نقرات لوحة المفاتيح على المستند. في حالتي ، بدا الرمز هكذا.

function simulateKey(keyCode, type, modifiers) {
    var evtName = (typeof (type) === "string") ? "key" + type : "keydown";
    var modifier = (typeof (modifiers) === "object") ? modifier : {};

    var event = document.createEvent("HTMLEvents");
    event.initEvent(evtName, true, false);
    event.keyCode = keyCode;

    for (var i in modifiers) {
        event[i] = modifiers[i];
    }
    document.dispatchEvent(event);
}

function keyPress(keyCode) {
    simulateKey(keyCode)
    setTimeout(() => simulateKey(keyCode, "up"), 15);
}

نكتب رموز الزر:
let _accelerator = 69;
let _brake = 81;
let _translateLeft = 65;
let _translateRigth = 68;
let _translateUp = 87;
let _translateDown = 83;

let _left = 37;
let _rigth = 39;
let _up = 38;
let _down = 40;

let _rollRigth = 105;
let _rollLeft = 103;

يتضمن أي نظام تحكم العمل في دورة. دعونا نجعلها أسهل ، بزيادات قدرها 200 مللي ثانية. سننظم عدادًا لواحد ، سنظل بحاجة إليه.

let index = 0;
function A() {

    index++;
    setTimeout(A, 200);
}

A();

العودة إلى هيكل الموقع.

ميزتها المثيرة للاهتمام هي أن ISS مرسومة على قماش ، ولكن المعلومات حول حالة المركبة الفضائية لدينا يتم رسمها بالترميز المعتاد. يبدو أن مطوري الموقع افترضوا أنه سيكون هناك متحمسون مشابهون يرغبون في "أتمتة" اللعبة ومنحهم مثل هذه الفرصة ... أو ربما من خلال ترميزها ، كان الأمر غبيًا وسهل القيام به.



لذا ، دعنا نضيف خطين إضافيين للضوء من أجل الحصول على معلومات حول حالة المركبة الفضائية.


    let range = parseFloat($("#range .rate").outerText.split(' '));

    let yDistance = parseFloat($("#y-range .distance").outerText.split(' ')[0]);
    let zDistance = parseFloat($("#z-range .distance").outerText.split(' ')[0]);

    let rollError = parseFloat($("#roll .error").outerText);
    let pitchError = parseFloat($("#pitch .error").outerText);
    let yawError = parseFloat($("#yaw .error").outerText);

    let rate = parseFloat($("#rate .rate").outerText.split(' ')[0]);

كما ترون ، لم أخرج كل شيء. لقد قمت بسحب قيم الإزاحة فقط ، لكنني لم أحسب معدل تغيير القيم ، ولهذا السبب ...

في الواقع ، هذا هو التكرار الثالث للخوارزمية. في البداية ، إنه خيار بسيط ، حيث يأخذ كل 200 مللي ثانية معلومات حول حالة السفينة ويضبطها إلى 0.

وبدا الأمر كذلك.

if (rollError !== -rollSpeed) {
        const rollLimit = (Math.abs(rollError) / 10);
        if (0 < rollError && rollSpeed < rollLimit) {
            keyPress(_rollRigth);
        } else if (rollError < -0 && -rollLimit < rollSpeed) {
            keyPress(_rollLeft);
        }
    }

وفي الواقع ، كان عاملاً. خاصة للنزوح الزاوي. وبالنسبة للإزاحة على طول المحاور ، استخدمت هذا الخيار.

   const zLimit = (Math.abs(yawError) / 10);
        if (0 < zDistance && zSpeed < zLimit) {
            keyPress(_translateDown);
        } else if (zDistance < 0 && -1 < zSpeed) {
            keyPress(_translateUp);
        }

لا يتم عرض سرعة إزاحة السفينة بالنسبة لكل محور ، ولكن ليس من الصعب حسابها.

function carculateSpeed() {
    let yDistance = parseFloat($("#y-range .distance").outerText.split(' ')[0]);
    let zDistance = parseFloat($("#z-range .distance").outerText.split(' ')[0]);

    ySpeed = yPrev - yDistance;
    yPrev = yDistance;

    zSpeed = zPrev - zDistance;
    zPrev = zDistance;
    setTimeout(carculateSpeed, 1000);
}
carculateSpeed();

وتبين بشكل محتمل جدا. دائرة التحكم في التغذية المرتدة الكلاسيكية. وبينما كانت السفينة على مسافة من محطة الفضاء الدولية ، قمنا بالطيران بسلاسة تامة [كما بدا لي حينها]. لكن المشاكل بدأت بالقرب من السفينة نفسها. في الواقع ، كانت السفينة نقانق للغاية وكان من المستحيل ماديًا الحصول على دقة 0.2 متر. الحقيقة هي أن اختلاط سفينتنا حصل ... دعنا نقول في الفضاء المستمر (بدقة كبيرة) ، لكننا رأينا فقط أعشار ذلك. وبالطبع ، في محاولة للرد عليهم كل 200 مللي ثانية ، حصلنا على إجراءات تنظيمية قوية جدًا. لقد قمنا بدس الأزرار عدة مرات بأدنى انحراف. وكلما اقتربنا من السفينة ، كلما زادت قيم الأوفست في القفز ، وهزنا السفينة أكثر من ذلك ... زيادة سعة حركتها ...

كان من الضروري في مكان ما لأخذ الدقة المفقودة. في التكرار الثاني في حل هذه المشكلة ، حاولت بنفسي أن أحسب بالفعل فقط على أساس سرعة الإزاحة. ونعم ، يبدو أنها تعمل بشكل جيد أيضًا ، ولكن هذا لم يحل مشكلة الحركة ...

ما هو جوهر مشكلة الحركة؟ حسنًا ، انظر ، نجد في الفضاء الخارجي ومن خلال النقر على أزرار التحكم نعطي تسريعًا للسفينة في طائرة أو أخرى. ولكن بمجرد تحرير الزر ، لا تتوقف الحركة. في الفضاء الخارجي ، بسبب الفراغ ، لا توجد مقاومة. وبمجرد أن قدمنا ​​دفعة (بالضغط على زر) ، بدأت السفينة في التحرك بهذه السرعة ... ويجب إيقافها بطريقة أو بأخرى. من السهل جدًا التوقف في المحاكي - تحتاج إلى إعطاء دفعة عكسية.

ولكن في التكرار الثاني للحل ، لم تعطيني الدقة المتزايدة للخطأ إجابة حول كيفية ضبط السرعة ...

وهنا كنا بحاجة إلى "بوصلة". يجب على قبطان أي سفينة / سفينة حساب الطريق مسبقًا. إذا أعطى الأمر الإبطاء بعد دخوله الميناء ، فمن غير المحتمل أن يربك المجوهرات. ونحن نحتاج هذا فقط.

نحن بحاجة إلى حساب الطريق ، وعادة ما يقوم القباطنة بذلك ببوصلة مع حالة منفصلة من نصائحها. وسنفعل نفس الشيء. سنحسب المسار لثانية أمامه ، والتي ستشمل خمسة تكرارات للضغط على الزر أم لا.

    if (index % 5 === 0) {
        calculatePath(roll, rollError);
        calculatePath(pitch, pitchError);
        calculatePath(yaw, yawError);
        calculatePath(y, yDistance);
        calculatePath(z, zDistance);
    }

تحسب دالة carculatePath ، بناءً على قيمة الانحراف الحالية ، 5 خطوات يجب أن تقلل من الناحية النظرية هذا الانحراف إلى 0. ليس بالضرورة في هذا التكرار ، ولكن في كل مرة يجب أن نقترب من الصفر في شبكتنا الأكثر تفصيلاً.

function calculatePath(data, value) {
    data.path = [];
    if (data.prev === value) {
        data.speed = 0;
    }
    for (let i = 0; i < 5; i++) {
        if (0 < value + data.speed * (i + 1)) {
            data.speed -= 0.1;
            data.path.push(-1);
        } else if (value + data.speed * (i + 1) < -0) {
            data.speed += 0.1;
            data.path.push(1);
        } else if (i > 0) {
            if (0 < data.speed) {
                data.speed -= 0.1;
                data.path.push(-1);
            } else if (data.speed < 0) {
                data.speed += 0.1;
                data.path.push(1);
            } else {
                data.path.push(0);
            }
        } else {
            data.path.push(0);
        }
    }
    data.prev = value;
}

هذا كل شيء ، نحن نحسب المسار كل ثانية "متساوية" (الفهرس٪ 5 === 0) والآن تحتاج فقط إلى الذهاب إلى هذه الدورة.


 let rollStep = roll.path[index % 5];
    if (0 < rollStep) {
        keyPress(_rollLeft);
    } else if (rollStep < 0) {
        keyPress(_rollRigth);
    }

    let pitchStep = pitch.path[index % 5];
    if (0 < pitchStep) {
        keyPress(_up);
    } else if (pitchStep < 0) {
        keyPress(_down);
    }

    let yawStep = yaw.path[index % 5];
    if (0 < yawStep) {
        keyPress(_left);
    } else if (yawStep < 0) {
        keyPress(_rigth);
    }

    let yStep = y.path[index % 5];
    if (0 < yStep) {
        keyPress(_translateRigth);
    } else if (yStep < 0) {
        keyPress(_translateLeft);
    }

    let zStep = z.path[index % 5];
    if (0 < zStep) {
        keyPress(_translateUp);
    } else if (zStep < 0) {
        keyPress(_translateDown);
    }

الحساب الوحيد الذي نجا منذ التكرار الأول هو الاقتراب من السفينة.

هذا كل شيء ، حسنًا ، نحن نمضي قدمًا بسرعة منخفضة نسبيًا

    const rangeLimit = Math.min(Math.max((Math.abs(range) / 100), 0.05), 2);
    if (-rate < rangeLimit) {
        keyPress(_accelerator);
    } else if (-rangeLimit < -rate) {
        keyPress(_brake);
    }

تحت المفسد هو الرمز الكامل. يمكنك التحقق من أدائها بنفسك على الموقع الإلكتروني iss-sim.spacex.com

كود كامل
function simulateKey(keyCode, type, modifiers) {
    var evtName = (typeof (type) === "string") ? "key" + type : "keydown";
    var modifier = (typeof (modifiers) === "object") ? modifier : {};

    var event = document.createEvent("HTMLEvents");
    event.initEvent(evtName, true, false);
    event.keyCode = keyCode;

    for (var i in modifiers) {
        event[i] = modifiers[i];
    }
    document.dispatchEvent(event);
}

function keyPress(keyCode) {
    simulateKey(keyCode)
    setTimeout(() => simulateKey(keyCode, "up"), 15);
}

let _accelerator = 69;
let _brake = 81;
let _translateLeft = 65;
let _translateRigth = 68;
let _translateUp = 87;
let _translateDown = 83;

let _left = 37;
let _rigth = 39;
let _up = 38;
let _down = 40;

let _rollRigth = 105;
let _rollLeft = 103;

let index = 0;

roll = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

pitch = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

yaw = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

z = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

y = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

function calculatePath(data, value) {
    data.path = [];
    if (data.prev === value) {
        data.speed = 0;
    }
    for (let i = 0; i < 5; i++) {
        if (0 < value + data.speed * (i + 1)) {
            data.speed -= 0.1;
            data.path.push(-1);
        } else if (value + data.speed * (i + 1) < -0) {
            data.speed += 0.1;
            data.path.push(1);
        } else if (i > 0) {
            if (0 < data.speed) {
                data.speed -= 0.1;
                data.path.push(-1);
            } else if (data.speed < 0) {
                data.speed += 0.1;
                data.path.push(1);
            } else {
                data.path.push(0);
            }
        } else {
            data.path.push(0);
        }
    }
    data.prev = value;
}

function A() {
    let range = parseFloat($("#range .rate").textContent.split(' '));

    let yDistance = parseFloat($("#y-range .distance").textContent.split(' ')[0]);
    let zDistance = parseFloat($("#z-range .distance").textContent.split(' ')[0]);

    let rollError = parseFloat($("#roll .error").textContent);
    let pitchError = parseFloat($("#pitch .error").textContent);
    let yawError = parseFloat($("#yaw .error").textContent);

    let rate = parseFloat($("#rate .rate").textContent.split(' ')[0]);

    if (index % 5 === 0) {
        calculatePath(roll, rollError);
        calculatePath(pitch, pitchError);
        calculatePath(yaw, yawError);
        calculatePath(y, yDistance);
        calculatePath(z, zDistance);
    }

    let rollStep = roll.path[index % 5];
    if (0 < rollStep) {
        keyPress(_rollLeft);
    } else if (rollStep < 0) {
        keyPress(_rollRigth);
    }

    let pitchStep = pitch.path[index % 5];
    if (0 < pitchStep) {
        keyPress(_up);
    } else if (pitchStep < 0) {
        keyPress(_down);
    }

    let yawStep = yaw.path[index % 5];
    if (0 < yawStep) {
        keyPress(_left);
    } else if (yawStep < 0) {
        keyPress(_rigth);
    }

    let yStep = y.path[index % 5];
    if (0 < yStep) {
        keyPress(_translateRigth);
    } else if (yStep < 0) {
        keyPress(_translateLeft);
    }

    let zStep = z.path[index % 5];
    if (0 < zStep) {
        keyPress(_translateUp);
    } else if (zStep < 0) {
        keyPress(_translateDown);
    }

    const rangeLimit = Math.min(Math.max((Math.abs(range) / 100), 0.05), 2);
    if (-rate < rangeLimit) {
        keyPress(_accelerator);
    } else if (-rangeLimit < -rate) {
        keyPress(_brake);
    }

    index++;
    setTimeout(A, 200);
}

A();


في الواقع كل شيء. شكرا لكم جميعا على القراءة :)



ملاحظة


نعم ، لقد قمت بقليل من المرح و السخرية. لا تأخذ المقال على محمل الجد ، أردت فقط أن أشعر وكأنني رائد فضاء [أو مسافر فضاء] ، مثل يوري غاغارين ... على الرغم من أن هذا على الأرجح هو العمل الهندسي لسيرجي بافلوفيتش كوروليف ، الذي للأسف لم يقم بزيارة الفضاء الذي حلم به ...

All Articles