UNPKG

webdsky

Version:

Emulation of the DSKY user interface of the Apollo Guidance Computer for the Web platform

381 lines (332 loc) 12.1 kB
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-FileCopyrightText: Copyright 2020 Michael Karl Franzl <public.michael@franzl.name> import './digits/digit.js'; import './digits/sign.js'; import createChild from './lib/create_child.js'; /* * Element naming follows the prints at * https://archive.org/stream/acelectroniclmma00acel_0#page/n344/mode/1up */ class DskyInterface extends HTMLElement { constructor() { super(); this.state = { sign1m: 0, sign1p: 0, sign2m: 0, sign2p: 0, sign3m: 0, sign3p: 0, }; this.rootEl = this.attachShadow({ mode: 'closed' }); const styleHref = new URL('./webDSKY.css', import.meta.url).href; createChild(this.rootEl, 'link', { attributes: { rel: 'stylesheet', href: styleHref } }); // Pre-fetch assets to reduce flashing of unstyled content. // Pre-fetching using the link HTML tag doesn't seem to work from web components, // so let's just use fetch(). fetch(styleHref); fetch(new URL('./LM11-co46-dsky-bg.png', import.meta.url).href); fetch(new URL('./LM11-co46-dsky-overlay.png', import.meta.url).href); fetch(new URL('./digits/digit.svg', import.meta.url).href); fetch(new URL('./digits/sign.svg', import.meta.url).href); } connectedCallback() { const iface = createChild(this.rootEl, 'div', { classes: ['interface'] }); createChild(iface, 'div', { classes: ['overlay'] }); createChild(iface, 'div', { classes: ['digitContainer', 'blur'] }); const digitContainer = createChild(iface, 'div', { classes: ['digitContainer'] }); this.elements = { ds1: {}, // right side ds2: {}, // left side keys: {}, }; // create 3 rows of 5 digits each for (let row = 1; row <= 3; row++) { const div = createChild(digitContainer, 'div', { classes: ['digitRow', `row${row}`] }); createChild(div, 'div', { classes: ['separator'] }); this.elements.ds1[`${row}sign`] = createChild(div, 'dsky-display-sign'); for (let digit = 5; digit >= 1; digit--) this.elements.ds1[`${row}${digit}`] = createChild(div, 'dsky-display-digit'); } // create verb, noun, prog digits for (const row of [4, 5, 7]) { const div = createChild(digitContainer, 'div', { classes: ['digitRow', `row${row}`] }); for (let digit = 1; digit <= 2; digit++) this.elements.ds1[`${row}${digit}`] = createChild(div, 'dsky-display-digit'); } // create verb, noun, prog backlights for (const id of ['prog', 'verb', 'noun']) createChild(digitContainer, 'div', { classes: [`${id}_bg`] }); // create COMP ACTY backlight this.elements.ds1.comp_acty = createChild(digitContainer, 'div', { classes: ['acty_bg'] }); this.elements.ds1.comp_acty.style.display = 'none'; // create 7 rows and 2 columns of alarm indicator lights const lampContainer = createChild(this.rootEl, 'div', { id: 'lamps' }); for (let row = 0; row < 7; row++) { for (let col = 0; col < 2; col++) { const id = `lamp${row}${col}`; this.elements.ds2[id] = createChild(lampContainer, 'div', { id, classes: ['lamp'] }); } } // Create keys. // p = plus, m = minus, o = proceed, e = enter, k = key release, c = clr, r = rset // v = verb, n = noun const keys = '0123456789pmvncpkero'; for (const key of keys) { const element = this.elements.keys[key] = createChild(this.rootEl, 'div', { id: `key_${key}`, classes: ['key'], }); element.setAttribute('char', key); element.addEventListener('mousedown', (event) => { const char = event.target.getAttribute('char'); if (char === 'o') this.dispatchEvent(new CustomEvent('proceed', { detail: 0 })); else this.dispatchEvent(new CustomEvent('keypress', { detail: DskyInterface.keyToValue(char) })); }); element.addEventListener('mouseup', (event) => { const char = event.target.getAttribute('char'); if (char === 'o') this.dispatchEvent(new CustomEvent('proceed', { detail: 1 })); else this.dispatchEvent(new CustomEvent('keypress', { detail: 0 })); }); } } emit(port, value) { this.dispatchEvent(new CustomEvent('output', { detail: { port, value } })); } static keyToValue(button) { switch (button.toLowerCase()) { case '1': return 1; case '2': return 2; case '3': return 3; case '4': return 4; case '5': return 5; case '6': return 6; case '7': return 7; case '8': return 8; case '9': return 9; case '0': return 16; case 'v': return 17; // verb case 'r': return 18; // reset case 'k': return 25; // key rel case 'p': return 26; case 'm': return 27; case 'e': return 28; case 'c': return 30; // clear case 'n': return 31; // noun default: return null; } } attributeChangedCallback(attrName, oldVal, newVal) { if (attrName === 'data-input1') { const value = parseInt(newVal, 2); this.handleDigitUpdate(value); } else if (attrName === 'data-input2') { const value = parseInt(newVal, 2); this.handleLampsUpdate(value); } else if (attrName === 'data-keypress') { if (!oldVal && !newVal) return; if (!oldVal && newVal) { // emulates keydown const key = newVal.toLowerCase(); const element = this.elements.keys[key]; if (!element) return; element.dispatchEvent(new Event('mousedown')); element.classList.add('pressed'); } else if (oldVal && !newVal) { // emulates keyup const key = oldVal.toLowerCase(); const element = this.elements.keys[key]; if (!element) return; element.dispatchEvent(new Event('mouseup')); element.classList.remove('pressed'); } } } handleLampsUpdate(value) { // I don't know yet what this is used for. if (value & 1 << 0) console.log('AGC WARNING is on'); // Update COMP ACTY if (value & 1 << 1) this.elements.ds1.comp_acty.style.display = 'inherit'; else this.elements.ds1.comp_acty.style.display = 'none'; this.switchLamp('00', value & 1 << 2); // UPLINK ACTY this.switchLamp('01', value & 1 << 3); // TEMP this.switchLamp('30', value & 1 << 4); // KEY REL // Blink verb and noun digits (means: user input requested) if (value & 1 << 5) { this.elements.ds1['41'].classList.add('off'); this.elements.ds1['42'].classList.add('off'); this.elements.ds1['51'].classList.add('off'); this.elements.ds1['52'].classList.add('off'); } else { this.elements.ds1['41'].classList.remove('off'); this.elements.ds1['42'].classList.remove('off'); this.elements.ds1['51'].classList.remove('off'); this.elements.ds1['52'].classList.remove('off'); } this.switchLamp('40', value & 1 << 6); // OPR ERR this.switchLamp('31', value & 1 << 7); // RESTART this.switchLamp('20', value & 1 << 8); // STBY // I don't know yet what this is used for. if (value & 1 << 9) console.log('EL OFF is on'); } switchLamp(id, state) { const element = this.elements.ds2[`lamp${id}`]; if (state) element.classList.add('on'); else element.classList.remove('on'); } handleDigitUpdate(newVal) { const selector = newVal >> 11; /** * The following is a translation of table 4-C at * https://archive.org/stream/acelectroniclmma00acel_0#page/n352/mode/1up * * I found the table not to be fully correct. I corrected it empirically. */ const valueSign = (newVal >> 10) & 0x01; let id1; let id2; switch (selector) { case 1: id1 = '32'; id2 = '31'; this.state.sign3m = valueSign; break; case 2: id1 = '34'; id2 = '33'; this.state.sign3p = valueSign; break; case 3: id1 = '21'; id2 = '35'; break; case 4: id1 = '23'; id2 = '22'; this.state.sign2m = valueSign; break; case 5: id1 = '25'; id2 = '24'; this.state.sign2p = valueSign; break; case 6: id1 = '12'; id2 = '11'; this.state.sign1m = valueSign; break; case 7: id1 = '14'; id2 = '13'; this.state.sign1p = valueSign; break; case 8: id2 = '15'; break; case 9: // noun id1 = '51'; id2 = '52'; break; case 10: // verb id1 = '41'; id2 = '42'; break; case 11: // prog id1 = '71'; id2 = '72'; break; case 12: this.switchLamp('50', newVal & 1 << 0); // PRIO DISP this.switchLamp('60', newVal & 1 << 1); // NO DAP this.switchLamp('61', newVal & 1 << 2); // VEL this.switchLamp('10', newVal & 1 << 3); // NO ATT this.switchLamp('51', newVal & 1 << 4); // ALT this.switchLamp('11', newVal & 1 << 5); // GIMBAL LOCK this.switchLamp('41', newVal & 1 << 7); // TRACKER this.switchLamp('21', newVal & 1 << 8); // PROG break; default: break; } // Render right digit of selected pair if (id1) { const element = this.elements.ds1[id1]; const value = (newVal >> 5) & 0b00011111; const segments = DskyInterface.valueToSegmentLabels(value); element.setAttribute('segments', segments); } // Render right digit of selected pair if (id2) { const element = this.elements.ds1[id2]; // right element const value = newVal & 0b00011111; // right digit const segments = DskyInterface.valueToSegmentLabels(value); element.setAttribute('segments', segments); } // Render + or - sign for each row for (const row of [1, 2, 3]) { // Quoted from // https://www.ibiblio.org/apollo/developer.html#Packets_for_Socket_Implementation_of_AGS // // > for each sign bit, the most recent 1+ and 1- flags are saved. If both are 0, then the +/- // > sign is blank; if 1+ is set and 1- is not, then the '+' sign is displayed; if just the 1- // > flag is set, or if both 1+ and 1- flags are set, the '-' sign is displayed. let segments; if (!this.state[`sign${row}m`] && !this.state[`sign${row}p`]) segments = ''; else if (this.state[`sign${row}p`] && !this.state[`sign${row}m`]) segments = 'abc'; else segments = 'b'; this.elements.ds1[`${row}sign`].setAttribute('segments', segments); } } /* * The following implements more combinations than given in table 4-C1 of * https://archive.org/stream/acelectroniclmma00acel_0#page/n353/mode/1up * thanks to * https://www.ibiblio.org/apollo/developer.html */ static valueToSegmentLabels(value) { switch (value) { case 0: return ''; // blank case 1: return 'h'; case 2: return 'm'; case 3: return 'hm'; // 1 case 4: return 'fm'; case 5: return 'fhm'; case 6: return 'fm'; case 7: return 'fhm'; case 8: return 'j'; case 9: return 'jh'; case 10: return 'jm'; case 11: return 'jhm'; case 12: return 'fjm'; case 13: return 'fjhm'; // 4 case 14: return 'fjm'; case 15: return 'fjhm'; // 4 case 16: return 'ek'; case 17: return 'keh'; case 18: return 'em'; case 19: return 'ehm'; // 7 case 20: return 'efknm'; case 21: return 'ehmnkf'; // 0 case 22: return 'efmn'; case 23: return 'fehmn'; case 24: return 'ejkn'; case 25: return 'ehjkn'; // 2 case 26: return 'ejmn'; case 27: return 'ehjmn'; // 3 case 28: return 'efknmj'; // 6 case 29: return 'ehmnkfj'; // 8 case 30: return 'efjmn'; // 5 case 31: return 'efjhmn'; // 9 default: return null; } } static get observedAttributes() { return ['data-input1', 'data-input2', 'data-keypress']; } } customElements.define('dsky-interface', DskyInterface);