inital commit
47
index.html
Normal file
@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Werma Signal Controler</title>
|
||||
<style>
|
||||
html {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Werma Light Control</h1>
|
||||
This is a small web interface to control the lights and buzzer of a Werma signal tower using the <a href="https://www.werma.com/de/s_c4i595/USB-Anschlusselement_RM_5VDC_BK/64084000.html">640.840.00 connection module</a>. 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
|
||||
<section>
|
||||
<h2>Buzzer</h2>
|
||||
<p>
|
||||
<button onclick="setBuzz(0)">Off</button>
|
||||
<button onclick="setBuzz(1)">On</button><br>
|
||||
<button onclick="setBuzz(2)">Pulse</button>
|
||||
<button onclick="setBuzz(3)">Pulse (alternate)</button><br>
|
||||
</p>
|
||||
<hr>
|
||||
<h2>Lights</h2>
|
||||
<i>Auto applies changes</i>
|
||||
Ring 1 - Red: <input type="num" id="light1" value="0" max="3"><br>
|
||||
Ring 2 - Yellow: <input type="num" id="light2" value="0" max="3"><br>
|
||||
Ring 3 - Green: <input type="num" id="light3" value="0" max="3"><br>
|
||||
<hr>
|
||||
<p>
|
||||
0 - Off<br>
|
||||
1 - On<br>
|
||||
2 - Pulse<br>
|
||||
3 - Pulse (alternate)
|
||||
</p>
|
||||
<hr>
|
||||
<h2>Connection control</h2>
|
||||
<button onclick="connectToLight()">Connect to device</button><br><br>
|
||||
<button onclick="sendLightData()">Send data to device</button><br>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- make three selector-->
|
||||
</body>
|
||||
<script src="script.js"></script>
|
||||
</html>
|
341
nxtRender.js
Normal file
@ -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);
|
BIN
res/images/base_1.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
res/images/base_2.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
res/images/buttons/Alternate1.png
Normal file
After Width: | Height: | Size: 919 B |
BIN
res/images/buttons/Alternate2.png
Normal file
After Width: | Height: | Size: 915 B |
BIN
res/images/buttons/Off.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
res/images/buttons/On.png
Normal file
After Width: | Height: | Size: 887 B |
BIN
res/images/licht_blau.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
res/images/licht_gelb.jpg
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
res/images/licht_gruen.jpg
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
res/images/licht_rot.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
res/images/licht_summer.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
res/images/licht_weiss.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
res/images/module_blue.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
res/images/module_green.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
res/images/module_red.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
res/images/module_sounder.jpg
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
res/images/module_white.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
res/images/module_yellow.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
res/images/no.png
Normal file
After Width: | Height: | Size: 973 B |
BIN
res/images/top.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
72
res/style.css
Normal file
@ -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;
|
||||
}
|
||||
|
76
script.js
Normal file
@ -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<CR> 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");
|
||||
}
|
90
scriptV2.js
Normal file
@ -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<CR> 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");
|
||||
}
|
||||
|
112
v2.html
Normal file
@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Werma Signal Controller</title>
|
||||
<link rel="stylesheet" href="res/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Werma Light Control</h1>
|
||||
This is a small web interface which allows you to controll a Werma signal tower using the <a href="https://www.werma.com/USB-Anschlusselement-RM-5VDC-BK/64084000">640.840.00 connection module</a>.
|
||||
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.<br>
|
||||
<i>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.</i>
|
||||
|
||||
<h5>Currently unsupported features</h5>
|
||||
<ul>
|
||||
<li>Buzzers (somewhat, you cannot currently controll them)</li>
|
||||
<li>Persistant configuration ("startup settings")</li>
|
||||
<li>Any bidirectional communication (getting SW version, etc.)</li>
|
||||
</ul>
|
||||
|
||||
<!-- 1/4 of screen is a preview, rest is controll surface-->
|
||||
|
||||
<section style="display: flex; flex-direction: row;">
|
||||
<div style="width: 25%; max-width: 290px;">
|
||||
<h2>Preview</h2>
|
||||
<!-- Images are placed above each other -->
|
||||
<div id="leuchte">
|
||||
<img src="res/images/top.jpg" id="module_top">
|
||||
<div id="fill">
|
||||
|
||||
</div>
|
||||
<img src="res/images/base_2.jpg" id="base_2">
|
||||
<img src="res/images/base_1.jpg" id="base_1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="width: 25%; max-width: 290px;">
|
||||
<h2>Control</h2>
|
||||
<div id="ctrlPane">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div style="width: 75%;">
|
||||
<div id="serialState">
|
||||
|
||||
</div>
|
||||
<section>
|
||||
<h2>Serial</h2>
|
||||
<!-- Serial connection -->
|
||||
<button onclick="serial_connect()">Connect</button>
|
||||
<button onclick="serial_send()">Force refresh</button>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Setup</h2>
|
||||
<!-- Load old config -->
|
||||
<details>
|
||||
<summary>Load config from string</summary>
|
||||
<textarea id="config" style="width: 100%; height: 100px;"></textarea>
|
||||
<button onclick="loadConfig()">Load</button>
|
||||
<div id="config_error">
|
||||
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Export current config</summary>
|
||||
<textarea id="config_out" style="width: 100%; height: 100px;" readonly></textarea>
|
||||
</details>
|
||||
<pre id="configWarn">
|
||||
|
||||
</pre>
|
||||
<hr>
|
||||
<h3>Configure light</h3>
|
||||
<!-- Select top -->
|
||||
<label for="setup_top">Top:</label>
|
||||
<select id="setup_top" name="setup_top">
|
||||
<option value="buzzer">Buzzer</option>
|
||||
<option value="normal">Normal</option>
|
||||
</select>
|
||||
<br>
|
||||
<div class="ringConf">
|
||||
<!-- All 5 rings (setup) -->
|
||||
<h5>Ring 5</h5>
|
||||
<div id="ring5Confg">
|
||||
|
||||
</div>
|
||||
<h5>Ring 4</h5>
|
||||
<div id="ring4Confg">
|
||||
|
||||
</div>
|
||||
<h5>Ring 3</h5>
|
||||
<div id="ring3Confg">
|
||||
|
||||
</div>
|
||||
<h5>Ring 2</h5>
|
||||
<div id="ring2Confg">
|
||||
|
||||
</div>
|
||||
<h5>Ring 1</h5>
|
||||
<div id="ring1Confg">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<script src="nxtRender.js"></script>
|
||||
</body>
|
||||
</html>
|