commit 35ce6de561d0de1450d075684506039ce3a96474 Author: Dorian Zedler Date: Sat Feb 18 11:53:06 2023 +0000 Update pages 🚀 diff --git a/.domains b/.domains new file mode 100644 index 0000000..37bdf48 --- /dev/null +++ b/.domains @@ -0,0 +1 @@ +ok-ready-go.speedclimbing.org \ No newline at end of file diff --git a/index.css b/index.css new file mode 100644 index 0000000..1c20522 --- /dev/null +++ b/index.css @@ -0,0 +1,79 @@ +@keyframes blinker { + 50% { + opacity: 0.3; + } + } + +.loading { + font-size: 2em; + font-weight: bold; + text-align: center; + line-height: 1.2; +} + +.timer { + font-size: 8em; + font-weight: bold; + text-align: center; + line-height: 1.2; +} + +.timer.over { + color: #0f0; + animation: blinker 2s ease infinite; +} + +.timer.sending { + color: #ff0; + animation: blinker 2s ease infinite; +} + +html { + height: 100%; +} + +body { + height: 100%; +} + +main { + padding-top: 0 !important; + height: 100%; +} + +.timer-container-div { + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.timer-div { + cursor: pointer; +} + +.tap-area { + width: 100vw; + height: 100vh; + position: absolute; + top: 0; + left: 0; + cursor: pointer; +} + +.hide-before-init { + display: none; +} + +.remote-connection-card { + position: absolute; + bottom: 10px; + left: 0; + background-color: var(--background-color); + width: 100%; +} + +details { + border-bottom: 0; + padding-bottom: 0; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..4ec1f8a --- /dev/null +++ b/index.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Initializing...

+
+
+
+
+
+
+

OK! .. READY! ... GO!

+

(connected)

+
+ +
+

+
+
+
+ +
+
+
+
+

If you want to automatically transfer the time to a computer, follow these steps:

+ +
+
    +
  • Enter a password and press tap connect
  • + +
  • +
    + + + Make sure, this is exactly the same on your computer! + +
    +
  • +
  • Download the receiver tool from here to your computer +
  • +
  • Start the receiver tool on your computer
  • +
  • Enter the same password on the receiver tool and click connect
  • +
  • Done!
  • +
+
+ +
+
    +
  • Download the receiver tool from here to your computer +
  • +
  • Start the receiver tool on your computer
  • +
  • Enter the password '' on the receiver tool and click connect
  • +
  • Done!
  • +
  • +

    If you no longer want to send the time, tap Disconnect

    + +
  • +
+
+
+ Remote connection +
+
+
+ +
+

Connecting...

+
+
+ \ No newline at end of file diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..18b1e45 --- /dev/null +++ b/js/index.js @@ -0,0 +1,72 @@ +async function sayNumber(number) { + console.log(number); + number = number.toString(); + number = number.replace(/[^0-9]/, ""); + for (let i = 0; i < number.length; i++) { + await playAudio(document.getElementById(`sound-${number[i]}`)); + } +} + +function playAudio(audio) { + return new Promise((res) => { + audio.play(); + audio.onended = res; + }); +} + +function uuidv4() { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => + ( + c ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))) + ).toString(16) + ); +} + +function PasswordForm() { + return { + formData: { + password: "", + }, + submitForm() { + Alpine.store("localState").password = this.formData.password; + }, + }; +} + +function Timer() { + return { + time: 0, + over: false, + init() { + setInterval(() => { + const startedAt = Alpine.store("localState").startedAt; + const resultTime = Alpine.store("localState").time; + + let time; + if (!startedAt && !resultTime) { + time = 0; + this.over = false; + } else if (resultTime) { + time = resultTime / 1000; + this.over = true; + } else { + this.over = false; + time = (new Date().getTime() - startedAt) / 1000; + } + + this.time = time.toFixed(2); + }, 10); + }, + }; +} + +const event = new Event('wasm-loaded'); +wasm_bindgen('lib/crypto_helper_bg.wasm').then(() => { + document.dispatchEvent(event); +}); + +document.addEventListener("alpine:init", () => { + document.getElementsByClassName("hide-after-init").item(0).outerHTML = "" + document.getElementsByClassName("hide-before-init").item(0).classList.remove("hide-before-init") +}); \ No newline at end of file diff --git a/js/localState.js b/js/localState.js new file mode 100644 index 0000000..5aeb23e --- /dev/null +++ b/js/localState.js @@ -0,0 +1,105 @@ +document.addEventListener("alpine:init", () => { + Alpine.store("localState", { + _state: 0, + // 0: idle + // 1: starting + // 2: running + // 3: sending time to mqtt + // 4: stopped + // 5: connecting to mqtt + + startedAt: null, + time: null, + stateHint: "", + password: null, + + init() { + Alpine.effect(() => { + if (this.password == null) { + this.password = localStorage.getItem("password"); + } else { + localStorage.setItem("password", this.password); + } + + const mqtt = Alpine.store("mqtt"); + if (!mqtt) return; + if (this.password == null || this.password == "") { + mqtt.disconnect(); + } else { + mqtt.connect(); + } + }); + + Alpine.effect(() => { + switch (this._state) { + case 0: + this.stateHint = "Tap here to start"; + break; + case 1: + this.stateHint = "Get ready..."; + break; + case 2: + this.stateHint = "Tap anywhere to stop"; + break; + case 3: + this.stateHint = "Sending time to MQTT..."; + break; + + case 4: + this.stateHint = "Tap here to reset"; + break; + } + }); + }, + + next() { + playAudio(document.getElementById("sound-silence")); + + if (this._state == 1 || this._state == 3) return; + + this._setState((this._state + 1) % 5); + }, + + _setState(state) { + switch (state) { + case 0: { + this.startedAt = null; + this.time = null; + break; + } + case 1: { + playAudio(document.getElementById("sound-ok-ready-go")).then(() => { + Alpine.store("localState")._setState(2); + }); + break; + } + case 2: { + this.startedAt = new Date().getTime() - 200; + break; + } + case 3: { + this.time = + Math.floor(((new Date().getTime() - this.startedAt) / 1000).toFixed(2) * 1000); + this.startedAt = null; + sayNumber(this.time / 10); + + if (Alpine.store("mqtt").connected) { + Alpine.store("mqtt") + .sendTime(this.time) + .then(() => { + Alpine.store("localState")._setState(4); + }); + break; + } + + state = 4; + break; + } + default: + break; + } + + this._state = state; + }, + }); +}); diff --git a/js/mqtt.js b/js/mqtt.js new file mode 100644 index 0000000..e354282 --- /dev/null +++ b/js/mqtt.js @@ -0,0 +1,132 @@ +let wasm_inited_resolve; +const wasm_inited = new Promise((resolve) => { + wasm_inited_resolve = resolve; +}); + +document.addEventListener("wasm-loaded", () => { + wasm_inited_resolve(wasm_bindgen); +}); + +document.addEventListener("alpine:init", () => { + Alpine.store("mqtt", { + connected: false, + _client: null, + _topics: null, + _c: null, + + _pendingPromises: {}, + + sendTime(time) { + if (!this.connected) return null; + + const id = uuidv4(); + const promise = new Promise((resolve, reject) => { + this._pendingPromises[id] = [resolve, reject]; + }); + this._publish({ + id: id, // used to prevent replay attacks and to identify confirm messages + kind: "Time", // can be "time" or "confirm" + time: time, // only used for "time" + }); + + return promise; + }, + + async connect() { + if (this.connected) return; + + const password = Alpine.store("localState").password; + const that = this; + const brokerDomain = "broker.emqx.io"; + const url = `wss://${brokerDomain}:8084/mqtt`; + + if (!password) return false; + + const { Crypto } = await wasm_inited; + + Alpine.store("localState")._state = 5; + + // derive key from password + this._c = new Crypto(password, brokerDomain); + + console.log("Test", this._encrypt("test")); + + this._topics = { + time: Crypto.sha256( + this._encrypt(`org.speedclimbing.ok-ready-go.${password}.time`) + ).toString(), + confirmation: Crypto.sha256( + this._encrypt( + `org.speedclimbing.ok-ready-go.${password}.confirmation` + ) + ).toString(), + }; + + console.log("Connecting to MQTT broker..."); + + const options = { + // Clean session + clean: true, + connectTimeout: 4000, + }; + + this._client = mqtt.connect(url, options); + + this._client.on("connect", () => { + Alpine.store("localState")._state = 0; + + that._client.subscribe(that._topics.confirmation); + this.connected = true; + }); + + this._client.on("message", (topic, message) => { + // message is Buffer + message = that._decrypt(message.toString()); + const data = JSON.parse(message); + + if ( + topic !== that._topics.confirmation || + data.kind !== "Confirm" || + Object.keys(this._pendingPromises).indexOf(data.id) === -1 + ) + return; + + console.log("<<< ", data); + this._pendingPromises[data.id][0](); + }); + }, + + disconnect() { + if (!this.connected) return; + + this._client.end(true); + + this._client = null; + this.connected = false; + this._topics = null; + + for (const promiseId in this._pendingPromises) { + this._pendingPromises[promiseId][1](); + } + + this._pendingPromises = {}; + }, + + _publish(data) { + const encryptedData = this._encrypt(JSON.stringify(data)); + console.log(">>> ", data); + this._client.publish(this._topics.time, encryptedData, { + qos: 1, + retain: false, + }); + }, + + _encrypt(data) { + return this._c.encrypt(data); + }, + + _decrypt(data) { + return this._c.decrypt(data); + }, + }); +}); diff --git a/lib/crypto_helper.js b/lib/crypto_helper.js new file mode 100644 index 0000000..b7ff657 --- /dev/null +++ b/lib/crypto_helper.js @@ -0,0 +1,329 @@ +let wasm_bindgen; +(function() { + const __exports = {}; + let script_src; + if (typeof document === 'undefined') { + script_src = location.href; + } else { + script_src = new URL(document.currentScript.src, location.href).toString(); + } + let wasm; + + const heap = new Array(128).fill(undefined); + + heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +let cachedUint8Memory0 = null; + +function getUint8Memory0() { + if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedInt32Memory0 = null; + +function getInt32Memory0() { + if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} +/** +*/ +__exports.greet = function() { + wasm.greet(); +}; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} +/** +*/ +class Crypto { + + static __wrap(ptr) { + const obj = Object.create(Crypto.prototype); + obj.ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_crypto_free(ptr); + } + /** + * @param {string} password + * @param {string} salt + */ + constructor(password, salt) { + const ptr0 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(salt, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.crypto_new(ptr0, len0, ptr1, len1); + return Crypto.__wrap(ret); + } + /** + * @param {string} input + * @returns {string} + */ + static sha256(input) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.crypto_sha256(retptr, ptr0, len0); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } + } + /** + * @param {string} plaintext + * @returns {string} + */ + encrypt(plaintext) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(plaintext, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.crypto_encrypt(retptr, this.ptr, ptr0, len0); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } + } + /** + * @param {string} ciphertext + * @returns {string} + */ + decrypt(ciphertext) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(ciphertext, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.crypto_decrypt(retptr, this.ptr, ptr0, len0); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } + } +} +__exports.Crypto = Crypto; + +async function load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function getImports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_alert_0cc0cb8b17d72dde = function(arg0, arg1) { + alert(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbg_new_abda76e883ba8a5f = function() { + const ret = new Error(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { + const ret = getObject(arg1).stack; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } + }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + + return imports; +} + +function initMemory(imports, maybe_memory) { + +} + +function finalizeInit(instance, module) { + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + cachedInt32Memory0 = null; + cachedUint8Memory0 = null; + + + return wasm; +} + +function initSync(module) { + const imports = getImports(); + + initMemory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return finalizeInit(instance, module); +} + +async function init(input) { + if (typeof input === 'undefined') { + input = script_src.replace(/\.js$/, '_bg.wasm'); + } + const imports = getImports(); + + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + + initMemory(imports); + + const { instance, module } = await load(await input, imports); + + return finalizeInit(instance, module); +} + +wasm_bindgen = Object.assign(init, { initSync }, __exports); + +})(); diff --git a/lib/crypto_helper_bg.wasm b/lib/crypto_helper_bg.wasm new file mode 100644 index 0000000..393265c Binary files /dev/null and b/lib/crypto_helper_bg.wasm differ diff --git a/sound/0.mp3 b/sound/0.mp3 new file mode 100644 index 0000000..7d1455e Binary files /dev/null and b/sound/0.mp3 differ diff --git a/sound/1.mp3 b/sound/1.mp3 new file mode 100644 index 0000000..b174a74 Binary files /dev/null and b/sound/1.mp3 differ diff --git a/sound/2.mp3 b/sound/2.mp3 new file mode 100644 index 0000000..812a778 Binary files /dev/null and b/sound/2.mp3 differ diff --git a/sound/3.mp3 b/sound/3.mp3 new file mode 100644 index 0000000..5818736 Binary files /dev/null and b/sound/3.mp3 differ diff --git a/sound/4.mp3 b/sound/4.mp3 new file mode 100644 index 0000000..7ced6a4 Binary files /dev/null and b/sound/4.mp3 differ diff --git a/sound/5.mp3 b/sound/5.mp3 new file mode 100644 index 0000000..4cf80ec Binary files /dev/null and b/sound/5.mp3 differ diff --git a/sound/6.mp3 b/sound/6.mp3 new file mode 100644 index 0000000..1f3005a Binary files /dev/null and b/sound/6.mp3 differ diff --git a/sound/7.mp3 b/sound/7.mp3 new file mode 100644 index 0000000..60c01b0 Binary files /dev/null and b/sound/7.mp3 differ diff --git a/sound/8.mp3 b/sound/8.mp3 new file mode 100644 index 0000000..5a1c53b Binary files /dev/null and b/sound/8.mp3 differ diff --git a/sound/9.mp3 b/sound/9.mp3 new file mode 100644 index 0000000..320079e Binary files /dev/null and b/sound/9.mp3 differ diff --git a/sound/ok-ready-go.mp3 b/sound/ok-ready-go.mp3 new file mode 100644 index 0000000..5e08287 Binary files /dev/null and b/sound/ok-ready-go.mp3 differ diff --git a/sound/silence.mp3 b/sound/silence.mp3 new file mode 100644 index 0000000..16de7c3 Binary files /dev/null and b/sound/silence.mp3 differ