mirror of
https://github.com/cesanta/mongoose.git
synced 2025-01-15 02:08:08 +08:00
Merge pull request #2472 from cesanta/mqtt-dashboard
Added MQTT Dashboard
This commit is contained in:
commit
5ee8552e6c
@ -86,7 +86,7 @@ export function Notification({ok, text, close}) {
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<${ok ? Icons.ok : Icons.failed} class="h-6 w-6 ${ok ? 'text-green-400' : 'text-red-400'}" />
|
||||
<${ok ? Icons.ok : Icons.fail} class="h-6 w-6 ${ok ? 'text-green-400' : 'text-red-400'}" />
|
||||
<//>
|
||||
<div class="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p class="text-sm font-medium text-gray-900">${text}</p>
|
||||
|
15
examples/mqtt-dashboard/client/Makefile
Normal file
15
examples/mqtt-dashboard/client/Makefile
Normal file
@ -0,0 +1,15 @@
|
||||
PWD := $(shell pwd)
|
||||
|
||||
all: bundle.js main.css
|
||||
make -C ../../http-server ARGS="-d $(PWD)"
|
||||
|
||||
# Bundle JS libraries (preact, preact-router, ...) into a single file
|
||||
bundle.js:
|
||||
curl -s https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-router -o $@
|
||||
|
||||
# Create optimised CSS. Prerequisite: npm -g i tailwindcss tailwindcss-font-inter
|
||||
main.css: index.html $(wildcard *.js)
|
||||
npx tailwindcss -o $@ --minify
|
||||
|
||||
clean:
|
||||
true
|
1
examples/mqtt-dashboard/client/bundle.js
Symbolic link
1
examples/mqtt-dashboard/client/bundle.js
Symbolic link
@ -0,0 +1 @@
|
||||
../../device-dashboard/web_root/bundle.js
|
1
examples/mqtt-dashboard/client/components.js
Symbolic link
1
examples/mqtt-dashboard/client/components.js
Symbolic link
@ -0,0 +1 @@
|
||||
../../device-dashboard/web_root/components.js
|
1
examples/mqtt-dashboard/client/history.min.js
vendored
Symbolic link
1
examples/mqtt-dashboard/client/history.min.js
vendored
Symbolic link
@ -0,0 +1 @@
|
||||
../../device-dashboard/web_root/history.min.js
|
16
examples/mqtt-dashboard/client/index.html
Normal file
16
examples/mqtt-dashboard/client/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-white">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor'> <path stroke-linecap='round' stroke-linejoin='round' d='M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0' /> </svg>" />
|
||||
<link href="main.css" rel="stylesheet" />
|
||||
<link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
|
||||
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
||||
</head>
|
||||
<body class="h-full"></body>
|
||||
<script src="history.min.js"></script>
|
||||
<script type="module" src="main.js"></script>
|
||||
</html>
|
1
examples/mqtt-dashboard/client/main.css
Normal file
1
examples/mqtt-dashboard/client/main.css
Normal file
File diff suppressed because one or more lines are too long
532
examples/mqtt-dashboard/client/main.js
Normal file
532
examples/mqtt-dashboard/client/main.js
Normal file
@ -0,0 +1,532 @@
|
||||
'use strict';
|
||||
import { h, render, useState, useEffect, useRef, html } from './bundle.js';
|
||||
import { Icons, Setting, Button, tipColors, Colored, Notification} 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 url = 'ws://broker.hivemq.com:8000/mqtt'
|
||||
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 onClick = () => {
|
||||
const isValidTopic = (value) => {
|
||||
return !forbiddenChars.some(char => value.includes(char));
|
||||
};
|
||||
|
||||
if (isValidTopic(inputValue)) {
|
||||
localStorage.setItem('topic', inputValue)
|
||||
setTopicFn(inputValue);
|
||||
window.location.reload();
|
||||
} else {
|
||||
setSaveResult("Error: The topic cannot contain these characters: " + forbiddenChars)
|
||||
}
|
||||
};
|
||||
|
||||
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="mx-auto flex justify-between items-center px-5">
|
||||
<div class="text-gray-800 font-semibold tracking-wide">
|
||||
<h1 class="text-2xl">MQTT Dashboard</h1>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<div class="text-lg font-medium text-gray-700 text-center">
|
||||
<div class="text-gray-500">MQTT Server:</div>
|
||||
<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;">
|
||||
${url}
|
||||
</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 }) {
|
||||
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>`;
|
||||
|
||||
const Device = ({ d }) => html`
|
||||
<tr class="hover:bg-slate-100 cursor-pointer" onClick=${() => onClickFn(d.id)}>
|
||||
<td class="border-b-2 border-slate-300 py-2 text-center align-middle">
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<span>ID ${d.id}</span>
|
||||
<${Colored} colors=${d.online ? tipColors.green : tipColors.red} text=${d.online ? 'online' : 'offline'} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
return html`
|
||||
<div class="m-4 divide-y divide-gray-200 overflow-auto rounded bg-white">
|
||||
<div class="font-semibold flex items-center text-gray-600 px-3 justify-center whitespace-nowrap">
|
||||
<div class="font-bold flex items-center text-gray-600">
|
||||
Devices list
|
||||
</div>
|
||||
<//>
|
||||
<div class="inline-block min-w-full align-middle" style="max-height: 82vh; overflow: auto;">
|
||||
<table class="w-full table table-bordered">
|
||||
<tbody>
|
||||
${(devices ? devices : []).map(d => h(Device, { d }))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
function FirmwareStatus({title, info, children}) {
|
||||
const state = ['UNAVAILABLE', 'FIRST_BOOT', 'NOT_COMMITTED', 'COMMITTED'][(info.status || 0) % 4];
|
||||
const valid = info.status > 0;
|
||||
return html`
|
||||
<div class="bg-white divide-y border rounded">
|
||||
<div class="font-light bg-white uppercase flex items-center text-gray-600 px-4 py-2">
|
||||
${title}
|
||||
<//>
|
||||
<div class="px-4 py-2 relative">
|
||||
<div class="my-1">Status: ${state}<//>
|
||||
<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">Flashed at: ${valid ? new Date(info.timestamp * 1000).toLocaleString() : 'n/a'}<//>
|
||||
${children}
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
function UploadFileButton(props) {
|
||||
const [upload, setUpload] = useState(null); // Upload promise
|
||||
const [status, setStatus] = useState({}); // Current upload status
|
||||
const btn = useRef(null);
|
||||
const input = useRef(null);
|
||||
|
||||
const setStatusByID = function(message, id) {
|
||||
setStatus(prvStatus => ({...prvStatus, [id]: message}))
|
||||
};
|
||||
|
||||
// Send a large file chunk by chunk
|
||||
const sendFileData = function(fileName, fileData, chunkSize) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const finish = ok => {
|
||||
setUpload(null);
|
||||
const res = props.onupload ? props.onupload(ok, fileName, fileData.length) : null;
|
||||
if (res && typeof (res.catch) === 'function') {
|
||||
res.catch(() => false).then(() => ok ? resolve() : reject());
|
||||
} else {
|
||||
ok ? resolve() : reject();
|
||||
}
|
||||
};
|
||||
|
||||
const sendChunk = function(offset) {
|
||||
var chunk = fileData.subarray(offset, offset + chunkSize) || '';
|
||||
var ok;
|
||||
setStatusByID('Uploading ' + fileName + ', bytes ' + offset + '..' +
|
||||
(offset + chunk.length) + ' of ' + fileData.length, props.id);
|
||||
const params = {chunk: btoa(String.fromCharCode.apply(null, chunk)), offset: offset, total: fileData.length}
|
||||
props.publishFn("ota.upload", params)
|
||||
.then(function(res) {
|
||||
if (res.result === "ok" && chunk.length > 0) sendChunk(offset + chunk.length);
|
||||
ok = res.result === "ok";
|
||||
return res;
|
||||
})
|
||||
.then(function(res) {
|
||||
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);
|
||||
sendChunk(0);
|
||||
});
|
||||
};
|
||||
|
||||
const onchange = function(ev) {
|
||||
if (!ev.target.files[0]) return;
|
||||
let r = new FileReader(), f = ev.target.files[0];
|
||||
r.readAsArrayBuffer(f);
|
||||
r.onload = function() {
|
||||
setUpload(sendFileData(f.name, new Uint8Array(r.result), 2048));
|
||||
ev.target.value = '';
|
||||
ev.preventDefault();
|
||||
btn && btn.current.base.click();
|
||||
};
|
||||
};
|
||||
|
||||
const onclick = function(ev) {
|
||||
let fn; setUpload(x => fn = x);
|
||||
if (!fn) input.current.click(); // No upload in progress, show file dialog
|
||||
return fn;
|
||||
};
|
||||
|
||||
if (props.clean) {
|
||||
setStatusByID(null, props.id)
|
||||
props.setCleanFn(false)
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="inline-flex flex-col ${props.class}">
|
||||
<input class="hidden" type="file" ref=${input} onchange=${onchange} accept=${props.accept} />
|
||||
<${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 }) {
|
||||
const [clean, setClean] = useState(false)
|
||||
const refresh = () => {};
|
||||
useEffect(refresh, []);
|
||||
const oncommit = ev => { publishFn("ota.commit") };
|
||||
const onreboot = ev => { publishFn("device.reset")};
|
||||
const onrollback = ev => { publishFn("ota.rollback") };
|
||||
const onerase = ev => {};
|
||||
const onupload = function(ok, name, size) {
|
||||
if (!ok) return false;
|
||||
return new Promise(r => setTimeout(ev => { refresh(); r(); }, 3000)).then(r => setClean(true));
|
||||
};
|
||||
|
||||
const defaultInfo = {status: 0, crc32: 0, size: 0, timestamp: 0}
|
||||
return html`
|
||||
<div class="bg-slate-200 border rounded-md text-ellipsis overflow-auto">
|
||||
<div class="bg-slate-100 px-4 py-2 flex items-center justify-between font-light uppercase text-gray-600">
|
||||
Over-the-air firmware updates
|
||||
</div>
|
||||
<div class="gap-1 grid grid-cols-3 lg:grid-cols-3">
|
||||
<${FirmwareStatus} title="Current firmware image" info=${info[0] ? info[0] : defaultInfo}>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<${Button} title="Commit this firmware" onclick=${oncommit}
|
||||
icon=${Icons.thumbUp} disabled=${disabled} cls="w-full" />
|
||||
<//>
|
||||
<//>
|
||||
<${FirmwareStatus} title="Previous firmware image" info=${info[1] ? info[1] : defaultInfo}>
|
||||
<${Button} title="Rollback to this firmware" onclick=${onrollback}
|
||||
icon=${Icons.backward} disabled=${disabled} cls="w-full" />
|
||||
<//>
|
||||
<div class="bg-white xm-4 divide-y border rounded flex flex-col">
|
||||
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
|
||||
Device control
|
||||
<//>
|
||||
<div class="px-4 py-3 flex flex-col gap-2 grow">
|
||||
<${UploadFileButton}
|
||||
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" />
|
||||
<div class="grow"><//>
|
||||
<${Button} title="Reboot device" onclick=${onreboot} 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} ) {
|
||||
const [localConfig, setLocalConfig] = useState();
|
||||
const [saveResult, setSaveResult] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
useEffect(() => {
|
||||
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 mksetfn = k => (v => setLocalConfig(x => Object.assign({}, x, { [k]: v })));
|
||||
|
||||
const onSave = () => {
|
||||
setSaving(true)
|
||||
publishFn("config.set", localConfig).then(r => {
|
||||
setDeviceConfig(deviceData.id, localConfig)
|
||||
setSaveResult("Success!")
|
||||
setSaving(false)
|
||||
}).catch(e => {
|
||||
setDeviceConfig(deviceData.id, deviceData.config)
|
||||
setSaveResult("Failed!")
|
||||
setSaving(false)
|
||||
})
|
||||
};
|
||||
|
||||
if (!deviceData || !localConfig) {
|
||||
return ``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="m-4 space-y-2">
|
||||
<h3 class="text-lg font-semibold mb-3">Device ${deviceData.id} Configuration Panel</h3>
|
||||
<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">
|
||||
<span class="mr-2">
|
||||
Status: ${html`<${Colored} colors=${deviceData.online ? tipColors.green : tipColors.red} text=${deviceData.online ? 'online' : 'offline'} />`}
|
||||
</span>
|
||||
<//>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<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">
|
||||
<${Setting} title="LED status" value=${localConfig.led_status} setfn=${mksetfn('led_status')} type="switch" disabled=${!deviceData.online} />
|
||||
<${Setting} title="LED Pin" type="number" value=${localConfig.led_pin} setfn=${mksetfn('led_pin')} disabled=${!deviceData.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">
|
||||
${saveResult && html`<${Notification} ok=${saveResult === "Success!"}
|
||||
text=${saveResult} close=${() => setSaveResult(null)} />`}
|
||||
<${Button} icon=${Icons.save} onclick=${onSave} title=${saving ? "Saving..." : "Save Settings"} disabled=${!deviceData.online || saving} />
|
||||
</div>
|
||||
<//>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<${FirmwareUpdate} deviceID=${deviceData.id} publishFn=${publishFn} disabled=${!deviceData.online} info=${[localConfig.crnt_fw, localConfig.prev_fw]} />
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const App = function() {
|
||||
const [devices, setDevices] = useState([]);
|
||||
const [currentDevID, setCurrentDevID] = useState(localStorage.getItem('currentDevID') || '');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [topic, setTopic] = useState(localStorage.getItem('topic') || default_topic);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const responseHandlers = useRef({});
|
||||
let connSuccessful = false;
|
||||
|
||||
function addResponseHandler(correlationId, handler) {
|
||||
responseHandlers[correlationId] = handler;
|
||||
}
|
||||
|
||||
function removeResponseHandler(correlationId) {
|
||||
delete responseHandlers[correlationId];
|
||||
}
|
||||
|
||||
const onRefresh = () => {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
const initConn = () => {
|
||||
client = mqtt.connect(url, {
|
||||
connectTimeout: 5000,
|
||||
reconnectPeriod: 0
|
||||
});
|
||||
|
||||
client.on('connect', () => {
|
||||
console.log('Connected to the broker');
|
||||
setLoading(false);
|
||||
setError(null); // Reset error state upon successful connection
|
||||
connSuccessful = true;
|
||||
|
||||
const statusTopic = topic + '/+/status'
|
||||
const txTopic = topic + '/+/tx'
|
||||
|
||||
const subscribe = (topic) => {
|
||||
client.subscribe(topic, (err) => {
|
||||
if (err) {
|
||||
console.error('Error subscribing to topic:', err);
|
||||
setError('Error subscribing to topic');
|
||||
} else {
|
||||
console.log('Successfully subscribed to ', topic);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
subscribe(statusTopic)
|
||||
subscribe(txTopic)
|
||||
});
|
||||
|
||||
client.on('message', (topic, message) => {
|
||||
//console.log(`Received message from ${topic}: ${message.toString()}`);
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(message.toString());
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return;
|
||||
}
|
||||
|
||||
if (topic.endsWith("/status")) {
|
||||
const deviceID = topic.split('/')[1]
|
||||
let device = {};
|
||||
device.id = deviceID;
|
||||
const params = response.params
|
||||
if (!params) {
|
||||
console.error("Invalid response")
|
||||
return
|
||||
}
|
||||
device.online = params.status === "online"
|
||||
if (device.online) {
|
||||
device.config = {}
|
||||
device.config.led_status = params.led_status
|
||||
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)
|
||||
} else if (topic.endsWith("/tx")) {
|
||||
if (!response.id) {
|
||||
console.error("Invalid response")
|
||||
return
|
||||
}
|
||||
const handler = responseHandlers[response.id];
|
||||
if (handler) {
|
||||
handler(response);
|
||||
removeResponseHandler(response.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
console.error('Connection error:', err);
|
||||
setError('Connection cannot be established.');
|
||||
});
|
||||
|
||||
client.on('close', () => {
|
||||
if (!connSuccessful) {
|
||||
console.error('Failed to connect to the broker.');
|
||||
setError('Connection cannot be established.');
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initConn();
|
||||
}, []);
|
||||
|
||||
const handlePublish = (methodName, parameters, timeout = 5000) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const randomIdGenerator = function(length) {
|
||||
return Math.random().toString(36).substring(2, length+2);
|
||||
};
|
||||
const randomID = randomIdGenerator(40);
|
||||
const timeoutID = setTimeout(() => {
|
||||
removeResponseHandler(randomID);
|
||||
reject(new Error('Request timed out'));
|
||||
}, timeout);
|
||||
|
||||
addResponseHandler(randomID, (messageData) => {
|
||||
clearTimeout(timeoutID);
|
||||
resolve(messageData);
|
||||
});
|
||||
|
||||
if (currentDevID) {
|
||||
const rxTopic = topic + `/${currentDevID}/rx`;
|
||||
const rpcPayload = {
|
||||
method: methodName,
|
||||
id: randomID
|
||||
};
|
||||
|
||||
if (parameters) {
|
||||
rpcPayload.params = parameters;
|
||||
}
|
||||
|
||||
client.publish(rxTopic, JSON.stringify(rpcPayload));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getDeviceByID = (deviceID) => {
|
||||
return devices.find(d => d.id === deviceID);
|
||||
}
|
||||
|
||||
const setDevice = (devData) => {
|
||||
setDevices(prevDevices => {
|
||||
const devIndex = prevDevices.findIndex(device => device.id === devData.id);
|
||||
if (devIndex !== -1) {
|
||||
if (!devData.online && !devData.config) {
|
||||
const updatedDevices = [...prevDevices];
|
||||
updatedDevices[devIndex].online = false;
|
||||
return updatedDevices;
|
||||
} else {
|
||||
return prevDevices.map(device => device.id === devData.id ? devData : device);
|
||||
}
|
||||
} else {
|
||||
return [...prevDevices, devData];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setDeviceConfig = (deviceID, config) => {
|
||||
setDevices(prevDevices => {
|
||||
return prevDevices.map(device => {
|
||||
if (device.id === deviceID) {
|
||||
return {
|
||||
...device,
|
||||
config: config
|
||||
};
|
||||
}
|
||||
return device;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onDeviceClick = (deviceID) => {
|
||||
const device = getDeviceByID(deviceID);
|
||||
if (device) {
|
||||
setCurrentDevID(device.id);
|
||||
localStorage.setItem('currentDevID', device.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return html`
|
||||
<div class="min-h-screen bg-slate-100 flex flex-col">
|
||||
<${Header}/>
|
||||
<div class="flex-grow flex items-center justify-center">
|
||||
<div class="bg-slate-300 p-8 rounded-lg shadow-md w-full max-w-md">
|
||||
<h1 class="text-2xl font-bold text-gray-700 mb-4 text-center">Connection Error</h1>
|
||||
<p class="text-gray-600 text-center mb-4">
|
||||
Unable to connect to the MQTT broker.
|
||||
</p>
|
||||
<div class="text-center relative">
|
||||
<${Button} title="Retry" onclick=${onRefresh} icon=${Icons.refresh} class="absolute top-4 right-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="min-h-screen bg-slate-100 flex flex-col space-y-2">
|
||||
<${Header} topic=${topic} setTopicFn=${setTopic}/>
|
||||
<div class="transition-all duration-300 transform flex flex-grow gap-4">
|
||||
<div class="flex-none bg-white rounded shadow ">
|
||||
<${Devices} devices=${devices} onClickFn=${onDeviceClick} />
|
||||
<//>
|
||||
<div class="flex-1 flex-grow bg-white rounded shadow h-full">
|
||||
<${Config} deviceData=${getDeviceByID(currentDevID)} setDeviceConfig=${setDeviceConfig} publishFn=${handlePublish}/>
|
||||
<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
window.onload = () => render(h(App), document.body);
|
17
examples/mqtt-dashboard/client/tailwind.config.js
Normal file
17
examples/mqtt-dashboard/client/tailwind.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
content: ['./*.{html,js}'],
|
||||
xplugins: [ 'tailwindcss', 'xautoprefixer' ],
|
||||
corePlugins: {outline: false},
|
||||
theme: {
|
||||
extend: {},
|
||||
fontFamily: {
|
||||
sans:
|
||||
[
|
||||
"Inter var, Helvetica, sans-serif", {
|
||||
fontFeatureSettings: '"cv11", "ss01"',
|
||||
fontVariationSettings: '"opsz" 32',
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
32
examples/mqtt-dashboard/device/Makefile
Normal file
32
examples/mqtt-dashboard/device/Makefile
Normal file
@ -0,0 +1,32 @@
|
||||
PROG ?= example # Program we are building
|
||||
DELETE = rm -rf # Command to remove files
|
||||
OUT ?= -o $(PROG) # Compiler argument for output file
|
||||
SOURCES = main.c mongoose.c # Source code files
|
||||
CFLAGS = -W -Wall -Wextra -g -I. # Build options
|
||||
|
||||
# Mongoose build options. See https://mongoose.ws/documentation/#build-options
|
||||
CFLAGS_MONGOOSE += -DMG_ENABLE_LINES=1
|
||||
|
||||
ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe
|
||||
PROG ?= example.exe # Use .exe suffix for the binary
|
||||
CC = gcc # Use MinGW gcc compiler
|
||||
CFLAGS += -lws2_32 # Link against Winsock library
|
||||
DELETE = cmd /C del /Q /F /S # Command prompt command to delete files
|
||||
OUT ?= -o $(PROG) # Build output
|
||||
MAKE += WINDOWS=1 CC=$(CC)
|
||||
endif
|
||||
|
||||
all: $(PROG) # Default target. Build and run program
|
||||
$(RUN) ./$(PROG) $(ARGS)
|
||||
|
||||
$(PROG): $(SOURCES) # Build program from sources
|
||||
$(CC) $(SOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(OUT)
|
||||
|
||||
clean: # Cleanup. Delete built program and all build artifacts
|
||||
$(DELETE) $(PROG) *.o *.obj *.exe *.dSYM mbedtls
|
||||
|
||||
# see https://mongoose.ws/tutorials/tls/#how-to-build for TLS build options
|
||||
|
||||
mbedtls: # Pull and build mbedTLS library
|
||||
git clone --depth 1 -b v2.28.2 https://github.com/mbed-tls/mbedtls $@
|
||||
$(MAKE) -C mbedtls/library
|
0
examples/mqtt-dashboard/device/README.md
Normal file
0
examples/mqtt-dashboard/device/README.md
Normal file
313
examples/mqtt-dashboard/device/main.c
Normal file
313
examples/mqtt-dashboard/device/main.c
Normal file
@ -0,0 +1,313 @@
|
||||
// Copyright (c) 2023 Cesanta Software Limited
|
||||
// All rights reserved
|
||||
//
|
||||
// Example MQTT client. It performs the following steps:
|
||||
//
|
||||
// To enable SSL/TLS, see https://mongoose.ws/tutorials/tls/#how-to-build
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
#define DEVICE_ID_LEN 10
|
||||
#define ROOT_TOPIC_LEN 30
|
||||
#define KEEP_ALIVE_INTERVAL 60
|
||||
#define MQTT_SERVER_URL "mqtt://broker.hivemq.com:1883"
|
||||
#define DEFAULT_ROOT_TOPIC "topic_mg_device"
|
||||
|
||||
static const char *s_url;
|
||||
static char *s_device_id;
|
||||
static const char *s_root_topic;
|
||||
static int s_qos = 1; // MQTT QoS
|
||||
static struct mg_connection *s_conn; // Client connection
|
||||
static struct mg_rpc *s_rpc_head = NULL;
|
||||
|
||||
struct device_config {
|
||||
bool led_status;
|
||||
uint8_t led_pin;
|
||||
uint8_t brightness;
|
||||
uint8_t log_level;
|
||||
};
|
||||
|
||||
static struct device_config s_device_config;
|
||||
|
||||
// Handle interrupts, like Ctrl-C
|
||||
static int s_signo;
|
||||
static void signal_handler(int signo) {
|
||||
s_signo = signo;
|
||||
}
|
||||
|
||||
static void generate_device_id(void) {
|
||||
char tmp[DEVICE_ID_LEN + 1];
|
||||
mg_random_str(tmp, DEVICE_ID_LEN);
|
||||
s_device_id = strdup(tmp);
|
||||
}
|
||||
|
||||
static size_t print_fw_status(void (*out)(char, void *), void *ptr,
|
||||
va_list *ap) {
|
||||
int fw = va_arg(*ap, int);
|
||||
return mg_xprintf(out, ptr, "{%m:%d,%m:%c%lx%c,%m:%u,%m:%u}",
|
||||
MG_ESC("status"), mg_ota_status(fw), MG_ESC("crc32"), '"',
|
||||
mg_ota_crc32(fw), '"', MG_ESC("size"), mg_ota_size(fw),
|
||||
MG_ESC("timestamp"), mg_ota_timestamp(fw));
|
||||
}
|
||||
|
||||
static void publish_status(struct mg_connection *c) {
|
||||
int status_topic_len = 50;
|
||||
char* status_topic = calloc(status_topic_len, sizeof(char));
|
||||
if (!status_topic) {
|
||||
MG_ERROR(("Out of memory"));
|
||||
return;
|
||||
}
|
||||
mg_snprintf(status_topic, status_topic_len, "%s/%s/status", s_root_topic,
|
||||
s_device_id);
|
||||
struct mg_str pubt = mg_str(status_topic);
|
||||
struct mg_mqtt_opts pub_opts;
|
||||
memset(&pub_opts, 0, sizeof(pub_opts));
|
||||
pub_opts.topic = pubt;
|
||||
int json_len = 400;
|
||||
char *device_status_json;
|
||||
device_status_json = calloc(json_len, sizeof(char));
|
||||
if (!device_status_json) {
|
||||
MG_ERROR(("Out of memory"));
|
||||
return;
|
||||
}
|
||||
mg_snprintf(device_status_json, json_len,
|
||||
"{%m:%m,%m:{%m:%m,%m:%s,%m:%hhu,%m:%hhu,%m:%hhu,%m:%M,%m:%M}}",
|
||||
MG_ESC("method"), MG_ESC("status.notify"), MG_ESC("params"),
|
||||
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_INFO(("%lu PUBLISHED %.*s -> %.*s", c->id, (int) data.len, data.ptr,
|
||||
(int) pubt.len, pubt.ptr));
|
||||
free(device_status_json);
|
||||
free(status_topic);
|
||||
}
|
||||
|
||||
static void publish_response(struct mg_connection *c, char *buf, size_t len) {
|
||||
int tx_topic_len = 50;
|
||||
char* tx_topic = calloc(tx_topic_len, sizeof(char));
|
||||
if (!tx_topic) {
|
||||
MG_ERROR(("Out of memory"));
|
||||
return;
|
||||
}
|
||||
mg_snprintf(tx_topic, tx_topic_len, "%s/%s/tx", s_root_topic,
|
||||
s_device_id);
|
||||
struct mg_str pubt = mg_str(tx_topic);
|
||||
struct mg_mqtt_opts pub_opts;
|
||||
memset(&pub_opts, 0, sizeof(pub_opts));
|
||||
pub_opts.topic = pubt;
|
||||
struct mg_str data = mg_str_n(buf, len);
|
||||
pub_opts.message = data;
|
||||
pub_opts.qos = s_qos;
|
||||
mg_mqtt_pub(c, &pub_opts);
|
||||
MG_INFO(("%lu PUBLISHED %.*s -> %.*s", c->id, (int) data.len, data.ptr,
|
||||
(int) pubt.len, pubt.ptr));
|
||||
free(tx_topic);
|
||||
}
|
||||
|
||||
static void subscribe(struct mg_connection *c) {
|
||||
int rx_topic_len = 50;
|
||||
char* rx_topic = calloc(rx_topic_len, sizeof(char));
|
||||
if (!rx_topic) {
|
||||
MG_ERROR(("Out of memory"));
|
||||
return;
|
||||
}
|
||||
mg_snprintf(rx_topic, rx_topic_len, "%s/%s/rx", s_root_topic,
|
||||
s_device_id);
|
||||
struct mg_str subt = mg_str(rx_topic);
|
||||
struct mg_mqtt_opts sub_opts;
|
||||
memset(&sub_opts, 0, sizeof(sub_opts));
|
||||
sub_opts.topic = subt;
|
||||
sub_opts.qos = s_qos;
|
||||
mg_mqtt_sub(c, &sub_opts);
|
||||
MG_INFO(("%lu SUBSCRIBED to %.*s", c->id, (int) subt.len, subt.ptr));
|
||||
free(rx_topic);
|
||||
}
|
||||
|
||||
static void rpc_config_set(struct mg_rpc_req *r) {
|
||||
bool tmp_status, ok;
|
||||
long tmp_brightness, tmp_level, tmp_pin;
|
||||
|
||||
ok = mg_json_get_bool(r->frame, "$.params.led_status", &tmp_status);
|
||||
if (ok) s_device_config.led_status = tmp_status;
|
||||
|
||||
tmp_brightness = mg_json_get_long(r->frame, "$.params.brightness", -1);
|
||||
if (tmp_brightness >= 0) s_device_config.brightness = tmp_brightness;
|
||||
|
||||
tmp_level = mg_json_get_long(r->frame, "$.params.log_level", -1);
|
||||
if (tmp_level >= 0) s_device_config.log_level = tmp_level;
|
||||
|
||||
tmp_pin = mg_json_get_long(r->frame, "$.params.led_pin", -1);
|
||||
if (tmp_pin > 0) s_device_config.led_pin = tmp_pin;
|
||||
|
||||
mg_rpc_ok(r, "%m", MG_ESC("ok"));
|
||||
}
|
||||
|
||||
static void rpc_ota_commit(struct mg_rpc_req *r) {
|
||||
if (mg_ota_commit()) {
|
||||
mg_rpc_ok(r, "%m", MG_ESC("ok"));
|
||||
} else {
|
||||
mg_rpc_err(r, 1, "Failed to commit the firmware");
|
||||
}
|
||||
}
|
||||
|
||||
static void rpc_device_reset(struct mg_rpc_req *r) {
|
||||
mg_rpc_ok(r, "%m", MG_ESC("ok"));
|
||||
}
|
||||
|
||||
static void rpc_ota_rollback(struct mg_rpc_req *r) {
|
||||
if (mg_ota_rollback()) {
|
||||
mg_rpc_ok(r, "%m", MG_ESC("ok"));
|
||||
} else {
|
||||
mg_rpc_err(r, 1, "Failed to rollback to the previous firmware");
|
||||
}
|
||||
}
|
||||
|
||||
static void rpc_ota_upload(struct mg_rpc_req *r) {
|
||||
long ofs = mg_json_get_long(r->frame, "$.params.offset", -1);
|
||||
long tot = mg_json_get_long(r->frame, "$.params.total", -1);
|
||||
int len;
|
||||
char *file_chunk = mg_json_get_b64(r->frame, "$.params.chunk", &len);
|
||||
if (!file_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 {
|
||||
mg_rpc_ok(r, "%m", MG_ESC("ok"));
|
||||
if (data.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(file_chunk);
|
||||
}
|
||||
|
||||
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
||||
if (ev == MG_EV_OPEN) {
|
||||
MG_INFO(("%lu CREATED", c->id));
|
||||
// c->is_hexdumping = 1;
|
||||
} else if (ev == MG_EV_CONNECT) {
|
||||
MG_INFO(("Device ID is connected %s", s_device_id));
|
||||
} else if (ev == MG_EV_ERROR) {
|
||||
// On error, log error message
|
||||
MG_ERROR(("%lu ERROR %s", c->id, (char *) ev_data));
|
||||
} else if (ev == MG_EV_MQTT_OPEN) {
|
||||
// MQTT connect is successful
|
||||
MG_INFO(("%lu CONNECTED to %s", c->id, s_url));
|
||||
subscribe(c);
|
||||
publish_status(c);
|
||||
} else if (ev == MG_EV_MQTT_MSG) {
|
||||
// When we get echo response, print it
|
||||
struct mg_mqtt_message *mm = (struct mg_mqtt_message *) ev_data;
|
||||
if (!mg_strcmp(mm->topic, mg_str("ota.upload"))) {
|
||||
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_rpc_req r = {&s_rpc_head, 0, mg_pfn_iobuf, &io, 0, mm->data};
|
||||
mg_rpc_process(&r);
|
||||
if (io.buf) {
|
||||
publish_response(c, (char *) io.buf, io.len);
|
||||
publish_status(c);
|
||||
}
|
||||
mg_iobuf_free(&io);
|
||||
} else if (ev == MG_EV_CLOSE) {
|
||||
MG_INFO(("%lu CLOSED", c->id));
|
||||
s_conn = NULL; // Mark that we're closed
|
||||
}
|
||||
(void) fn_data;
|
||||
}
|
||||
|
||||
// Timer function - recreate client connection if it is closed
|
||||
static void timer_fn(void *arg) {
|
||||
struct mg_mgr *mgr = (struct mg_mgr *) arg;
|
||||
char status_topic[50];
|
||||
memset(status_topic, 0, sizeof(status_topic));
|
||||
mg_snprintf(status_topic, sizeof(status_topic), "%s/%s/status", s_root_topic,
|
||||
s_device_id);
|
||||
char msg[200];
|
||||
memset(msg, 0, sizeof(msg));
|
||||
mg_snprintf(msg, sizeof(msg), "{%m:%m,%m:{%m:%m}}", MG_ESC("method"),
|
||||
MG_ESC("status.notify"), MG_ESC("params"), MG_ESC("status"),
|
||||
MG_ESC("offline"));
|
||||
|
||||
struct mg_mqtt_opts opts = {.clean = true,
|
||||
.qos = s_qos,
|
||||
.topic = mg_str(status_topic),
|
||||
.version = 4,
|
||||
.keepalive = KEEP_ALIVE_INTERVAL,
|
||||
.retain = true,
|
||||
.message = mg_str(msg)};
|
||||
if (s_conn == NULL) s_conn = mg_mqtt_connect(mgr, s_url, &opts, fn, NULL);
|
||||
}
|
||||
|
||||
static void timer_keepalive(void *arg) {
|
||||
mg_mqtt_send_header(s_conn, MQTT_CMD_PINGREQ, 0, 0);
|
||||
(void) arg;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
struct mg_mgr mgr;
|
||||
int i;
|
||||
int pingreq_interval_ms = KEEP_ALIVE_INTERVAL * 1000 - 500;
|
||||
s_url = MQTT_SERVER_URL;
|
||||
s_root_topic = DEFAULT_ROOT_TOPIC;
|
||||
|
||||
// Parse command-line flags
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-u") == 0 && argv[i + 1] != NULL) {
|
||||
s_url = argv[++i];
|
||||
} else if (strcmp(argv[i], "-i") == 0 && argv[i + 1] != NULL) {
|
||||
s_device_id = strdup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "-t") == 0 && argv[i + 1] != NULL) {
|
||||
s_root_topic = argv[++i];
|
||||
} else if (strcmp(argv[i], "-v") == 0 && argv[i + 1] != NULL) {
|
||||
mg_log_set(atoi(argv[++i]));
|
||||
} else {
|
||||
MG_ERROR(("Unknown option: %s. Usage:", argv[i]));
|
||||
MG_ERROR(("%s [-u mqtts://SERVER:PORT] [-i DEVICE_ID] [-t TOPIC_NAME] [-v DEBUG_LEVEL]",
|
||||
argv[0], argv[i]));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
signal(SIGINT, signal_handler); // Setup signal handlers - exist event
|
||||
signal(SIGTERM, signal_handler); // manager loop on SIGINT and SIGTERM
|
||||
|
||||
if (!s_device_id) generate_device_id();
|
||||
|
||||
// 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_head, 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_head, mg_str("ota.rollback"), rpc_ota_rollback, NULL);
|
||||
mg_rpc_add(&s_rpc_head, mg_str("ota.upload"), rpc_ota_upload, NULL);
|
||||
|
||||
mg_mgr_init(&mgr);
|
||||
mg_timer_add(&mgr, 3000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn, &mgr);
|
||||
mg_timer_add(&mgr, pingreq_interval_ms, MG_TIMER_REPEAT, timer_keepalive,
|
||||
&mgr);
|
||||
while (s_signo == 0) mg_mgr_poll(&mgr, 1000); // Event loop, 1s timeout
|
||||
mg_mgr_free(&mgr); // Finished, cleanup
|
||||
mg_rpc_del(&s_rpc_head, NULL); // Deallocate RPC handlers
|
||||
free(s_device_id);
|
||||
return 0;
|
||||
}
|
1
examples/mqtt-dashboard/device/mongoose.c
Symbolic link
1
examples/mqtt-dashboard/device/mongoose.c
Symbolic link
@ -0,0 +1 @@
|
||||
../../../mongoose.c
|
1
examples/mqtt-dashboard/device/mongoose.h
Symbolic link
1
examples/mqtt-dashboard/device/mongoose.h
Symbolic link
@ -0,0 +1 @@
|
||||
../../../mongoose.h
|
Loading…
x
Reference in New Issue
Block a user