mirror of
https://github.com/cesanta/mongoose.git
synced 2026-11-09 16:21:45 +00:00
MQTT dashboard nitpicks
This commit is contained in:
1 parent
79e138ef28
commit
bbd27a0a9c
6 files changed
+400
-387
No files matched your search
@@ -230,7 +230,7 @@ export function Setting(props) {
|
|||||||
if (props.type == 'select') input = SelectValue;
|
if (props.type == 'select') input = SelectValue;
|
||||||
return html`
|
return html`
|
||||||
<div class=${props.cls || 'grid grid-cols-2 gap-2 my-1'}>
|
<div class=${props.cls || 'grid grid-cols-2 gap-2 my-1'}>
|
||||||
<label class="flex items-center text-sm text-gray-700 mr-2 font-medium">${props.title}<//>
|
<label class="flex items-center text-sm text-gray-700 mr-2 font-medium ${props.title || 'hidden'}">${props.title}<//>
|
||||||
<div class="flex items-center">${h(input, props)}<//>
|
<div class="flex items-center">${h(input, props)}<//>
|
||||||
<//>`;
|
<//>`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter var,Helvetica,sans-serif;font-feature-settings:"cv11","ss01";font-variation-settings:"opsz" 32}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.right-4{right:1rem}.top-0{top:0}.top-4{top:1rem}.isolate{isolation:isolate}.z-10{z-index:10}.z-\[48\]{z-index:48}.m-4{margin:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-1{margin-bottom:.25rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-3{margin-top:.75rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-0{width:0}.w-11{width:2.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-96{width:24rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.flex-grow,.grow{flex-grow:1}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-5{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x:1.25rem}.translate-Line truncated
|
/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter var,Helvetica,sans-serif;font-feature-settings:"cv11","ss01";font-variation-settings:"opsz" 32}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.right-4{right:1rem}.top-0{top:0}.top-4{top:1rem}.isolate{isolation:isolate}.z-10{z-index:10}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mr-8{margin-right:2rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-2\.5{margin-top:.625rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-0{width:0}.w-11{width:2.75rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-96{width:24rem}.w-full{width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.flex-grow,.grow{flex-grow:1}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-5{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x:1.25rem}.translate-y-0{--tw-translate-y:0px}.transLine truncated
|
||||||
@@ -1,95 +1,70 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import { h, render, useState, useEffect, useRef, html } from './bundle.js';
|
import {h, html, render, useEffect, useRef, useState} from './bundle.js';
|
||||||
import { Icons, Setting, Button, tipColors, Colored, Notification} from './components.js';
|
import {Button, Colored, Icons, Notification, Setting, tipColors} from './components.js';
|
||||||
|
|
||||||
const Logo = props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.87 12.85"><defs><style>.ll-cls-1{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:0.5px;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="ll-cls-1" d="M12.62,1.82V8.91A1.58,1.58,0,0,1,11,10.48H4a1.44,1.44,0,0,1-1-.37A.69.69,0,0,1,2.84,10l-.1-.12a.81.81,0,0,1-.15-.48V5.57a.87.87,0,0,1,.86-.86H4.73V7.28a.86.86,0,0,0,.86.85H9.42a.85.85,0,0,0,.85-.85V3.45A.86.86,0,0,0,10.13,3,.76.76,0,0,0,10,2.84a.29.29,0,0,0-.12-.1,1.49,1.49,0,0,0-1-.37H2.39V1.82A1.57,1.57,0,0,1,4,.25H11A1.57,1.57,0,0,1,12.62,1.82Z"/><path class="ll-cls-1" d="M10.48,10.48V11A1.58,1.58,0,0,1,8.9,12.6H1.82A1.57,1.57,0,0,1,.25,11V3.94A1.57,1.57,0,0,1,1.82,2.37H8.9a1.49,1.49,0,0,1,1,.37l.12.1a.76.76,0,0,1,.11.14.86.86,0,0,1,.14.47V7.28a.85.85,0,0,1-.85.85H8.13V5.57a.86.86,0,0,0-.85-.86H3.45a.87.87,0,0,0-.86.86V9.4a.81.81,0,0,0,.15.48l.1.12a.69.69,0,0,0,.13.11,1.44,1.44,0,0,0,1,.37Z"/></g></g></svg>`;
|
const Logo = props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.87 12.85"><defs><style>.ll-cls-1{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:0.5px;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="ll-cls-1" d="M12.62,1.82V8.91A1.58,1.58,0,0,1,11,10.48H4a1.44,1.44,0,0,1-1-.37A.69.69,0,0,1,2.84,10l-.1-.12a.81.81,0,0,1-.15-.48V5.57a.87.87,0,0,1,.86-.86H4.73V7.28a.86.86,0,0,0,.86.85H9.42a.85.85,0,0,0,.85-.85V3.45A.86.86,0,0,0,10.13,3,.76.76,0,0,0,10,2.84a.29.29,0,0,0-.12-.1,1.49,1.49,0,0,0-1-.37H2.39V1.82A1.57,1.57,0,0,1,4,.25H11A1.57,1.57,0,0,1,12.62,1.82Z"/><path class="ll-cls-1" d="M10.48,10.48V11A1.58,1.58,0,0,1,8.9,12.6H1.82A1.57,1.57,0,0,1,.25,11V3.94A1.57,1.57,0,0,1,1.82,2.37H8.9a1.49,1.49,0,0,1,1,.37l.12.1a.76.76,0,0,1,.11.14.86.86,0,0,1,.14.47V7.28a.85.85,0,0,1-.85.85H8.13V5.57a.86.86,0,0,0-.85-.86H3.45a.87.87,0,0,0-.86.86V9.4a.81.81,0,0,0,.15.48l.1.12a.69.69,0,0,0,.13.11,1.44,1.44,0,0,0,1,.37Z"/></g></g></svg>`;
|
||||||
|
const DefaultTopic = 'mg_mqtt_dashboard';
|
||||||
|
const DefaultUrl = location.protocol == 'https:'
|
||||||
|
? 'wss://broker.hivemq.com:8443/mqtt'
|
||||||
|
: 'ws://broker.hivemq.com:8000/mqtt';
|
||||||
|
const DefaultDeviceConfig = {pins: [], log_level: 0};
|
||||||
|
let MqttClient;
|
||||||
|
|
||||||
const url = 'ws://broker.hivemq.com:8000/mqtt'
|
function Header({topic, setTopic, url, setUrl, connected}) {
|
||||||
const default_topic = 'topic_mg_device'
|
|
||||||
let client;
|
|
||||||
|
|
||||||
function Header( {topic, setTopicFn} ) {
|
|
||||||
const [inputValue, setInputValue] = useState(topic);
|
|
||||||
const [saveResult, setSaveResult] = useState(null);
|
|
||||||
const onInput = (ev) => setInputValue(ev.target.value);
|
|
||||||
const forbiddenChars = ['$', '*', '+', '#', '/'];
|
const forbiddenChars = ['$', '*', '+', '#', '/'];
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
const isValidTopic = (value) => {
|
const isValidTopic = val => !forbiddenChars.some(char => val.includes(char));
|
||||||
return !forbiddenChars.some(char => value.includes(char));
|
if (isValidTopic(topic)) {
|
||||||
};
|
localStorage.setItem('topic', topic)
|
||||||
|
setTopicFn(topic);
|
||||||
if (isValidTopic(inputValue)) {
|
|
||||||
localStorage.setItem('topic', inputValue)
|
|
||||||
setTopicFn(inputValue);
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
setSaveResult("Error: The topic cannot contain these characters: " + forbiddenChars)
|
setSaveResult('Error: The topic cannot contain these characters: ' + forbiddenChars);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="bg-white sticky top-0 z-[48] w-full border-b-2 border-gray-300 py-3 shadow-md transition-all duration-300 transform">
|
<div class="top-0 py-2 border-b-2 bg-stone-100">
|
||||||
<div class="mx-auto flex justify-between items-center px-5">
|
<div class="flex px-4">
|
||||||
<div class="text-gray-800 font-semibold tracking-wide">
|
<div class="flex grow gap-4">
|
||||||
<h1 class="text-2xl">MQTT Dashboard</h1>
|
<h1 class="text-2xl font-semibold">MQTT Dashboard</h1>
|
||||||
</div>
|
<small class="text-gray-500 mt-2.5">powered by Mongoose<//>
|
||||||
<div class="flex space-x-4">
|
<//>
|
||||||
<div class="text-lg font-medium text-gray-700 text-center">
|
<div class="flex gap-2 items-center">
|
||||||
<div class="text-gray-500">MQTT Server:</div>
|
<${Setting} value=${url} addonLeft="MQTT Server" cls="py-1 w-96" disabled=${connected} />
|
||||||
<code class="bg-gray-100 font-normal text-sm rounded w-full flex-1 py-0.5 px-2 text-gray-700" style="font-size: 0.8em; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 1px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 5px;">
|
<${Setting} value=${topic} addonLeft="Root Topic" cls="py-1" disabled=${connected} />
|
||||||
${url}
|
<${Button} icon=${Icons.link} onclick=${onClick} title=${connected ? 'Disconnect' : 'Connect'} />
|
||||||
</code>
|
<//>
|
||||||
</div>
|
<//>
|
||||||
<div style="border-left: 1px solid #ddd; height: 50px;"></div>
|
<//>`;
|
||||||
<div class="text-lg font-medium text-gray-700 text-center">
|
|
||||||
<div class="text-gray-500">Topic:</div>
|
|
||||||
<div class="flex w-full items-center rounded border shadow-sm bg-white">
|
|
||||||
<input type="text" value=${inputValue} onchange=${onInput} step="1" class="bg-white font-normal text-sm rounded w-full flex-1 py-0.5 px-2 text-gray-700 placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-1 mt-3 flex place-content-end"><${Button} icon=${Icons.save} onclick=${onClick} title="Change topic" /><//>
|
|
||||||
${saveResult && html`<${Notification} ok=${saveResult === "Success!"}
|
|
||||||
text=${saveResult} close=${() => setSaveResult(null)} />`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function Devices({ devices, onClickFn }) {
|
function Sidebar({devices, onclick}) {
|
||||||
const Td = props => html`
|
const Td = props => html`
|
||||||
<td class="whitespace-nowrap border-b border-slate-200 py-2 px-4 pr-3 text-sm text-slate-900">${props.text}</td>`;
|
<td class="whitespace-nowrap border-b border-slate-200 py-2 px-4 pr-3 text-sm text-slate-900">${
|
||||||
|
props.text}</td>`;
|
||||||
|
|
||||||
const Device = ({ d }) => html`
|
const Device = ({d}) => html`
|
||||||
<tr class="hover:bg-slate-100 cursor-pointer" onClick=${() => onClickFn(d.id)}>
|
<div class="hover:bg-stone-100 cursor-pointer flex gap-3 px-4 py-2 justify-between"
|
||||||
<td class="border-b-2 border-slate-300 py-2 text-center align-middle">
|
onclick=${ev => onclick(d.id)}>
|
||||||
<div class="flex flex-col items-center justify-center h-full">
|
<span>${d.id}</span>
|
||||||
<span>ID ${d.id}</span>
|
<${Colored}
|
||||||
<${Colored} colors=${d.online ? tipColors.green : tipColors.red} text=${d.online ? 'online' : 'offline'} />
|
colors=${d.online ? tipColors.green : tipColors.red}
|
||||||
</div>
|
text=${d.online ? 'online' : 'offline'} />
|
||||||
</td>
|
<//>`;
|
||||||
</tr>`;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="m-4 divide-y divide-gray-200 overflow-auto rounded bg-white">
|
<div class="overflow-auto divide-y border-r xbasis-60 w-60 flex-none">
|
||||||
<div class="font-semibold flex items-center text-gray-600 px-3 justify-center whitespace-nowrap">
|
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2 bg-stone-100">
|
||||||
<div class="font-bold flex items-center text-gray-600">
|
Devices
|
||||||
Devices list
|
|
||||||
</div>
|
|
||||||
<//>
|
<//>
|
||||||
<div class="inline-block min-w-full align-middle" style="max-height: 82vh; overflow: auto;">
|
${(devices ? devices : []).map(d => h(Device, {d}))}
|
||||||
<table class="w-full table table-bordered">
|
<//>`;
|
||||||
<tbody>
|
|
||||||
${(devices ? devices : []).map(d => h(Device, { d }))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function FirmwareStatus({title, info, children}) {
|
function FirmwareStatus({title, info, children}) {
|
||||||
const state = ['UNAVAILABLE', 'FIRST_BOOT', 'NOT_COMMITTED', 'COMMITTED'][(info.status || 0) % 4];
|
const state =
|
||||||
|
['UNAVAILABLE', 'FIRST_BOOT', 'NOT_COMMITTED',
|
||||||
|
'COMMITTED'][(info.status || 0) % 4];
|
||||||
const valid = info.status > 0;
|
const valid = info.status > 0;
|
||||||
return html`
|
return html`
|
||||||
<div class="bg-white divide-y border rounded">
|
<div class="bg-white divide-y border rounded">
|
||||||
@@ -100,7 +75,8 @@ function FirmwareStatus({title, info, children}) {
|
|||||||
<div class="my-1">Status: ${state}<//>
|
<div class="my-1">Status: ${state}<//>
|
||||||
<div class="my-1">CRC32: ${valid ? info.crc32.toString(16) : 'n/a'}<//>
|
<div class="my-1">CRC32: ${valid ? info.crc32.toString(16) : 'n/a'}<//>
|
||||||
<div class="my-1">Size: ${valid ? info.size : 'n/a'}<//>
|
<div class="my-1">Size: ${valid ? info.size : 'n/a'}<//>
|
||||||
<div class="my-1">Flashed at: ${valid ? new Date(info.timestamp * 1000).toLocaleString() : 'n/a'}<//>
|
<div class="my-1">Flashed at: ${
|
||||||
|
valid ? new Date(info.timestamp * 1000).toLocaleString() : 'n/a'}<//>
|
||||||
${children}
|
${children}
|
||||||
<//>
|
<//>
|
||||||
<//>`;
|
<//>`;
|
||||||
@@ -121,7 +97,9 @@ function UploadFileButton(props) {
|
|||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
const finish = ok => {
|
const finish = ok => {
|
||||||
setUpload(null);
|
setUpload(null);
|
||||||
const res = props.onupload ? props.onupload(ok, fileName, fileData.length) : null;
|
const res = props.onupload ?
|
||||||
|
props.onupload(ok, fileName, fileData.length) :
|
||||||
|
null;
|
||||||
if (res && typeof (res.catch) === 'function') {
|
if (res && typeof (res.catch) === 'function') {
|
||||||
res.catch(() => false).then(() => ok ? resolve() : reject());
|
res.catch(() => false).then(() => ok ? resolve() : reject());
|
||||||
} else {
|
} else {
|
||||||
@@ -132,27 +110,36 @@ function UploadFileButton(props) {
|
|||||||
const sendChunk = function(offset) {
|
const sendChunk = function(offset) {
|
||||||
var chunk = fileData.subarray(offset, offset + chunkSize) || '';
|
var chunk = fileData.subarray(offset, offset + chunkSize) || '';
|
||||||
var ok;
|
var ok;
|
||||||
setStatusByID('Uploading ' + fileName + ', bytes ' + offset + '..' +
|
setStatusByID(
|
||||||
(offset + chunk.length) + ' of ' + fileData.length, props.id);
|
'Uploading ' + fileName + ', bytes ' + offset + '..' +
|
||||||
const params = {chunk: btoa(String.fromCharCode.apply(null, chunk)), offset: offset, total: fileData.length}
|
(offset + chunk.length) + ' of ' + fileData.length,
|
||||||
props.publishFn("ota.upload", params)
|
props.id);
|
||||||
.then(function(res) {
|
const params = {
|
||||||
if (res.result === "ok" && chunk.length > 0) sendChunk(offset + chunk.length);
|
chunk: btoa(String.fromCharCode.apply(null, chunk)),
|
||||||
ok = res.result === "ok";
|
offset: offset,
|
||||||
return res;
|
total: fileData.length
|
||||||
})
|
};
|
||||||
.then(function(res) {
|
props.publishFn('ota.upload', params)
|
||||||
if (!ok) setStatusByID('Error: ' + res.error, props.id), finish(ok); // Fail
|
.then(function(res) {
|
||||||
if (chunk.length > 0) return; // More chunks to send
|
if (res.result === 'ok' && chunk.length > 0)
|
||||||
setStatus(x => x + '. Done, resetting device...');
|
sendChunk(offset + chunk.length);
|
||||||
finish(ok); // All chunks sent
|
ok = res.result === 'ok';
|
||||||
})
|
return res;
|
||||||
.catch(e => {
|
})
|
||||||
setStatusByID("Error: timed out", props.id);
|
.then(function(res) {
|
||||||
finish(false)
|
if (!ok)
|
||||||
});
|
setStatusByID('Error: ' + res.error, props.id),
|
||||||
|
finish(ok); // Fail
|
||||||
|
if (chunk.length > 0) return; // More chunks to send
|
||||||
|
setStatus(x => x + '. Done, resetting device...');
|
||||||
|
finish(ok); // All chunks sent
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
setStatusByID('Error: timed out', props.id);
|
||||||
|
finish(false)
|
||||||
|
});
|
||||||
};
|
};
|
||||||
//setFailed(false);
|
// setFailed(false);
|
||||||
sendChunk(0);
|
sendChunk(0);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -170,7 +157,8 @@ function UploadFileButton(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onclick = function(ev) {
|
const onclick = function(ev) {
|
||||||
let fn; setUpload(x => fn = x);
|
let fn;
|
||||||
|
setUpload(x => fn = x);
|
||||||
if (!fn) input.current.click(); // No upload in progress, show file dialog
|
if (!fn) input.current.click(); // No upload in progress, show file dialog
|
||||||
return fn;
|
return fn;
|
||||||
};
|
};
|
||||||
@@ -182,39 +170,53 @@ function UploadFileButton(props) {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="inline-flex flex-col ${props.class}">
|
<div class="inline-flex flex-col ${props.class}">
|
||||||
<input class="hidden" type="file" ref=${input} onchange=${onchange} accept=${props.accept} />
|
<input class="hidden" type="file" ref=${input} onchange=${onchange} accept=${
|
||||||
<${Button} title=${props.title} icon=${Icons.download} onclick=${onclick} ref=${btn} colors=${props.colors} disabled=${props.disabled} />
|
props.accept} />
|
||||||
<div class="py-2 text-sm text-slate-400 ${status || 'hidden'}">${status[props.id]}<//>
|
<${Button} title=${props.title} icon=${Icons.download} onclick=${
|
||||||
|
onclick} ref=${btn} colors=${props.colors} disabled=${props.disabled} />
|
||||||
|
<div class="py-2 text-sm text-slate-400 ${status || 'hidden'}">${
|
||||||
|
status[props.id]}<//>
|
||||||
<//>`;
|
<//>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FirmwareUpdate({ publishFn, disabled, info, deviceID }) {
|
function FirmwareUpdate({publishFn, disabled, info, deviceID}) {
|
||||||
const [clean, setClean] = useState(false)
|
const [clean, setClean] = useState(false)
|
||||||
const refresh = () => {};
|
const refresh = () => {};
|
||||||
useEffect(refresh, []);
|
useEffect(refresh, []);
|
||||||
const oncommit = ev => { publishFn("ota.commit") };
|
const oncommit = ev => {
|
||||||
const onreboot = ev => { publishFn("device.reset")};
|
publishFn('ota.commit')
|
||||||
const onrollback = ev => { publishFn("ota.rollback") };
|
};
|
||||||
|
const onreboot = ev => {
|
||||||
|
publishFn('device.reset')
|
||||||
|
};
|
||||||
|
const onrollback = ev => {
|
||||||
|
publishFn('ota.rollback')
|
||||||
|
};
|
||||||
const onerase = ev => {};
|
const onerase = ev => {};
|
||||||
const onupload = function(ok, name, size) {
|
const onupload = function(ok, name, size) {
|
||||||
if (!ok) return false;
|
if (!ok) return false;
|
||||||
return new Promise(r => setTimeout(ev => { refresh(); r(); }, 3000)).then(r => setClean(true));
|
return new Promise(r => setTimeout(ev => {
|
||||||
|
refresh();
|
||||||
|
r();
|
||||||
|
}, 3000)).then(r => setClean(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultInfo = {status: 0, crc32: 0, size: 0, timestamp: 0}
|
const defaultInfo = {status: 0, crc32: 0, size: 0, timestamp: 0};
|
||||||
return html`
|
return html`
|
||||||
<div class="bg-slate-200 border rounded-md text-ellipsis overflow-auto">
|
<div class="bg-slate-200 border rounded text-ellipsis overflow-auto mt-4">
|
||||||
<div class="bg-slate-100 px-4 py-2 flex items-center justify-between font-light uppercase text-gray-600">
|
<div class="bg-white px-4 py-2 flex items-center justify-between font-light uppercase text-gray-600">
|
||||||
Over-the-air firmware updates
|
Over-the-air firmware updates
|
||||||
</div>
|
</div>
|
||||||
<div class="gap-1 grid grid-cols-3 lg:grid-cols-3">
|
<div class="gap-1 grid grid-cols-3 lg:grid-cols-3">
|
||||||
<${FirmwareStatus} title="Current firmware image" info=${info[0] ? info[0] : defaultInfo}>
|
<${FirmwareStatus} title="Current firmware image"
|
||||||
|
info=${info[0] ? info[0] : defaultInfo}>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<${Button} title="Commit this firmware" onclick=${oncommit}
|
<${Button} title="Commit this firmware" onclick=${oncommit}
|
||||||
icon=${Icons.thumbUp} disabled=${disabled} cls="w-full" />
|
icon=${Icons.thumbUp} disabled=${disabled} cls="w-full" />
|
||||||
<//>
|
<//>
|
||||||
<//>
|
<//>
|
||||||
<${FirmwareStatus} title="Previous firmware image" info=${info[1] ? info[1] : defaultInfo}>
|
<${FirmwareStatus} title="Previous firmware image" info=${
|
||||||
|
info[1] ? info[1] : defaultInfo}>
|
||||||
<${Button} title="Rollback to this firmware" onclick=${onrollback}
|
<${Button} title="Rollback to this firmware" onclick=${onrollback}
|
||||||
icon=${Icons.backward} disabled=${disabled} cls="w-full" />
|
icon=${Icons.backward} disabled=${disabled} cls="w-full" />
|
||||||
<//>
|
<//>
|
||||||
@@ -224,105 +226,117 @@ function FirmwareUpdate({ publishFn, disabled, info, deviceID }) {
|
|||||||
<//>
|
<//>
|
||||||
<div class="px-4 py-3 flex flex-col gap-2 grow">
|
<div class="px-4 py-3 flex flex-col gap-2 grow">
|
||||||
<${UploadFileButton}
|
<${UploadFileButton}
|
||||||
title="Upload new firmware: choose .bin file:" publishFn=${publishFn} onupload=${onupload} clean=${clean} setCleanFn=${setClean} disabled=${disabled}
|
title="Upload new firmware: choose .bin file:"
|
||||||
|
publishFn=${publishFn} onupload=${onupload} clean=${clean}
|
||||||
|
setCleanFn=${setClean} disabled=${disabled}
|
||||||
id=${deviceID} url="api/firmware/upload" accept=".bin,.uf2" />
|
id=${deviceID} url="api/firmware/upload" accept=".bin,.uf2" />
|
||||||
<div class="grow"><//>
|
<div class="grow"><//>
|
||||||
<${Button} title="Reboot device" onclick=${onreboot} icon=${Icons.refresh} disabled=${disabled} cls="w-full" />
|
<${Button} title="Reboot device" onclick=${onreboot}
|
||||||
<${Button} title="Erase last sector" onclick=${onerase} icon=${Icons.doc} disabled=${disabled} cls="w-full hidden" />
|
icon=${Icons.refresh} disabled=${disabled} cls="w-full" />
|
||||||
|
<${Button} title="Erase last sector" onclick=${onerase}
|
||||||
|
icon=${Icons.doc} disabled=${disabled} cls="w-full hidden" />
|
||||||
<//>
|
<//>
|
||||||
<//>
|
<//>
|
||||||
<//>
|
<//>
|
||||||
<//>`;
|
<//>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Config( {deviceData, setDeviceConfig, publishFn} ) {
|
function DeviceControlPanel({device, setDeviceConfig, publishFn, connected}) {
|
||||||
const [localConfig, setLocalConfig] = useState();
|
const cfg = device && device.config ? device.config : DefaultDeviceConfig;
|
||||||
|
const [localConfig, setLocalConfig] = useState(cfg);
|
||||||
const [saveResult, setSaveResult] = useState(null);
|
const [saveResult, setSaveResult] = useState(null);
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
useEffect(() => {
|
useEffect(() => setLocalConfig(cfg), [device]);
|
||||||
if (deviceData) {
|
|
||||||
if (deviceData.config) {
|
|
||||||
setLocalConfig(deviceData.config)
|
|
||||||
} else {
|
|
||||||
let config = {}
|
|
||||||
setLocalConfig(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [deviceData]);
|
|
||||||
|
|
||||||
const logOptions = [[0, 'Disable'], [1, 'Error'], [2, 'Info'], [3, 'Debug']];
|
const logOptions = [[0, 'Disable'], [1, 'Error'], [2, 'Info'], [3, 'Debug']];
|
||||||
const mksetfn = k => (v => setLocalConfig(x => Object.assign({}, x, { [k]: v })));
|
const mksetfn = k => (v => setLocalConfig(x => Object.assign({}, x, {[k]: v})));
|
||||||
|
|
||||||
const onSave = () => {
|
const onSave = ev => publishFn('config.set', localConfig).then(r => {
|
||||||
setSaving(true)
|
setDeviceConfig(device.id, localConfig)
|
||||||
publishFn("config.set", localConfig).then(r => {
|
setSaveResult('Success!')
|
||||||
setDeviceConfig(deviceData.id, localConfig)
|
}).catch(e => {
|
||||||
setSaveResult("Success!")
|
setDeviceConfig(device.id, device.config)
|
||||||
setSaving(false)
|
setSaveResult('Failed!')
|
||||||
}).catch(e => {
|
})
|
||||||
setDeviceConfig(deviceData.id, deviceData.config)
|
|
||||||
setSaveResult("Failed!")
|
|
||||||
setSaving(false)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const onLedToggle = function(ev) {
|
if (!device || !localConfig) {
|
||||||
localConfig.led_status = !localConfig.led_status;
|
return html`
|
||||||
onSave();
|
<div class="flex grow text-gray-400 justify-center items-center text-xl">
|
||||||
};
|
No device selected. Click on a device on a sidebar
|
||||||
|
<//>`;
|
||||||
if (!deviceData || !localConfig) {
|
|
||||||
return ``;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To delete device, set an empty retained message
|
||||||
|
const onforget = function(ev) {
|
||||||
|
MqttClient.publish(device.topic, '', {retain: true});
|
||||||
|
location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const mksetpin = (pin, val) => setLocalConfig(x => {
|
||||||
|
let pins = x.pins.slice();
|
||||||
|
pins[pin] = val ? 0 : 1;
|
||||||
|
return Object.assign({}, x, {pins});
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingstyle = 'inline-block mr-8 my-1 bg-gray-100 rounded px-2';
|
||||||
|
const Pin = (value, pin) => html`
|
||||||
|
<div class="${settingstyle} w-32">
|
||||||
|
<${Setting}
|
||||||
|
setfn=${ev => mksetpin(pin, value)}
|
||||||
|
type="switch" value=${value} xcls="py-1 w-32 overflow-auto"
|
||||||
|
disabled=${!device.online || !connected} title="Pin ${pin}" />
|
||||||
|
<//>`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="m-4 space-y-2">
|
<div class="flex-1 xgrow overflow-auto bg-gray-200 xpx-4 xpy-2 p-3">
|
||||||
<h3 class="text-lg font-semibold mb-3">Device ${deviceData.id} Configuration Panel</h3>
|
<div class="divide-y border rounded bg-white xbg-slate-100 my-y">
|
||||||
<div class="divide-y border rounded bg-slate-200">
|
|
||||||
<div class="bg-slate-100 px-4 py-2 flex items-center font-light uppercase text-gray-600">
|
<div class="px-4 py-2 flex justify-between items-center font-light uppercase text-gray-600 rounded">
|
||||||
<span class="mr-2">
|
<div class="flex gap-2 items-center">
|
||||||
Status: ${html`<${Colored} colors=${deviceData.online ? tipColors.green : tipColors.red} text=${deviceData.online ? 'online' : 'offline'} />`}
|
<span>Device ${device.id}<//>
|
||||||
</span>
|
${html`<${Colored}
|
||||||
<//>
|
colors=${device.online ? tipColors.green : tipColors.red}
|
||||||
<div class="grid grid-cols-2 gap-1">
|
text=${ device.online ? 'online' : 'offline'} />`}
|
||||||
<div class="py-1 divide-y border rounded bg-white flex flex-col">
|
|
||||||
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
|
|
||||||
LED Settings
|
|
||||||
<//>
|
<//>
|
||||||
<div class="py-2 px-5 flex-1 flex flex-col relative">
|
<${Button} title="Forget this device"
|
||||||
<${Setting} title="LED status" value=${localConfig.led_status} setfn=${onLedToggle} type="switch" disabled=${!deviceData.online} />
|
disabled=${device.online || !connected}
|
||||||
<${Setting} title="LED Pin" type="number" value=${localConfig.led_pin} setfn=${mksetfn('led_pin')} disabled=${!deviceData.online} />
|
icon=${Icons.fail} onclick=${onforget}/>
|
||||||
|
<//>
|
||||||
|
|
||||||
|
<div class="p-4 xgrid gap-3">
|
||||||
|
${localConfig.pins.map((val, pin) => Pin(val, pin))}
|
||||||
|
<div><//>
|
||||||
|
|
||||||
|
<div class="${settingstyle} w-48">
|
||||||
|
<${Setting} title="Log Level" type="select"
|
||||||
|
value=${localConfig.log_level} setfn=${mksetfn('log_level')}
|
||||||
|
options=${logOptions} disabled=${!device.online}/>
|
||||||
<//>
|
<//>
|
||||||
</div>
|
|
||||||
<div class="py-1 divide-y border rounded bg-white flex flex-col">
|
|
||||||
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
|
|
||||||
Log & Display
|
|
||||||
<//>
|
|
||||||
<div class="py-2 px-5 flex-1 flex flex-col relative">
|
|
||||||
<${Setting} title="Log Level" type="select" value=${localConfig.log_level} setfn=${mksetfn('log_level')} options=${logOptions} disabled=${!deviceData.online}/>
|
|
||||||
<${Setting} title="Brightness" type="number" value=${localConfig.brightness} setfn=${mksetfn('brightness')} disabled=${!deviceData.online} />
|
|
||||||
<div class="mt-3 flex justify-end">
|
<div class="mt-3 flex justify-end">
|
||||||
${saveResult && html`<${Notification} ok=${saveResult === "Success!"}
|
${saveResult && html`<${Notification} ok=${saveResult === 'Success!'}
|
||||||
text=${saveResult} close=${() => setSaveResult(null)} />`}
|
text=${saveResult} close=${() => setSaveResult(null)} />`}
|
||||||
<${Button} icon=${Icons.save} onclick=${onSave} title=${saving ? "Saving..." : "Save Settings"} disabled=${!deviceData.online || saving} />
|
<${Button} icon=${Icons.save} onclick=${onSave}
|
||||||
</div>
|
title="Save Settings" disabled=${!device.online} />
|
||||||
<//>
|
<//>
|
||||||
</div>
|
<//>
|
||||||
</div>
|
<//>
|
||||||
</div>
|
|
||||||
<${FirmwareUpdate} deviceID=${deviceData.id} publishFn=${publishFn} disabled=${!deviceData.online} info=${[localConfig.crnt_fw, localConfig.prev_fw]} />
|
<${FirmwareUpdate} deviceID=${device.id} publishFn=${publishFn}
|
||||||
</div>`;
|
disabled=${!device.online}
|
||||||
|
info=${[localConfig.crnt_fw, localConfig.prev_fw]} />
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = function() {
|
const App = function() {
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [currentDevID, setCurrentDevID] = useState(localStorage.getItem('currentDevID') || '');
|
const [currentDevID, setCurrentDevID] = useState(localStorage.getItem('currentDevID') || '');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [topic, setTopic] = useState(localStorage.getItem('topic') || default_topic);
|
const [url, setUrl] = useState(localStorage.getItem('url') || DefaultUrl);
|
||||||
|
const [topic, setTopic] = useState(localStorage.getItem('topic') || DefaultTopic);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
const responseHandlers = useRef({});
|
const responseHandlers = useRef({});
|
||||||
let connSuccessful = false;
|
|
||||||
|
|
||||||
function addResponseHandler(correlationId, handler) {
|
function addResponseHandler(correlationId, handler) {
|
||||||
responseHandlers[correlationId] = handler;
|
responseHandlers[correlationId] = handler;
|
||||||
@@ -332,32 +346,27 @@ const App = function() {
|
|||||||
delete responseHandlers[correlationId];
|
delete responseHandlers[correlationId];
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRefresh = () => {
|
const onRefresh = ev => window.location.reload();
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
const initConn = () => {
|
const initConn = () => {
|
||||||
client = mqtt.connect(url, {
|
MqttClient = mqtt.connect(url, {connectTimeout: 5000, reconnectPeriod: 0});
|
||||||
connectTimeout: 5000,
|
|
||||||
reconnectPeriod: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('connect', () => {
|
MqttClient.on('connect', () => {
|
||||||
console.log('Connected to the broker');
|
//console.log('Connected to the broker');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(null); // Reset error state upon successful connection
|
setError(null); // Reset error state upon successful connection
|
||||||
connSuccessful = true;
|
setConnected(true);
|
||||||
|
|
||||||
const statusTopic = topic + '/+/status'
|
const statusTopic = topic + '/+/status'
|
||||||
const txTopic = topic + '/+/tx'
|
const txTopic = topic + '/+/tx'
|
||||||
|
|
||||||
const subscribe = (topic) => {
|
const subscribe = (topic) => {
|
||||||
client.subscribe(topic, (err) => {
|
MqttClient.subscribe(topic, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error subscribing to topic:', err);
|
console.error('Error subscribing to topic:', err);
|
||||||
setError('Error subscribing to topic');
|
setError('Error subscribing to topic');
|
||||||
} else {
|
} else {
|
||||||
console.log('Successfully subscribed to ', topic);
|
//console.log('Successfully subscribed to ', topic);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -366,40 +375,34 @@ const App = function() {
|
|||||||
subscribe(txTopic)
|
subscribe(txTopic)
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('message', (topic, message) => {
|
MqttClient.on('message', (topic, message) => {
|
||||||
//console.log(`Received message from ${topic}: ${message.toString()}`);
|
// console.log(`Received message from ${topic}: ${message.toString()}`);
|
||||||
|
if (message.length == 0) return;
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = JSON.parse(message.toString());
|
response = JSON.parse(message.toString());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topic.endsWith("/status")) {
|
if (topic.endsWith('/status')) {
|
||||||
const deviceID = topic.split('/')[1]
|
let device = {topic: topic, id: topic.split('/')[1], config: DefaultDeviceConfig};
|
||||||
let device = {};
|
const params = response.params;
|
||||||
device.id = deviceID;
|
|
||||||
const params = response.params
|
|
||||||
if (!params) {
|
if (!params) {
|
||||||
console.error("Invalid response")
|
console.error('Invalid response');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
device.online = params.status === "online"
|
device.online = params.status === 'online'
|
||||||
if (device.online) {
|
if (device.online) {
|
||||||
device.config = {}
|
device.config = params;
|
||||||
device.config.led_status = params.led_status
|
if (!device.config.pins) device.config.pins = [];
|
||||||
device.config.led_pin = params.led_pin
|
|
||||||
device.config.brightness = params.brightness
|
|
||||||
device.config.log_level = params.log_level
|
|
||||||
device.config.crnt_fw = params.crnt_fw
|
|
||||||
device.config.prev_fw = params.prev_fw
|
|
||||||
}
|
}
|
||||||
setDevice(device)
|
setDevice(device)
|
||||||
} else if (topic.endsWith("/tx")) {
|
} else if (topic.endsWith('/tx')) {
|
||||||
if (!response.id) {
|
if (!response.id) {
|
||||||
console.error("Invalid response")
|
console.error('Invalid response');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const handler = responseHandlers[response.id];
|
const handler = responseHandlers[response.id];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
@@ -409,13 +412,13 @@ const App = function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('error', (err) => {
|
MqttClient.on('error', (err) => {
|
||||||
console.error('Connection error:', err);
|
console.error('Connection error:', err);
|
||||||
setError('Connection cannot be established.');
|
setError('Connection cannot be established.');
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('close', () => {
|
MqttClient.on('close', () => {
|
||||||
if (!connSuccessful) {
|
if (!connected) {
|
||||||
console.error('Failed to connect to the broker.');
|
console.error('Failed to connect to the broker.');
|
||||||
setError('Connection cannot be established.');
|
setError('Connection cannot be established.');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -423,14 +426,12 @@ const App = function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => initConn(), []);
|
||||||
initConn();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePublish = (methodName, parameters, timeout = 5000) => {
|
const handlePublish = (methodName, parameters, timeout = 5000) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const randomIdGenerator = function(length) {
|
const randomIdGenerator = function(length) {
|
||||||
return Math.random().toString(36).substring(2, length+2);
|
return Math.random().toString(36).substring(2, length + 2);
|
||||||
};
|
};
|
||||||
const randomID = randomIdGenerator(40);
|
const randomID = randomIdGenerator(40);
|
||||||
const timeoutID = setTimeout(() => {
|
const timeoutID = setTimeout(() => {
|
||||||
@@ -445,27 +446,21 @@ const App = function() {
|
|||||||
|
|
||||||
if (currentDevID) {
|
if (currentDevID) {
|
||||||
const rxTopic = topic + `/${currentDevID}/rx`;
|
const rxTopic = topic + `/${currentDevID}/rx`;
|
||||||
const rpcPayload = {
|
const rpcPayload = {method: methodName, id: randomID};
|
||||||
method: methodName,
|
|
||||||
id: randomID
|
|
||||||
};
|
|
||||||
|
|
||||||
if (parameters) {
|
if (parameters) {
|
||||||
rpcPayload.params = parameters;
|
rpcPayload.params = parameters;
|
||||||
}
|
}
|
||||||
|
MqttClient.publish(rxTopic, JSON.stringify(rpcPayload));
|
||||||
client.publish(rxTopic, JSON.stringify(rpcPayload));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceByID = (deviceID) => {
|
const getDeviceByID = (deviceID) => devices.find(d => d.id === deviceID);
|
||||||
return devices.find(d => d.id === deviceID);
|
|
||||||
}
|
|
||||||
|
|
||||||
const setDevice = (devData) => {
|
const setDevice = (devData) => {
|
||||||
setDevices(prevDevices => {
|
setDevices(prevDevices => {
|
||||||
const devIndex = prevDevices.findIndex(device => device.id === devData.id);
|
const devIndex =
|
||||||
|
prevDevices.findIndex(device => device.id === devData.id);
|
||||||
if (devIndex !== -1) {
|
if (devIndex !== -1) {
|
||||||
if (!devData.online && !devData.config) {
|
if (!devData.online && !devData.config) {
|
||||||
const updatedDevices = [...prevDevices];
|
const updatedDevices = [...prevDevices];
|
||||||
@@ -484,10 +479,7 @@ const App = function() {
|
|||||||
setDevices(prevDevices => {
|
setDevices(prevDevices => {
|
||||||
return prevDevices.map(device => {
|
return prevDevices.map(device => {
|
||||||
if (device.id === deviceID) {
|
if (device.id === deviceID) {
|
||||||
return {
|
return {...device, config: config};
|
||||||
...device,
|
|
||||||
config: config
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return device;
|
return device;
|
||||||
});
|
});
|
||||||
@@ -500,7 +492,7 @@ const App = function() {
|
|||||||
setCurrentDevID(device.id);
|
setCurrentDevID(device.id);
|
||||||
localStorage.setItem('currentDevID', device.id);
|
localStorage.setItem('currentDevID', device.id);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return html`
|
return html`
|
||||||
@@ -513,7 +505,8 @@ const App = function() {
|
|||||||
Unable to connect to the MQTT broker.
|
Unable to connect to the MQTT broker.
|
||||||
</p>
|
</p>
|
||||||
<div class="text-center relative">
|
<div class="text-center relative">
|
||||||
<${Button} title="Retry" onclick=${onRefresh} icon=${Icons.refresh} class="absolute top-4 right-4" />
|
<${Button} title="Retry" onclick=${onRefresh}
|
||||||
|
icon=${Icons.refresh} class="absolute top-4 right-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -521,15 +514,17 @@ const App = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="min-h-screen bg-slate-100 flex flex-col space-y-2">
|
<div class="h-full flex flex-col">
|
||||||
<${Header} topic=${topic} setTopicFn=${setTopic}/>
|
<${Header}
|
||||||
<div class="transition-all duration-300 transform flex flex-grow gap-4">
|
topic=${topic} setTopic=${setTopic} url=${url}
|
||||||
<div class="flex-none bg-white rounded shadow ">
|
setUrl=${setUrl} connected=${connected}
|
||||||
<${Devices} devices=${devices} onClickFn=${onDeviceClick} />
|
/>
|
||||||
<//>
|
<div class="flex grow overflow-auto">
|
||||||
<div class="flex-1 flex-grow bg-white rounded shadow h-full">
|
<${Sidebar} devices=${devices} onclick=${onDeviceClick} />
|
||||||
<${Config} deviceData=${getDeviceByID(currentDevID)} setDeviceConfig=${setDeviceConfig} publishFn=${handlePublish}/>
|
<${DeviceControlPanel}
|
||||||
<//>
|
device=${getDeviceByID(currentDevID)} connected=${connected}
|
||||||
|
setDeviceConfig=${setDeviceConfig} publishFn=${handlePublish}
|
||||||
|
/>
|
||||||
<//>
|
<//>
|
||||||
<//>`;
|
<//>`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,29 +10,27 @@ static void signal_handler(int signo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mocked device pins
|
// Mocked device pins
|
||||||
static bool s_pins[100];
|
static bool s_pins[NUM_PINS];
|
||||||
|
|
||||||
void hal_gpio_write(int pin, bool status) {
|
bool hal_gpio_write(int pin, bool status) {
|
||||||
if (pin >= 0 && pin < (int) (sizeof(s_pins) / sizeof(s_pins[0]))) {
|
bool ok = false;
|
||||||
|
if (pin >= 0 && pin < NUM_PINS) {
|
||||||
s_pins[pin] = status;
|
s_pins[pin] = status;
|
||||||
|
ok = true;
|
||||||
}
|
}
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hal_gpio_read(int pin) {
|
bool hal_gpio_read(int pin) {
|
||||||
return (pin >= 0 && pin < (int) (sizeof(s_pins) / sizeof(s_pins[0])))
|
return (pin >= 0 && pin < NUM_PINS) ? s_pins[pin] : false;
|
||||||
? s_pins[pin]
|
|
||||||
: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int hal_led_pin(void) {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
struct mg_mgr mgr;
|
struct mg_mgr mgr;
|
||||||
|
int i;
|
||||||
|
|
||||||
// Parse command-line flags
|
// Parse command-line flags
|
||||||
for (int i = 1; i < argc; i++) {
|
for (i = 1; i < argc; i++) {
|
||||||
if (strcmp(argv[i], "-u") == 0 && argv[i + 1] != NULL) {
|
if (strcmp(argv[i], "-u") == 0 && argv[i + 1] != NULL) {
|
||||||
g_url = argv[++i];
|
g_url = argv[++i];
|
||||||
} else if (strcmp(argv[i], "-i") == 0 && argv[i + 1] != NULL) {
|
} else if (strcmp(argv[i], "-i") == 0 && argv[i + 1] != NULL) {
|
||||||
@@ -62,8 +60,8 @@ int main(int argc, char *argv[]) {
|
|||||||
mg_mgr_poll(&mgr, 50);
|
mg_mgr_poll(&mgr, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
web_free();
|
||||||
mg_mgr_free(&mgr);
|
mg_mgr_free(&mgr);
|
||||||
web_destroy();
|
|
||||||
MG_INFO(("Exiting on signal %d", s_signo));
|
MG_INFO(("Exiting on signal %d", s_signo));
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -4,31 +4,50 @@
|
|||||||
#include "net.h"
|
#include "net.h"
|
||||||
|
|
||||||
char *g_url = MQTT_SERVER_URL;
|
char *g_url = MQTT_SERVER_URL;
|
||||||
|
char *g_root_topic = MQTT_ROOT_TOPIC;
|
||||||
char *g_device_id;
|
char *g_device_id;
|
||||||
char *g_root_topic;
|
|
||||||
static uint8_t s_qos = 1; // MQTT QoS
|
static uint8_t s_qos = 1; // MQTT QoS
|
||||||
static struct mg_connection *s_conn; // Client connection
|
static struct mg_connection *s_conn; // MQTT Client connection
|
||||||
static struct mg_rpc *s_rpc_head = NULL;
|
static struct mg_rpc *s_rpc = NULL; // List of registered RPC methods
|
||||||
|
|
||||||
struct device_config {
|
struct device_config {
|
||||||
bool led_status;
|
int pins[NUM_PINS]; // State of the GPIO pins
|
||||||
int led_pin;
|
int log_level; // Device logging level, 0-4
|
||||||
int brightness;
|
|
||||||
int log_level;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct device_config s_device_config;
|
static struct device_config s_device_config;
|
||||||
|
|
||||||
// This is for newlib and TLS (mbedTLS)
|
// Device ID generation function. Create an ID that is unique
|
||||||
uint64_t mg_now(void) {
|
// for a given device, and does not change between device restarts.
|
||||||
return mg_millis();
|
static void set_device_id(void) {
|
||||||
}
|
char buf[15] = "";
|
||||||
|
|
||||||
static void generate_device_id(void) {
|
#ifdef _WIN32
|
||||||
char tmp[DEVICE_ID_LEN + 1];
|
unsigned serial = 0;
|
||||||
tmp[DEVICE_ID_LEN] = '\0';
|
if (GetVolumeInformationA("c:\\", NULL, 0, &serial, NULL, NULL, NULL, 0)) {
|
||||||
mg_random_str(tmp, DEVICE_ID_LEN);
|
mg_snprintf(buf, sizeof(buf), "%lx", serial);
|
||||||
g_device_id = strdup(tmp);
|
}
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
FILE *fp = popen(
|
||||||
|
"ioreg -l | grep IOPlatformSerialNumber | cut -d'\"' -f4 | tr -d $'\n'",
|
||||||
|
"r");
|
||||||
|
if (fp != NULL) {
|
||||||
|
fread(buf, 1, sizeof(buf), fp);
|
||||||
|
fclose(fp);
|
||||||
|
}
|
||||||
|
#elif defined(__linux__)
|
||||||
|
char *id = mg_file_read(&mg_fs_posix, "/etc/machine-id", NULL);
|
||||||
|
if (id != NULL) {
|
||||||
|
mg_snprintf(buf, sizeof(buf), "%s", id);
|
||||||
|
free(id);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (buf[0] == '\0') mg_snprintf(buf, sizeof(buf), "%s", "MyDeviceID");
|
||||||
|
|
||||||
|
buf[sizeof(buf) - 1] = '\0';
|
||||||
|
g_device_id = strdup(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
static size_t print_fw_status(void (*out)(char, void *), void *ptr,
|
static size_t print_fw_status(void (*out)(char, void *), void *ptr,
|
||||||
@@ -40,45 +59,51 @@ static size_t print_fw_status(void (*out)(char, void *), void *ptr,
|
|||||||
MG_ESC("timestamp"), mg_ota_timestamp(fw));
|
MG_ESC("timestamp"), mg_ota_timestamp(fw));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static size_t print_ints(void (*out)(char, void *), void *ptr, va_list *ap) {
|
||||||
|
int *array = va_arg(*ap, int *);
|
||||||
|
size_t i, len = 0, num_elems = va_arg(*ap, size_t);
|
||||||
|
for (i = 0; i < num_elems; i++) {
|
||||||
|
len += mg_xprintf(out, ptr, "%s%d", i ? "," : "", array[i]);
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
static void publish_status(struct mg_connection *c) {
|
static void publish_status(struct mg_connection *c) {
|
||||||
char *status_topic = mg_mprintf("%s/%s/status", g_root_topic, g_device_id);
|
char topic[100];
|
||||||
struct mg_str pubt = mg_str(status_topic);
|
|
||||||
struct mg_mqtt_opts pub_opts;
|
struct mg_mqtt_opts pub_opts;
|
||||||
|
struct mg_iobuf io = {0, 0, 0, 512};
|
||||||
|
|
||||||
|
// Print JSON notification into the io buffer
|
||||||
|
mg_xprintf(mg_pfn_iobuf, &io,
|
||||||
|
"{%m:%m,%m:{%m:%m,%m:%d,%m:[%M],%m:%M,%m:%M}}", //
|
||||||
|
MG_ESC("method"), MG_ESC("status.notify"), MG_ESC("params"), //
|
||||||
|
MG_ESC("status"), MG_ESC("online"), //
|
||||||
|
MG_ESC(("log_level")), s_device_config.log_level, //
|
||||||
|
MG_ESC(("pins")), print_ints, s_device_config.pins, NUM_PINS, //
|
||||||
|
MG_ESC(("crnt_fw")), print_fw_status, MG_FIRMWARE_CURRENT, //
|
||||||
|
MG_ESC(("prev_fw")), print_fw_status, MG_FIRMWARE_PREVIOUS);
|
||||||
|
|
||||||
memset(&pub_opts, 0, sizeof(pub_opts));
|
memset(&pub_opts, 0, sizeof(pub_opts));
|
||||||
pub_opts.topic = pubt;
|
mg_snprintf(topic, sizeof(topic), "%s/%s/status", g_root_topic, g_device_id);
|
||||||
s_device_config.led_status = hal_gpio_read(s_device_config.led_pin);
|
pub_opts.topic = mg_str(topic);
|
||||||
char *device_status_json = mg_mprintf(
|
pub_opts.message = mg_str_n((char *) io.buf, io.len);
|
||||||
"{%m:%m,%m:{%m:%m,%m:%s,%m:%hhu,%m:%hhu,%m:%hhu,%m:%M,%m:%M}}",
|
pub_opts.qos = s_qos;
|
||||||
MG_ESC("method"), MG_ESC("status.notify"), MG_ESC("params"),
|
pub_opts.retain = true;
|
||||||
MG_ESC("status"), MG_ESC("online"), MG_ESC("led_status"),
|
|
||||||
s_device_config.led_status ? "true" : "false", MG_ESC("led_pin"),
|
|
||||||
s_device_config.led_pin, MG_ESC("brightness"), s_device_config.brightness,
|
|
||||||
MG_ESC(("log_level")), s_device_config.log_level, MG_ESC(("crnt_fw")),
|
|
||||||
print_fw_status, MG_FIRMWARE_CURRENT, MG_ESC(("prev_fw")),
|
|
||||||
print_fw_status, MG_FIRMWARE_PREVIOUS);
|
|
||||||
struct mg_str data = mg_str(device_status_json);
|
|
||||||
pub_opts.message = data;
|
|
||||||
pub_opts.qos = s_qos, pub_opts.retain = true;
|
|
||||||
mg_mqtt_pub(c, &pub_opts);
|
mg_mqtt_pub(c, &pub_opts);
|
||||||
MG_INFO(("%lu PUBLISHED %.*s -> %.*s", c->id, (int) data.len, data.ptr,
|
MG_INFO(("%lu PUBLISHED %s -> %.*s", c->id, topic, io.len, io.buf));
|
||||||
(int) pubt.len, pubt.ptr));
|
mg_iobuf_free(&io);
|
||||||
free(device_status_json);
|
|
||||||
free(status_topic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void publish_response(struct mg_connection *c, char *buf, size_t len) {
|
static void publish_response(struct mg_connection *c, char *buf, size_t len) {
|
||||||
char *tx_topic = mg_mprintf("%s/%s/tx", g_root_topic, g_device_id);
|
|
||||||
struct mg_str pubt = mg_str(tx_topic);
|
|
||||||
struct mg_mqtt_opts pub_opts;
|
struct mg_mqtt_opts pub_opts;
|
||||||
|
char topic[100];
|
||||||
|
mg_snprintf(topic, sizeof(topic), "%s/%s/tx", g_root_topic, g_device_id);
|
||||||
memset(&pub_opts, 0, sizeof(pub_opts));
|
memset(&pub_opts, 0, sizeof(pub_opts));
|
||||||
pub_opts.topic = pubt;
|
pub_opts.topic = mg_str(topic);
|
||||||
struct mg_str data = mg_str_n(buf, len);
|
pub_opts.message = mg_str_n(buf, len);
|
||||||
pub_opts.message = data;
|
|
||||||
pub_opts.qos = s_qos;
|
pub_opts.qos = s_qos;
|
||||||
mg_mqtt_pub(c, &pub_opts);
|
mg_mqtt_pub(c, &pub_opts);
|
||||||
MG_INFO(("%lu PUBLISHED %.*s -> %.*s", c->id, (int) data.len, data.ptr,
|
MG_INFO(("%lu PUBLISHED %s -> %.*s", c->id, topic, len, buf));
|
||||||
(int) pubt.len, pubt.ptr));
|
|
||||||
free(tx_topic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void subscribe(struct mg_connection *c) {
|
static void subscribe(struct mg_connection *c) {
|
||||||
@@ -94,29 +119,26 @@ static void subscribe(struct mg_connection *c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void rpc_config_set(struct mg_rpc_req *r) {
|
static void rpc_config_set(struct mg_rpc_req *r) {
|
||||||
bool tmp_status, ok;
|
struct device_config dc = s_device_config;
|
||||||
int tmp_brightness, tmp_level, tmp_pin;
|
dc.log_level = (int) mg_json_get_long(r->frame, "$.params.log_level", -1);
|
||||||
|
|
||||||
ok = mg_json_get_bool(r->frame, "$.params.led_status", &tmp_status);
|
if (dc.log_level < 0 || dc.log_level > MG_LL_VERBOSE) {
|
||||||
if (ok) s_device_config.led_status = tmp_status;
|
mg_rpc_err(r, -32602, "Log level must be from 0 to 4");
|
||||||
|
} else {
|
||||||
tmp_brightness = (int) mg_json_get_long(r->frame, "$.params.brightness", -1);
|
int i, val;
|
||||||
if (tmp_brightness >= 0) s_device_config.brightness = tmp_brightness;
|
for (i = 0; i < NUM_PINS; i++) {
|
||||||
|
char path[20];
|
||||||
tmp_level = (int) mg_json_get_long(r->frame, "$.params.log_level", -1);
|
mg_snprintf(path, sizeof(path), "$.params.pins[%lu]", i);
|
||||||
if (tmp_level >= 0) {
|
val = (int) mg_json_get_long(r->frame, path, -1);
|
||||||
s_device_config.log_level = tmp_level;
|
if (val >= 0 && val != dc.pins[i]) {
|
||||||
mg_log_set(s_device_config.log_level);
|
dc.pins[i] = val;
|
||||||
|
hal_gpio_write((int) i, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mg_log_set(dc.log_level);
|
||||||
|
s_device_config = dc;
|
||||||
|
mg_rpc_ok(r, "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
tmp_pin = (int) mg_json_get_long(r->frame, "$.params.led_pin", -1);
|
|
||||||
if (tmp_pin != -1) s_device_config.led_pin = tmp_pin;
|
|
||||||
|
|
||||||
if (tmp_pin != -1 && ok) {
|
|
||||||
hal_gpio_write(s_device_config.led_pin, s_device_config.led_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
mg_rpc_ok(r, "%m", MG_ESC("ok"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void rpc_ota_commit(struct mg_rpc_req *r) {
|
static void rpc_ota_commit(struct mg_rpc_req *r) {
|
||||||
@@ -143,31 +165,29 @@ static void rpc_ota_rollback(struct mg_rpc_req *r) {
|
|||||||
static void rpc_ota_upload(struct mg_rpc_req *r) {
|
static void rpc_ota_upload(struct mg_rpc_req *r) {
|
||||||
long ofs = mg_json_get_long(r->frame, "$.params.offset", -1);
|
long ofs = mg_json_get_long(r->frame, "$.params.offset", -1);
|
||||||
long tot = mg_json_get_long(r->frame, "$.params.total", -1);
|
long tot = mg_json_get_long(r->frame, "$.params.total", -1);
|
||||||
int len;
|
int len = 0;
|
||||||
char *file_chunk = mg_json_get_b64(r->frame, "$.params.chunk", &len);
|
char *buf = mg_json_get_b64(r->frame, "$.params.chunk", &len);
|
||||||
if (!file_chunk) {
|
if (buf == NULL) {
|
||||||
mg_rpc_err(r, 1, "Error processing the binary chunk.");
|
mg_rpc_err(r, 1, "Error processing the binary chunk.");
|
||||||
return;
|
|
||||||
}
|
|
||||||
struct mg_str data = mg_str_n(file_chunk, len);
|
|
||||||
if (ofs < 0 || tot < 0) {
|
|
||||||
mg_rpc_err(r, 1, "offset and total not set");
|
|
||||||
} else if (ofs == 0 && mg_ota_begin((size_t) tot) == false) {
|
|
||||||
mg_rpc_err(r, 1, "mg_ota_begin(%ld) failed\n", tot);
|
|
||||||
} else if (data.len > 0 && mg_ota_write(data.ptr, data.len) == false) {
|
|
||||||
mg_rpc_err(r, 1, "mg_ota_write(%lu) @%ld failed\n", data.len, ofs);
|
|
||||||
mg_ota_end();
|
|
||||||
} else if (data.len == 0 && mg_ota_end() == false) {
|
|
||||||
mg_rpc_err(r, 1, "mg_ota_end() failed\n", tot);
|
|
||||||
} else {
|
} else {
|
||||||
mg_rpc_ok(r, "%m", MG_ESC("ok"));
|
if (ofs < 0 || tot < 0) {
|
||||||
if (data.len == 0) {
|
mg_rpc_err(r, 1, "offset and total not set");
|
||||||
// Successful mg_ota_end() called, schedule device reboot
|
} else if (ofs == 0 && mg_ota_begin((size_t) tot) == false) {
|
||||||
mg_timer_add(s_conn->mgr, 500, 0, (void (*)(void *)) mg_device_reset,
|
mg_rpc_err(r, 1, "mg_ota_begin(%ld) failed\n", tot);
|
||||||
NULL);
|
} else if (len > 0 && mg_ota_write(buf, len) == false) {
|
||||||
|
mg_rpc_err(r, 1, "mg_ota_write(%lu) @%ld failed\n", len, ofs);
|
||||||
|
mg_ota_end();
|
||||||
|
} else if (len == 0 && mg_ota_end() == false) {
|
||||||
|
mg_rpc_err(r, 1, "mg_ota_end() failed\n", tot);
|
||||||
|
} else {
|
||||||
|
mg_rpc_ok(r, "%m", MG_ESC("ok"));
|
||||||
|
if (len == 0) { // Successful mg_ota_end() called, schedule device reboot
|
||||||
|
mg_timer_add(s_conn->mgr, 500, 0, (void (*)(void *)) mg_device_reset,
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
free(buf);
|
||||||
}
|
}
|
||||||
free(file_chunk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
||||||
@@ -187,11 +207,12 @@ static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
|||||||
} else if (ev == MG_EV_MQTT_MSG) {
|
} else if (ev == MG_EV_MQTT_MSG) {
|
||||||
// When we get echo response, print it
|
// When we get echo response, print it
|
||||||
struct mg_mqtt_message *mm = (struct mg_mqtt_message *) ev_data;
|
struct mg_mqtt_message *mm = (struct mg_mqtt_message *) ev_data;
|
||||||
if (mm->data.len < 1024)
|
|
||||||
MG_INFO(("%lu RECEIVED %.*s <- %.*s", c->id, (int) mm->data.len,
|
|
||||||
mm->data.ptr, (int) mm->topic.len, mm->topic.ptr));
|
|
||||||
struct mg_iobuf io = {0, 0, 0, 512};
|
struct mg_iobuf io = {0, 0, 0, 512};
|
||||||
struct mg_rpc_req r = {&s_rpc_head, 0, mg_pfn_iobuf, &io, 0, mm->data};
|
struct mg_rpc_req r = {&s_rpc, NULL, mg_pfn_iobuf,
|
||||||
|
&io, NULL, {mm->data.ptr, mm->data.len}};
|
||||||
|
size_t clipped_len = mm->data.len > 512 ? 512 : mm->data.len;
|
||||||
|
MG_INFO(("%lu RECEIVED %.*s <- %.*s", c->id, clipped_len, mm->data.ptr,
|
||||||
|
mm->topic.len, mm->topic.ptr));
|
||||||
mg_rpc_process(&r);
|
mg_rpc_process(&r);
|
||||||
if (io.buf) {
|
if (io.buf) {
|
||||||
publish_response(c, (char *) io.buf, io.len);
|
publish_response(c, (char *) io.buf, io.len);
|
||||||
@@ -206,51 +227,54 @@ static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timer function - recreate client connection if it is closed
|
// Timer function - recreate client connection if it is closed
|
||||||
static void timer_fn(void *arg) {
|
static void timer_reconnect(void *arg) {
|
||||||
struct mg_mgr *mgr = (struct mg_mgr *) arg;
|
struct mg_mgr *mgr = (struct mg_mgr *) arg;
|
||||||
if (s_conn == NULL) {
|
if (s_conn == NULL) {
|
||||||
char *status_topic = mg_mprintf("%s/%s/status", g_root_topic, g_device_id);
|
struct mg_mqtt_opts opts;
|
||||||
char *msg = mg_mprintf("{%m:%m,%m:{%m:%m}}", MG_ESC("method"),
|
char topic[100], message[100];
|
||||||
MG_ESC("status.notify"), MG_ESC("params"),
|
mg_snprintf(topic, sizeof(topic), "%s/%s/status", g_root_topic,
|
||||||
MG_ESC("status"), MG_ESC("offline"));
|
g_device_id);
|
||||||
|
mg_snprintf(message, sizeof(message), "{%m:%m,%m:{%m:%m}}",
|
||||||
struct mg_mqtt_opts opts = {.clean = true,
|
MG_ESC("method"), MG_ESC("status.notify"), MG_ESC("params"),
|
||||||
.qos = s_qos,
|
MG_ESC("status"), MG_ESC("offline"));
|
||||||
.topic = mg_str(status_topic),
|
memset(&opts, 0, sizeof(opts));
|
||||||
.version = 4,
|
opts.clean = true;
|
||||||
.keepalive = MQTT_KEEP_ALIVE_INTERVAL,
|
opts.qos = s_qos;
|
||||||
.retain = true,
|
opts.topic = mg_str(topic);
|
||||||
.message = mg_str(msg)};
|
opts.version = 4;
|
||||||
|
opts.keepalive = MQTT_KEEPALIVE_SEC;
|
||||||
|
opts.retain = true;
|
||||||
|
opts.message = mg_str(message);
|
||||||
s_conn = mg_mqtt_connect(mgr, g_url, &opts, fn, NULL);
|
s_conn = mg_mqtt_connect(mgr, g_url, &opts, fn, NULL);
|
||||||
free(msg);
|
|
||||||
free(status_topic);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void timer_keepalive(void *arg) {
|
static void timer_ping(void *arg) {
|
||||||
mg_mqtt_send_header(s_conn, MQTT_CMD_PINGREQ, 0, 0);
|
mg_mqtt_send_header(s_conn, MQTT_CMD_PINGREQ, 0, 0);
|
||||||
(void) arg;
|
(void) arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
void web_init(struct mg_mgr *mgr) {
|
void web_init(struct mg_mgr *mgr) {
|
||||||
int pingreq_interval_ms = MQTT_KEEP_ALIVE_INTERVAL * 1000 - 500;
|
int i, ping_interval_ms = MQTT_KEEPALIVE_SEC * 1000 - 500;
|
||||||
if (!g_device_id) generate_device_id();
|
set_device_id();
|
||||||
if (!g_root_topic) g_root_topic = MQTT_ROOT_TOPIC;
|
|
||||||
s_device_config.log_level = (int) mg_log_level;
|
s_device_config.log_level = (int) mg_log_level;
|
||||||
s_device_config.led_pin = hal_led_pin();
|
for (i = 0; i < NUM_PINS; i++) {
|
||||||
|
s_device_config.pins[i] = hal_gpio_read(i);
|
||||||
|
}
|
||||||
|
|
||||||
// Configure JSON-RPC functions we're going to handle
|
// Configure JSON-RPC functions we're going to handle
|
||||||
mg_rpc_add(&s_rpc_head, mg_str("config.set"), rpc_config_set, NULL);
|
mg_rpc_add(&s_rpc, mg_str("config.set"), rpc_config_set, NULL);
|
||||||
mg_rpc_add(&s_rpc_head, mg_str("ota.commit"), rpc_ota_commit, NULL);
|
mg_rpc_add(&s_rpc, mg_str("ota.commit"), rpc_ota_commit, NULL);
|
||||||
mg_rpc_add(&s_rpc_head, mg_str("device.reset"), rpc_device_reset, NULL);
|
mg_rpc_add(&s_rpc, mg_str("ota.rollback"), rpc_ota_rollback, NULL);
|
||||||
mg_rpc_add(&s_rpc_head, mg_str("ota.rollback"), rpc_ota_rollback, NULL);
|
mg_rpc_add(&s_rpc, mg_str("ota.upload"), rpc_ota_upload, NULL);
|
||||||
mg_rpc_add(&s_rpc_head, mg_str("ota.upload"), rpc_ota_upload, NULL);
|
mg_rpc_add(&s_rpc, mg_str("device.reset"), rpc_device_reset, NULL);
|
||||||
|
|
||||||
mg_timer_add(mgr, 3000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn, mgr);
|
mg_timer_add(mgr, 3000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_reconnect,
|
||||||
mg_timer_add(mgr, pingreq_interval_ms, MG_TIMER_REPEAT, timer_keepalive, mgr);
|
mgr);
|
||||||
|
mg_timer_add(mgr, ping_interval_ms, MG_TIMER_REPEAT, timer_ping, mgr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void web_destroy() {
|
void web_free(void) {
|
||||||
mg_rpc_del(&s_rpc_head, NULL); // Deallocate RPC handlers
|
mg_rpc_del(&s_rpc, NULL); // Deallocate RPC handlers
|
||||||
free(g_device_id);
|
free(g_device_id);
|
||||||
}
|
}
|
||||||
@@ -8,25 +8,21 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef MQTT_DASHBOARD
|
|
||||||
#define MQTT_DASHBOARD 1
|
#define MQTT_DASHBOARD 1
|
||||||
#endif
|
#define MQTT_KEEPALIVE_SEC 60
|
||||||
|
|
||||||
#define DEVICE_ID_LEN 10
|
|
||||||
#define MQTT_KEEP_ALIVE_INTERVAL 60
|
|
||||||
#define MQTT_SERVER_URL "mqtt://broker.hivemq.com:1883"
|
#define MQTT_SERVER_URL "mqtt://broker.hivemq.com:1883"
|
||||||
#define MQTT_ROOT_TOPIC "topic_mg_device"
|
#define MQTT_ROOT_TOPIC "mg_mqtt_dashboard"
|
||||||
|
#define NUM_PINS 30
|
||||||
|
|
||||||
extern char *g_url;
|
extern char *g_url;
|
||||||
extern char *g_device_id;
|
extern char *g_device_id;
|
||||||
extern char *g_root_topic;
|
extern char *g_root_topic;
|
||||||
|
|
||||||
void web_init(struct mg_mgr *mgr);
|
void web_init(struct mg_mgr *mgr);
|
||||||
void web_destroy();
|
void web_free(void);
|
||||||
|
|
||||||
void hal_gpio_write(int pin, bool status);
|
bool hal_gpio_write(int pin, bool status);
|
||||||
bool hal_gpio_read(int pin);
|
bool hal_gpio_read(int pin);
|
||||||
int hal_led_pin(void);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user