Added webserver

This commit is contained in:
Dennis Gunia
2025-10-19 19:51:30 +02:00
parent e02620c22a
commit 03840abca4
5 changed files with 56 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
events {}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
gzip on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 8087 default_server;
listen [::]:8087 default_server;
root /nas_projects/current/SplitFlapController/software/pc_client/nginx/www/web_gui;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
location /manage/ {
proxy_pass http://localhost:8089;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
}
}

12
software/pc_client/nginx/run.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# This file is part of SplitFlapController project.
# Copyright (C) 2024-2025 GuniaLabs (www.dennisgunia.de)
# Author: Dennis Gunia
#
# This program is licensed under the AGPLv3 license. You can find a copy
# of the license in the LICENSE file in the root directory of this
# project.
# Run the nginx server with the specified configuration file in background
sudo nginx -c $(pwd)/nginx.conf

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,224 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="css/pico.min.css">
<script type="text/javascript" src="js/sfc.js"></script>
<title>Hello world!</title>
</head>
<body>
<dialog id="dialog_change_calibration">
<article>
<h2>Change calibration</h2>
<section>
<form id="form_change_calibration">
<label>
New calibration data (default 1400):
<input id="form_change_calibration_data" name="calibration" type="number"
placeholder="Calibration Value" value="1400" />
</label>
</form>
<footer>
<button class="secondary btn_cancel">
Cancel
</button>
<button class="btn_confirm">Confirm</button>
</footer>
</article>
</dialog>
<dialog id="change_address">
<article>
<h2>Change address</h2>
<section>
<form id="form_change_calibration">
<label>
Current address:
<input id="form_change_address_old" name="addr_old" type="number" placeholder="Current Address"
value="0" />
New address:
<input id="form_change_address_new" name="addr_new" type="number" placeholder="New Address" />
</label>
</form>
<footer>
<button class="secondary btn_cancel">
Cancel
</button>
<button class="btn_confirm">Confirm</button>
</footer>
</article>
</dialog>
<dialog id="dialog_add_device">
<article>
<h2>Add device</h2>
<section>
<form id="form_change_calibration">
<label>
Device address:
<input id="form_add_device_addr" name="addr_old" type="number" placeholder="Current Address"
value="0" />
Device position:
<fieldset role="group">
<input id="form_add_device_x" name="x" type="x" placeholder="Position X" value="0" />
<input id="form_add_device_y" name="y" type="y" placeholder="Position Y" value="0" />
</fieldset>
</label>
</form>
<footer>
<button class="secondary btn_cancel">
Cancel
</button>
<button class="btn_confirm">Confirm</button>
</footer>
</article>
</dialog>
<main class="container">
<nav>
<ul>
<li><strong>SplitFlap</strong></li>
</ul>
<ul>
<li><a onclick="changeView('view_display')" class="secondary">Display</a></li>
<li>
<details class="dropdown">
<summary>
Configuration
</summary>
<ul dir="rtl">
<li><a onclick="changeView('view_conf_modules')">Modules</a></li>
<li><a href="#">Layout</a></li>
<li><a onclick="changeView('view_storage')">Storage</a></li>
</ul>
</details>
</li>
</ul>
</nav>
<div id="view_display"><!-- Main View : print -->
<h1>Display Something!</h1>
<form id="form_display">
<fieldset>
<label>
Text to display:
<input id="form_display_str" name="text" placeholder="Text" />
</label>
<label>
Position on display:
<fieldset role="group">
<input id="form_display_x" name="x" type="x" placeholder="Position X" value="0" />
<input id="form_display_y" name="y" type="y" placeholder="Position Y" value="0" />
</fieldset>
</label>
</fieldset>
<input type="button" onClick="btn_display()" value="Display" />
<input type="button" onClick="btn_clear()" value="Clear screen" />
</form>
</div>
<div id="view_conf_modules" style="display: none;">
<h1>Module config</h1>
<div role="group">
<button id="btn_refresh" onClick="load_module_conf()">Refresh</button>
</div>
<template id="module_list_template">
<details name="example" open>
<summary class="module_header"></summary>
<table>
<thead>
<tr>
<th scope="col">Property</th>
<th scope="col">Value</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Module ID</th>
<td>12,104</td>
<td></td>
</tr>
<tr>
<th scope="row">Address</th>
<td>4,880</td>
<td></td>
</tr>
<tr>
<th scope="row">Calibration</th>
<td>12,104</td>
<td><button>Set</button></td>
</tr>
<tr>
<th scope="row">Status</th>
<td>12,104</td>
<td></td>
</tr>
<tr>
<th scope="row">Rotations</th>
<td>12,104</td>
<td></td>
</tr>
<tr>
<th scope="row">Position</th>
<td>12,104</td>
<td></td>
</tr>
<tr>
<th scope="row">Motor On</th>
<td>12,104</td>
<td><input name="power" type="checkbox" role="switch" /></td>
</tr>
<tr>
<th scope="row">Voltage</th>
<td>12,104</td>
<td></td>
</tr>
<tr>
<th scope="row">Current flap</th>
<td>12,104</td>
<td></td>
</tr>
<tr>
<th scope="row">Error flags</th>
<td>-</td>
<td></td>
</tr>
</tbody>
</table>
<div role="group">
<button class="btn_reset">Reset</button>
<button class="btn_remove secondary pico-background-red-500">Remove</button>
</div>
</details>
<hr />
</template>
<div id="module_list"></div>
<div role="group">
<button class="btn_add" onclick="display_dialog_add_device()">Add Module</button>
<button class="btn_assign" onclick="display_dialog_change_address()">Assign Address</button>
</div>
</div>
<div id="view_storage" style="display: none;"><!-- Main View : print -->
<h1>Load/Save Config!</h1>
<div role="group">
<button class="btn_load" onclick="btn_load()">Load Config</button>
</div>
<div role="group">
<button class="btn_save" onclick="btn_save()">Safe Config</button>
</div>
</div>
</main>
</body>
</html>

