commit 5820933da15b6cec0438e6b6a9ac4b9a68c05251 Author: grey Date: Thu Mar 6 18:52:49 2025 +0100 inital commit diff --git a/index.html b/index.html new file mode 100644 index 0000000..6866c00 --- /dev/null +++ b/index.html @@ -0,0 +1,47 @@ + + + + + + Werma Signal Controler + + + +

Werma Light Control

+ This is a small web interface to control the lights and buzzer of a Werma signal tower using the 640.840.00 connection module. It assumes that: Ring 1 is green, Ring 2 is yellow, Ring 3 is red, Ring 4 is buzzer. Ring 5 is assumed to be unconnected +
+

Buzzer

+

+ +
+ +
+

+
+

Lights

+ Auto applies changes + Ring 1 - Red:
+ Ring 2 - Yellow:
+ Ring 3 - Green:
+
+

+ 0 - Off
+ 1 - On
+ 2 - Pulse
+ 3 - Pulse (alternate) +

+
+

Connection control

+

+
+ +
+ + + + + \ No newline at end of file diff --git a/nxtRender.js b/nxtRender.js new file mode 100644 index 0000000..8e94560 --- /dev/null +++ b/nxtRender.js @@ -0,0 +1,341 @@ +let lampContainer = document.getElementById("leuchte"); +let fillContainer = document.getElementById("fill"); +let configInput = document.getElementById("config"); +let configLoadError = document.getElementById("config_error"); +let configOutput = document.getElementById("config_out"); +let ctrlPane = document.getElementById("ctrlPane"); +let serialErrorState = document.getElementById("serialState"); + +const warn = document.getElementById("configWarn"); + +// Inputs +let input_setup_top = document.getElementById("setup_top"); + +// Elements +const topElement = document.getElementById("module_top"); + +const config_ring1 = document.getElementById("ring1Confg"); +const config_ring2 = document.getElementById("ring2Confg"); +const config_ring3 = document.getElementById("ring3Confg"); +const config_ring4 = document.getElementById("ring4Confg"); +const config_ring5 = document.getElementById("ring5Confg"); + +const config_ring_list = [config_ring1, config_ring2, config_ring3, config_ring4, config_ring5]; + +// Serial port +let port; +const usbVendorId = 0x0403; + +// URLs + +const urls = { + rings: { + red: "res/images/licht_rot.jpg", + green: "res/images/licht_gruen.jpg", + blue: "res/images/licht_blau.jpg", + yellow: "res/images/licht_gelb.jpg", + white: "res/images/licht_weiss.jpg", + empty: "", + }, + extras: { + top: { + normal: "res/images/top.jpg", + buzzer: "res/images/licht_summer.jpg" + } + }, + selector: { + rings: { + red: "res/images/module_red.jpg", + green: "res/images/module_green.jpg", + blue: "res/images/module_blue.jpg", + yellow: "res/images/module_yellow.jpg", + white: "res/images/module_white.jpg", + empty: "res/images/no.png", + }, + extras: { + top: { + normal: "res/images/no.png", + buzzer: "res/images/module_sounder.jpg" + } + } + }, + ctrls: { + off: "res/images/buttons/Off.png", + on: "res/images/buttons/On.png", + alternate1: "res/images/buttons/Alternate1.png", + alternate2: "res/images/buttons/Alternate2.png" + } +} + +// Attach event listeners +input_setup_top.addEventListener("change", (event) => { + myConfig.extras.top = event.target.value; + applyConfig(); +}); + +let myConfig = { + rings: { + 0: "green", + 1: "yellow", + 2: "red", + 3: "empty", + 4: "empty", + }, + extras: { + top: "buzzer" + }, + states: { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0 + } +} + +function validateConfig() { + // Check if there is a "hole" (empty) in the rings + // We want to detect ELEMENT EMPTY ELEMENT + // If the stack is "openended", thats fine + // Iterate over keys + let wasEmpty = false; + for (let i = 0; i < Object.keys(myConfig.rings).length; i++) { + console.log("Checking ring ", i); + isNowEmpty = myConfig.rings[i] === "empty"; + if (wasEmpty && !isNowEmpty) { + warn.innerText = "Invalid config: Empty ring in the middle of the stack!"; + return false; + } + console.log("Was empty: ", wasEmpty, " is empty: ", isNowEmpty); + wasEmpty = myConfig.rings[i] === "empty"; + + } + + let amountOfPopulatedRings = 0; + for (let i = 0; i < Object.keys(myConfig.rings).length; i++) { + if (myConfig.rings[i] !== "empty") { + amountOfPopulatedRings++; + } + } + if(amountOfPopulatedRings === 0) { + warn.innerText = "Invalid config: No rings selected!"; + return false; + } + + if(amountOfPopulatedRings == 5 && myConfig.extras.top === "buzzer") { + warn.innerText = "Invalid config: Buzzer selected with all rings!"; + return false; + } + + warn.innerText = ""; + return true; +} + +function applyConfig() { + validateConfig(); + // Remove state from output + let configCopy = JSON.parse(JSON.stringify(myConfig)); + delete configCopy.states; + configOutput.innerText = JSON.stringify(configCopy); + topElement.src = urls.extras.top[myConfig.extras.top]; + + // Update radio buttons + for (let i = config_ring_list.length-1; i >= 0; i--) { + let currElm = config_ring_list[i]; + for (let j = currElm.children.length-1; j >= 0; j--) { + const child = currElm.children[j]; + if (child.type === "radio") { + if (child.value === myConfig.rings[i]) { + child.checked = true; + } + } + } + } + + // Populate lamp tower + // Iterate over rings keys + for (let i = Object.keys(myConfig.rings).length - 1; i >= 0; i--) { + const ring = myConfig.rings[i]; + let ringElm = document.getElementById("ring" + (i + 1)); + if (!ringElm) { + ringElm = document.createElement("img"); + ringElm.id = "ring" + (i + 1); + fillContainer.appendChild(ringElm); + } else { + ringElm.src = urls.rings[ring]; + } + + if(myConfig.states[i] == 0) { + ringElm.classList.remove("is-lit"); + ringElm.classList.remove("is-alternating-1"); + ringElm.classList.remove("is-alternating-2"); + } else if(myConfig.states[i] == 2) { + ringElm.classList.add("is-alternating-1"); + ringElm.classList.remove("is-alternating-2"); + ringElm.classList.remove("is-lit"); + }else{ + ringElm.classList.add("is-lit"); + ringElm.classList.remove("is-alternating-1"); + ringElm.classList.remove("is-alternating-2"); + } + } + setTimeout(() => { + // Add divs for the controll elements in the same height as the rings + // But only show for the visible rings + for (let i = 0; i < Object.keys(myConfig.rings).length; i++) { + // Get top and bottom y position of the ring from html element + + const ring = myConfig.rings[i]; + let ringElm = document.getElementById("ring" + (i + 1)); + if (ring === "empty") { + continue; + } + console.log("Ring ", i, " is ", ring); + // output coords + console.log("Ring ", i, " coords: ", ringElm.getBoundingClientRect()); + console.log(ringElm) + let ctrlElm = document.getElementById("ctrl" + (i + 1)); + if (!ctrlElm) { + ctrlElm = document.createElement("div"); + ctrlElm.id = "ctrl" + (i + 1); + ctrlElm.classList.add("ctrl"); + ctrlPane.appendChild(ctrlElm); + } + // Set position by top and bottom of the ring (ignore left and right) + ctrlElm.style.position = "absolute"; + ctrlElm.style.top = ringElm.getBoundingClientRect().top + "px"; + ctrlElm.style.bottom = ringElm.getBoundingClientRect().bottom + "px"; + ctrlElm.style.width = "250px"; + + // Add buttons for the modes (off, on, alternate1, alternate2) + let actionList = ["off", "on", "alternate1", "alternate2"]; + for (let j = 0; j < actionList.length; j++) { + let action = actionList[j]; + let actionElm = document.getElementById("action" + (i + 1) + action); + if (!actionElm) { + actionElm = document.createElement("button"); + actionElm.id = "action" + (i + 1) + action; + // actionElm.innerText = action; + actionElm.style.backgroundImage = `url(${urls.ctrls[action]})`; + actionElm.style.backgroundSize = "contain"; + actionElm.style.backgroundRepeat = "no-repeat"; + actionElm.classList.add("ctrl-action"); + actionElm.addEventListener("click", () => { + writeAction(i, action); + }); + ctrlElm.appendChild(actionElm); + } + } + + + + + // ctrlElm.style.left = "0px"; + // ctrlElm.style.right = "0px"; + } +}, 200); + +} + +function writeAction(ring, state){ + actionToState = { + "off": 0, + "on": 1, + "alternate1": 2, + "alternate2": 3 + } + myConfig.states[ring] = actionToState[state]; + applyConfig(); + serial_send(); +} + +function loadConfig() { + try { + let config = JSON.parse(configInput.value); + // Merge configs, as states is not part of the input + config.states = myConfig.states; + myConfig = config; + applyConfig(); + } catch (error) { + console.error("Error loading config: ", error); + configLoadError.innerText = "Error loading config: " + error; + } finally { + configLoadError.innerText = "Config loaded!"; + } + + input_setup_top.value = myConfig.extras.top; + +} + +function serial_check_env() { + if (!navigator.serial) { + console.error("Serial not supported!"); + serialErrorState.innerText = "Serial not supported in this browser! Try using something chromium based!"; + return false; + } + return true; +} + +async function serial_connect() { + if (!serial_check_env()) { + return; + } + navigator.serial.requestPort({ filters: [{ usbVendorId }] }).then((serialPort) => { + serialPort.open({ baudRate: 9600 }); + port = serialPort; + serialErrorState.innerText = "Connected to serial port!"; + }).catch((error) => { + console.error("Error connecting to serial port: ", error); + serialErrorState.innerText = "Error connecting to serial port: " + error; + }); +} + +async function serial_send() { + if(port && port.writable) { + let data = `WR${myConfig.states[0]}${myConfig.states[1]}${myConfig.states[2]}${myConfig.states[3]}${myConfig.states[4]}\r`; + const encoder = new TextEncoder(); + const writer = port.writable.getWriter(); + await writer.write(encoder.encode(data)); + writer.releaseLock(); + } +} + +// // Populate config section +// // From urls->selector->rings +// // Show as radio buttons +for (let i = config_ring_list.length-1; i >= 0; i--) { + let currElm = config_ring_list[i]; + for (const key in urls.selector.rings) { + if (urls.selector.rings.hasOwnProperty(key)) { + const url = urls.selector.rings[key]; + let radio = document.createElement("input"); + radio.type = "radio"; + radio.name = "ring" + (i + 1); + radio.value = key; + radio.id = "ring" + (i + 1) + key; + radio.addEventListener("change", (event) => { + myConfig.rings[i] = event.target.value; + applyConfig(); + }); + currElm.appendChild(radio); + + let img = document.createElement("img"); + img.src = url; + img.addEventListener("click", () => { + radio.checked = true; + myConfig.rings[i] = key; + applyConfig(); + } + ); + currElm.appendChild(img); + } + } +} + + + +applyConfig(); +setTimeout(() => { + applyConfig(); +} +, 200); diff --git a/res/images/base_1.jpg b/res/images/base_1.jpg new file mode 100644 index 0000000..c062ebd Binary files /dev/null and b/res/images/base_1.jpg differ diff --git a/res/images/base_2.jpg b/res/images/base_2.jpg new file mode 100644 index 0000000..689e5e1 Binary files /dev/null and b/res/images/base_2.jpg differ diff --git a/res/images/buttons/Alternate1.png b/res/images/buttons/Alternate1.png new file mode 100644 index 0000000..bc97435 Binary files /dev/null and b/res/images/buttons/Alternate1.png differ diff --git a/res/images/buttons/Alternate2.png b/res/images/buttons/Alternate2.png new file mode 100644 index 0000000..8842c06 Binary files /dev/null and b/res/images/buttons/Alternate2.png differ diff --git a/res/images/buttons/Off.png b/res/images/buttons/Off.png new file mode 100644 index 0000000..3aea529 Binary files /dev/null and b/res/images/buttons/Off.png differ diff --git a/res/images/buttons/On.png b/res/images/buttons/On.png new file mode 100644 index 0000000..84f4d73 Binary files /dev/null and b/res/images/buttons/On.png differ diff --git a/res/images/licht_blau.jpg b/res/images/licht_blau.jpg new file mode 100644 index 0000000..36a6e12 Binary files /dev/null and b/res/images/licht_blau.jpg differ diff --git a/res/images/licht_gelb.jpg b/res/images/licht_gelb.jpg new file mode 100644 index 0000000..77b5e88 Binary files /dev/null and b/res/images/licht_gelb.jpg differ diff --git a/res/images/licht_gruen.jpg b/res/images/licht_gruen.jpg new file mode 100644 index 0000000..c801c46 Binary files /dev/null and b/res/images/licht_gruen.jpg differ diff --git a/res/images/licht_rot.jpg b/res/images/licht_rot.jpg new file mode 100644 index 0000000..b89c586 Binary files /dev/null and b/res/images/licht_rot.jpg differ diff --git a/res/images/licht_summer.jpg b/res/images/licht_summer.jpg new file mode 100644 index 0000000..3acf2f4 Binary files /dev/null and b/res/images/licht_summer.jpg differ diff --git a/res/images/licht_weiss.jpg b/res/images/licht_weiss.jpg new file mode 100644 index 0000000..b4c86f0 Binary files /dev/null and b/res/images/licht_weiss.jpg differ diff --git a/res/images/module_blue.jpg b/res/images/module_blue.jpg new file mode 100644 index 0000000..6b45ec6 Binary files /dev/null and b/res/images/module_blue.jpg differ diff --git a/res/images/module_green.jpg b/res/images/module_green.jpg new file mode 100644 index 0000000..c41d6ba Binary files /dev/null and b/res/images/module_green.jpg differ diff --git a/res/images/module_red.jpg b/res/images/module_red.jpg new file mode 100644 index 0000000..ce7f000 Binary files /dev/null and b/res/images/module_red.jpg differ diff --git a/res/images/module_sounder.jpg b/res/images/module_sounder.jpg new file mode 100644 index 0000000..7003902 Binary files /dev/null and b/res/images/module_sounder.jpg differ diff --git a/res/images/module_white.jpg b/res/images/module_white.jpg new file mode 100644 index 0000000..122cfb6 Binary files /dev/null and b/res/images/module_white.jpg differ diff --git a/res/images/module_yellow.jpg b/res/images/module_yellow.jpg new file mode 100644 index 0000000..ecc982e Binary files /dev/null and b/res/images/module_yellow.jpg differ diff --git a/res/images/no.png b/res/images/no.png new file mode 100644 index 0000000..7564e66 Binary files /dev/null and b/res/images/no.png differ diff --git a/res/images/top.jpg b/res/images/top.jpg new file mode 100644 index 0000000..46163b8 Binary files /dev/null and b/res/images/top.jpg differ diff --git a/res/style.css b/res/style.css new file mode 100644 index 0000000..9789dd6 --- /dev/null +++ b/res/style.css @@ -0,0 +1,72 @@ +html { + font-family: Arial, sans-serif; +} + +.leftPreview { + width: 50%; + + float: left; + padding: 10px; +} + +#leuchte img { + padding: 0px; + margin: 0px; + vertical-align: middle; + box-sizing: border-box; +} + +#leuchte br { + padding: 0px; + margin: 0px; + height: 0px; +} + +.ringConf img { + width: 10%; +} + +#ctrlPane { + margin-top: "24px" !important; +} + +.ctrl { + background-color: transparent; + border: 0px solid black; + height: 82px; + /* Center items vertically */ + display: flex; + align-items: center; + +} + +.ctrl-action { + background-color: transparent; + border: 0px solid black; + height: 40px; + width: 40px; + padding-left: 10px; +} + +.ctrl-action:active { + background-color: #f0f0f0; + filter: brightness(50%); +} + +.is-lit { + /* Make image brighter */ + filter: brightness(120%); +} + + +/* Blinking anim (no fading) */ +@keyframes blinker { + 0% { filter: brightness(120%); } + 50% { filter: brightness(100%); } + 100% { filter: brightness(120%); } +} + +.is-alternating-1 { + animation: blinker 1s linear infinite; +} + diff --git a/script.js b/script.js new file mode 100644 index 0000000..47cddb0 --- /dev/null +++ b/script.js @@ -0,0 +1,76 @@ +// Connect to serial port with 9600 baudrate +// everything in the browser + +lightState = { + red: 0, + yellow: 0, + green: 0, + buzzer: 0, +}; + +let light1Inp = document.getElementById("light1"); +let light2Inp = document.getElementById("light2"); +let light3Inp = document.getElementById("light3"); + +light1Inp.addEventListener("change", (event) => { + lightState.red = event.target.value; + sendLightData(); +}); + +light2Inp.addEventListener("change", (event) => { + lightState.yellow = event.target.value; + sendLightData(); +}); + +light3Inp.addEventListener("change", (event) => { + lightState.green = event.target.value; + sendLightData(); +}); + +async function connectToLight() { + // usbVendorId: 0x0403, usbProductId: 0x6015 + const usbVendorId = 0x0403; + port = await navigator.serial.requestPort({ filters: [{ usbVendorId }] }) + await port.open({ baudRate: 9600 /* pick your baud rate */ }); +} + +function setBuzz(value) { + lightState.buzzer = value; + sendLightData(); +} + +async function sendLightData() { + // WR12345 1: green, 2: yellow, 3: red, 4: buzzer, 5: empty (alsways 0) + let data = `WR${lightState.green}${lightState.yellow}${lightState.red}${lightState.buzzer}0\r`; + console.log("Sending data: ", data); + const encoder = new TextEncoder(); + const writer = port.writable.getWriter(); + await writer.write(encoder.encode(data)); + writer.releaseLock(); + readData(); +} + +async function readData() { + console.log("Reading data"); + while (port.readable) { + const reader = port.readable.getReader(); + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + // |reader| has been canceled. + break; + } + // Do something with |value|… + // Convert value to string + const decoder = new TextDecoder(); + console.log(decoder.decode(value)); + } + } catch (error) { + // Handle |error|… + } finally { + reader.releaseLock(); + } + } + console.log("Done reading"); +} diff --git a/scriptV2.js b/scriptV2.js new file mode 100644 index 0000000..a9e1934 --- /dev/null +++ b/scriptV2.js @@ -0,0 +1,90 @@ +// Connect to serial port with 9600 baudrate +// everything in the browser + +lightState = { + ring1: 0, + ring2: 0, + ring3: 0, + ring4: 0, + ring5: 0 + }; + + let light1Inp = document.getElementById("light1"); + let light2Inp = document.getElementById("light2"); + let light3Inp = document.getElementById("light3"); + let light4Inp = document.getElementById("light4"); + let light5Inp = document.getElementById("light5"); + + light1Inp.addEventListener("change", (event) => { + lightState.ring1 = event.target.value; + sendLightData(); + }); + + light2Inp.addEventListener("change", (event) => { + lightState.ring2 = event.target.value; + sendLightData(); + }); + + light3Inp.addEventListener("change", (event) => { + lightState.ring3 = event.target.value; + sendLightData(); + }); + + light4Inp.addEventListener("change", (event) => { + lightState.ring4 = event.target.value; + sendLightData(); + }); + + light5Inp.addEventListener("change", (event) => { + lightState.ring5 = event.target.value; + sendLightData(); + }); + + async function connectToLight() { + // usbVendorId: 0x0403, usbProductId: 0x6015 + const usbVendorId = 0x0403; + port = await navigator.serial.requestPort({ filters: [{ usbVendorId }] }) + await port.open({ baudRate: 9600 }); + } + + function setBuzz(value) { + lightState.buzzer = value; + sendLightData(); + } + + async function sendLightData() { + // WR12345 1: green, 2: yellow, 3: red, 4: buzzer, 5: empty (alsways 0) + let data = `WR${lightState.ring1}${lightState.ring2}${lightState.ring3}${lightState.ring4}${lightState.ring5}\r`; + console.log("Sending data: ", data); + const encoder = new TextEncoder(); + const writer = port.writable.getWriter(); + await writer.write(encoder.encode(data)); + writer.releaseLock(); + readData(); + } + + async function readData() { + console.log("Reading data"); + while (port.readable) { + const reader = port.readable.getReader(); + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + // |reader| has been canceled. + break; + } + // Do something with |value|… + // Convert value to string + const decoder = new TextDecoder(); + console.log(decoder.decode(value)); + } + } catch (error) { + // Handle |error|… + } finally { + reader.releaseLock(); + } + } + console.log("Done reading"); + } + \ No newline at end of file diff --git a/v2.html b/v2.html new file mode 100644 index 0000000..4142614 --- /dev/null +++ b/v2.html @@ -0,0 +1,112 @@ + + + + + + Werma Signal Controller + + + +

