+
+
+
+
+
+
diff --git a/web/main.css b/web/main.css
new file mode 100644
index 0000000..963c5ce
--- /dev/null
+++ b/web/main.css
@@ -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;
+ }
+}
diff --git a/web/main.js b/web/main.js
new file mode 100644
index 0000000..1c2c82c
--- /dev/null
+++ b/web/main.js
@@ -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);
+}