View File

@@ -0,0 +1,255 @@
// Create WebSocket connection.
let hostname = location.host;
if (hostname === "") {
hostname = "localhost";
}
console.log(`Connecting to ws://${hostname}`);
const socket = new WebSocket(`ws://${hostname}/manage/`);
// Connection opened
socket.addEventListener("open", (event) => {
SFCCommandAsync({ "command": "dm_load" });
//changeView("view_storage");
});
socket.addEventListener("message", (event) => {
switch (last_command) {
case "dm_dump":
parse_module_conf(JSON.parse(event.data));
break;
}
});
function toAddressStr(address) {
const hexbase = address.toString(16).padStart(4, '0').toUpperCase();
return `0x${hexbase}`;
}
// Send command
function SFCCommandAsync(data) {
last_command = data.command;
socket.send(JSON.stringify(data));
}
function btn_display() {
const text = document.getElementById("form_display_str").value;
const x = Number(document.getElementById("form_display_x").value);
const y = Number(document.getElementById("form_display_y").value);
const msg = {
"command": "dm_print",
"string": text,
"x": x,
"y": y
}
SFCCommandAsync(msg);
}
function btn_clear() {
const msg = {
"command": "dm_clear",
}
SFCCommandAsync(msg);
}
function btn_reset_module(address) {
const msg = {
"command": "dr_reset",
"address": address
}
SFCCommandAsync(msg);
}
function changeView(id) {
document.getElementById('view_display').style.display = 'none';
document.getElementById('view_conf_modules').style.display = 'none';
document.getElementById('view_storage').style.display = 'none';
document.getElementById(id).style.display = 'block';
switch (id) {
case 'view_display':
break;
case 'view_conf_modules':
load_module_conf();
break;
}
}
let modules = [];
function load_module_conf() {
document.getElementById('btn_refresh').ariaBusy = 'true';
document.getElementById('btn_refresh').disabled = true;
SFCCommandAsync({ "command": "dm_dump" });
}
function parse_module_conf(data) {
if (data["devices"]) {
modules = data["devices"];
const tbody = document.querySelector("#module_list");
tbody.innerHTML = "";
for (let i = 0; i < modules.length; i++) {
const mod = modules[i];
const template = document.querySelector("#module_list_template");
const clone = template.content.cloneNode(true);
let summary = clone.querySelector("summary");
summary.textContent = `Module ID ${mod["id"]} : ${mod["status"]["device"]}`;
switch (mod["status"]["device"]) {
case 'ONLINE':
summary.classList.add("pico-color-jade-500");
break
case 'OFFLINE':
summary.classList.add("pico-color-red-500");
break;
}
// fill table
let td = clone.querySelectorAll("td");
td[0].textContent = mod["id"];
td[2].textContent = `${mod["address"]} (${toAddressStr(mod["address"])})`;
td[4].textContent = `${mod["calibration"]} (${toAddressStr(mod["calibration"])})`;
td[6].textContent = mod["status"]["device"];
td[8].textContent = mod["status"]["rotations"];
td[10].textContent = `${mod["position"]["x"]}, ${mod["position"]["y"]}`;
td[12].textContent = mod["status"]["power"];
td[14].textContent = `${Math.round((mod["status"]["voltage"]) * 100) / 100} V`;
td[16].textContent = `id: ${mod["flapID"]}, char: '${mod["flapChar"]}'`;
// prepare flags
let flags = [];
Object.keys(mod["status"]["flags"]).forEach(flag => {
if (mod["status"]["flags"][flag]) {
flags.push(flag);
}
});
if (flags.length > 0) {
td[18].textContent = flags.join(", ");
}
clone.querySelector(".btn_reset").onclick = function () {
btn_reset_module(mod["address"]);
};
clone.querySelector(".btn_remove").onclick = function () {
const msg = {
"command": "dm_remove",
"id": mod["id"],
}
SFCCommandAsync(msg);
setTimeout(function () {
load_module_conf();
}, 200);
};
td[13].querySelector("input").checked = mod["status"]["power"];
// define set calibration button
td[5].querySelector("button").onclick = function () {
const dialog = document.getElementById("dialog_change_calibration");
dialog.showModal();
const dialog_el = dialog.querySelector("article").querySelector("section").querySelector("footer");
dialog_el.querySelector(".btn_confirm").onclick = function () {
const msg = {
"command": "dr_setcalibration",
"address": mod["address"],
"calibration": Number(dialog.querySelector("#form_change_calibration_data").value)
}
SFCCommandAsync(msg);
setTimeout(function () {
btn_reset_module(mod["address"]);
load_module_conf();
}, 200);
dialog.close();
};
dialog_el.querySelector(".btn_cancel").onclick = function () {
dialog.close();
}
};
// define power button
td[13].querySelector("input").onclick = function () {
const msg = {
"command": "dr_power",
"address": mod["address"],
"power": td[13].querySelector("input").checked
}
SFCCommandAsync(msg);
setTimeout(function () {
load_module_conf();
}, 200);
}
tbody.appendChild(clone);
document.getElementById('btn_refresh').ariaBusy = 'false';
document.getElementById('btn_refresh').disabled = false;
}
}
console.log(modules);
}
function display_dialog_change_address() {
const dialog = document.getElementById("change_address");
dialog.showModal();
const dialog_el = dialog.querySelector("article").querySelector("section").querySelector("footer");
const addr_old = Number(dialog.querySelector("#form_change_address_old").value);
const addr_new = Number(dialog.querySelector("#form_change_address_new").value);
dialog_el.querySelector(".btn_confirm").onclick = function () {
const msg = {
"command": "dr_setaddress",
"address": addr_old,
"newaddress": addr_new,
}
SFCCommandAsync(msg);
setTimeout(function () {
btn_reset_module(addr_old);
load_module_conf();
}, 200);
dialog.close();
};
dialog_el.querySelector(".btn_cancel").onclick = function () {
dialog.close();
}
}
function display_dialog_add_device() {
const dialog = document.getElementById("dialog_add_device");
dialog.showModal();
const dialog_el = dialog.querySelector("article").querySelector("section").querySelector("footer");
dialog_el.querySelector(".btn_confirm").onclick = function () {
const msg = {
"command": "dm_register",
"address": Number(dialog.querySelector("#form_add_device_addr").value),
"x": Number(dialog.querySelector("#form_add_device_x").value),
"y": Number(dialog.querySelector("#form_add_device_y").value)
}
SFCCommandAsync(msg);
setTimeout(function () {
load_module_conf();
}, 200);
dialog.close();
};
dialog_el.querySelector(".btn_cancel").onclick = function () {
dialog.close();
}
}
function btn_save(){
const msg = {
"command": "dm_save",
}
SFCCommandAsync(msg);
}
function btn_load(){
const msg = {
"command": "dm_load",
}
SFCCommandAsync(msg);
}