Werma Light Control

+ This is a small web interface which allows you to controll a Werma signal tower using the 640.840.00 connection module. + You can configure the web interface to match your tower's configuration. The default configuration is: Ring 1 is green, Ring 2 is yellow, Ring 3 is red, Ring 4 is buzzer. Ring 5 is assumed to be unconnected.
+ All "product photos" are the property of Werma, I did not take them myself. If you are Werma and want me to take down the images, reach out to me via the contact mail in the legal notes on my main domain. + +
Currently unsupported features
+
    +
  • Buzzers (somewhat, you cannot currently controll them)
  • +
  • Persistant configuration ("startup settings")
  • +
  • Any bidirectional communication (getting SW version, etc.)
  • +
+ + + +
+
+

Preview

+ +
+ +
+ +
+ + +
+
+ +
+

Control

+
+ +
+ + +
+
+
+ +
+
+

Serial

+ + + +
+
+

Setup

+ +
+ Load config from string + + +
+ +
+
+
+ Export current config + +
+
+
+				
+
+

Configure light

+ + + +
+
+ +
Ring 5
+
+ +
+
Ring 4
+
+ +
+
Ring 3
+
+ +
+
Ring 2
+
+ +
+
Ring 1
+
+ +
+
+ +
+
+
+ + + \ No newline at end of file