<${Button} title=${props.title} icon=${Icons.download} onclick=${onclick} ref=${btn} colors=${props.colors} disabled=${props.disabled} />
${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`
Over-the-air firmware updates
<${FirmwareStatus} title="Current firmware image" info=${info[0] ? info[0] : defaultInfo}>
<${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" />
/>
Device control
/>
<${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" />
/>
<${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`
Device ${deviceData.id} Configuration Panel
Status: ${html`<${Colored} colors=${deviceData.online ? tipColors.green : tipColors.red} text=${deviceData.online ? 'online' : 'offline'} />`}
/>
LED Settings
/>
<${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} />
/>
Log & Display
/>
<${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} />
${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} />
/>
<${FirmwareUpdate} deviceID=${deviceData.id} publishFn=${publishFn} disabled=${!deviceData.online} info=${[localConfig.crnt_fw, localConfig.prev_fw]} />
`;
}
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`
<${Header}/>
Connection Error
Unable to connect to the MQTT broker.
<${Button} title="Retry" onclick=${onRefresh} icon=${Icons.refresh} class="absolute top-4 right-4" />
`;
}
return html`
<${Header} topic=${topic} setTopicFn=${setTopic}/>