Font End using Bulma and MQTT.js for functionality

This commit is contained in:
Pfeifer 2025-07-04 11:53:15 +02:00
parent d7b785e58c
commit b400e5be3c
5 changed files with 292 additions and 0 deletions

0
web/.gitkeep Normal file
View File

BIN
web/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

58
web/index.html Normal file
View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IDFC</title>
<link rel="icon" href="img/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
<script src="https://unpkg.com/mqtt@5.13.1/dist/mqtt.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="main.css">
<script src="main.js"></script>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title has-text-primary">The M5 - MQTT Project</h1>
<h2 class="subtitle has-text-primary-light">by Patrik, Hazel and Reinhold</h2>
</div>
<div class="container m-5">
<div class="panel">
<form id="tabs" class="panel-tabs is-justify-content-flex-start has-text-success-light">
<input type="radio" id="chart-tab" name="tabs" data-tab="chart" checked/>
<label for="chart-tab" class="m-3">Chart</label>
<input type="radio" id="value-tab" name="tabs" data-tab="values"/>
<label for="value-tab" class="m-3">Values</label>
<input type="radio" id="connection-tab" name="tabs" data-tab="connection"/>
<label for="connection-tab" class="m-3">Connection</label>
</form>
<div id="block" class="panel-block has-background-grey">
<div id="chart" class="container" data-tab="chart">
<canvas></canvas>
</div>
<div class="container" data-tab="values">
<div class="smart-grid">
<div id="values" class="grid p-6"></div>
</div>
</div>
<div class="container" data-tab="connection" style="display: none;">
<div class="fixed-grid has-2-cols">
<div id="connection-grid" class="grid pr-6 has-text-success-light has-text-weight-semibold">
<label for="connection-broker" class="cell mx-6">Broker:</label>
<input id="connection-broker" class="cell input is-primary"/>
<label for="connection-topic" class="cell mx-6">Topic:</label>
<input id="connection-topic" class="cell input is-primary"/>
<button id="connection-submit" class="button cell is-primary is-col-start-2">Submit</button>
</div>
</div>
<div>
</div>
</div>
</div>
</section>
<div id="toaster"></div>
</body>
</html>

66
web/main.css Normal file
View File

@ -0,0 +1,66 @@
input[type="radio"] {
display: none;
}
label {
padding-bottom: 7px;
}
input[type="radio"]:checked + label {
font-weight: bold;
padding-bottom: 5px;
border-bottom: 2px solid hsl(142, 52%, 96%);
}
.inline-grid {
display: inline-grid;
}
#connection-grid {
grid-template-columns: max-content auto;
}
#toaster {
position: absolute;
translate: -50%;
top: 10px;
left: 50%;
max-height: 50%;
overflow: hidden;
}
.toast {
--duration: 3s;
background-color: rgba(0, 209, 178, .5);
display: flex;
justify-content: center;
align-items: center;
padding: 2px 10px 3px;
margin: 3px;
border-radius: 10px;
min-width: 70px;
color: white;
opacity: 0;
max-height: 40px;
animation: blend var(--duration);
}
@keyframes blend {
0% {
opacity: 0;
max-height: 40px;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
90% {
max-height: 40px;
}
100% {
opacity: 0;
max-height: 0px;
}
}

168
web/main.js Normal file
View File

@ -0,0 +1,168 @@
//const url = 'mqtt://tplinkwifi.net';
let client;
window.addEventListener('DOMContentLoaded', (event) => {
const tabs = document.querySelector('#tabs');
tabs.addEventListener('change', (event) => {
if (event.target.type != 'radio') {
return;
}
switchTab();
});
const buttonSubmit = document.querySelector('#connection-submit');
buttonSubmit.addEventListener('click', (event) => {
if (client) {
client.end();
}
const broker = document.querySelector('#connection-broker').value;
const topic = document.querySelector('#connection-topic').value;
connect(broker, topic);
buttonSubmit.classList.add('is-loading');
});
// start
document.querySelector('#connection-broker').value = 'test.mosquitto.org:8081';
document.querySelector('#connection-topic').value = 'test/2';
document.querySelector('#connection-submit').click();
});
function createChart(data) {
const elm = document.querySelector('#chart');
while (elm.childElementCount > 0) {
elm.firstElementChild.remove();
}
const ctx = document.createElement('canvas');
elm.appendChild(ctx);
// generate labels
const labels = [];
for (i = 1; i <= data.length; i++) {
labels.push(i);
}
const gridColor = '#4f4f4f';
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
data: data,
borderColor: '#d1fff8'
}]
},
options: {
scales: {
x: {
grid: {
color: gridColor
},
ticks: {
color: gridColor
}
},
y: {
grid: {
color: gridColor
},
ticks: {
color: gridColor
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
function loadValues(data) {
let elm = document.querySelector('#values');
while (elm.childElementCount > 0) {
elm.firstElementChild.remove();
}
for (let i = 0; i < data.length; i++) {
const val = document.createElement('span');
val.innerText = data[i];
val.classList.add('is-align-self-center');
const cell = document.createElement('div');
cell.classList.add('cell', 'tag', 'has-background-dark');
cell.appendChild(val);
elm.appendChild(cell);
}
}
function switchTab() {
const buttons = tabs.querySelectorAll('input[type="radio"]');
const block = document.querySelector('#block');
for (let i = 0; i < buttons.length; i++) {
const name = buttons[i].getAttribute('data-tab');
const container = block.querySelector(`.container[data-tab="${name}"]`);
if (buttons[i].checked) {
container.style.setProperty('display', 'block');
} else {
container.style.setProperty('display', 'none');
}
}
}
function connect(broker, topic) {
client = mqtt.connect('wss://' + broker);
const options = { retain: false, qos: 1 };
const submitButton = document.querySelector('#connection-submit');
client.on('connect', () => {
if (client.connected) {
toast('connected');
submitButton.classList.remove('is-loading');
}
});
client.on('close', () => {
console.log('close');
toast('connection closed');
submitButton.classList.remove('is-loading');
});
client.on('error', (err) => {
console.error(err);
toast('error');
submitButton.classList.remove('is-loading');
});
client.on('message', (topic, message) => {
console.log(message);
toast('message received');
submitButton.classList.remove('is-loading');
const tab = document.querySelector('#chart-tab');
tab.checked = true;
switchTab();
createChart(message);
loadValues(message);
});
client.subscribe(topic, options);
}
function toast(message) {
const duration = 3;
const elm = document.createElement('p');
elm.innerText = message;
elm.classList.add('toast');
document.querySelector('#toaster').appendChild(elm);
elm.style.setProperty('--duration', duration + 's');
setTimeout(() => {
elm.remove();
}, duration * 1000);
}