From 8a9b1d9873fd9bed10c6fb97dc2691c1f352c082 Mon Sep 17 00:00:00 2001 From: Sergey Lyubka Date: Wed, 9 Jul 2025 00:21:25 +0100 Subject: [PATCH] Simplify dashboard --- .../mqtt/mqtt-dashboard/dashboard/Makefile | 2 +- .../mqtt/mqtt-dashboard/dashboard/bundle.js | 2 +- .../mqtt/mqtt-dashboard/dashboard/index.html | 5 +- .../mqtt/mqtt-dashboard/dashboard/main.css | 2 +- .../mqtt/mqtt-dashboard/dashboard/main.js | 558 ++++++------------ tutorials/mqtt/mqtt-dashboard/device/main.c | 5 +- tutorials/mqtt/mqtt-dashboard/device/net.c | 108 +--- tutorials/mqtt/mqtt-dashboard/device/net.h | 1 + 8 files changed, 231 insertions(+), 452 deletions(-) mode change 120000 => 100644 tutorials/mqtt/mqtt-dashboard/dashboard/bundle.js diff --git a/tutorials/mqtt/mqtt-dashboard/dashboard/Makefile b/tutorials/mqtt/mqtt-dashboard/dashboard/Makefile index da015c3c..3fbd74c0 100644 --- a/tutorials/mqtt/mqtt-dashboard/dashboard/Makefile +++ b/tutorials/mqtt/mqtt-dashboard/dashboard/Makefile @@ -3,7 +3,7 @@ all: # 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 $@ + curl -s https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-router,@preact/signals-core,@preact/signals -o $@ # Create optimised CSS. Prerequisite: npm -g i tailwindcss tailwindcss-font-inter NPX ?= npx diff --git a/tutorials/mqtt/mqtt-dashboard/dashboard/bundle.js b/tutorials/mqtt/mqtt-dashboard/dashboard/bundle.js deleted file mode 120000 index 31d0e0b5..00000000 --- a/tutorials/mqtt/mqtt-dashboard/dashboard/bundle.js +++ /dev/null @@ -1 +0,0 @@ -../../../http/device-dashboard/web_root/bundle.js \ No newline at end of file diff --git a/tutorials/mqtt/mqtt-dashboard/dashboard/bundle.js b/tutorials/mqtt/mqtt-dashboard/dashboard/bundle.js new file mode 100644 index 00000000..48eef8f4 --- /dev/null +++ b/tutorials/mqtt/mqtt-dashboard/dashboard/bundle.js @@ -0,0 +1 @@ +var t,n,e,i,o,r,_,u,l,f,s,c,a,h={},p=[],d=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,v=Array.isArray;function y(t,n){for(var e in n)t[e]=n[e];return t}function m(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function g(n,e,i){var o,r,_,u={};for(_ in e)"key"==_?o=e[_]:"ref"==_?r=e[_]:u[_]=e[_];if(arguments.length>2&&(u.children=arguments.length>3?t.call(arguments,2):i),"function"==typeof n&&null!=n.defaultProps)for(_ in n.defaultProps)void 0===u[_]&&(u[_]=n.defaultProps[_]);return b(n,u,o,r,null)}function b(t,i,o,r,_){var u={type:t,props:i,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:null==_?++e:_,__i:-1,__u:0};return null==_&&null!=n.vnode&&n.vnode(u),u}function k(){return{current:null}}function w(t){return t.children}function x(t,n){this.props=t,this.context=n}function S(t,n){if(null==n)return t.__?S(t.__,t.__i+1):null;for(var e;ns&&o.sort(u),t=o.shift(),s=o.length,t.__d&&(i=void 0,_=(r=(e=t).__v).__e,l=[],f=[],e.__P&&((i=y({},r)).__v=r.__v+1,n.vnode&&n.vnode(i),F(e.__P,i,r,e.__n,e.__P.namespaceURI,32&r.__u?[_]:null,l,null==_?S(r):_,!!(32&r.__u),f),i.__v=r.__v,i.__.__k[i.__i]=i,R(l,i,f),i.__e!=_&&C(i)));E.__r=0}function P(t,n,e,i,o,r,_,u,l,f,s){var c,a,d,v,y,m,g=i&&i.__k||p,b=n.length;for(l=A(e,n,g,l,b),c=0;c0?b(_.type,_.props,_.key,_.ref?_.ref:null,_.__v):_).__=t,_.__b=t.__b+1,u=null,-1!=(f=_.__i=$(_,e,l,c))&&(c--,(u=e[f])&&(u.__u|=2)),null==u||null==u.__v?(-1==f&&(o>s?a--:ol?a--:a++,_.__u|=4))):t.__k[r]=null;if(c)for(r=0;r(null!=l&&0==(2&l.__u)?1:0))for(o=e-1,r=e+1;o>=0||r=0){if((l=n[o])&&0==(2&l.__u)&&_==l.key&&u==l.type)return o;o--}if(r0?t:v(t)?t.map(W):y({},t)}function O(e,i,o,r,_,u,l,f,s){var c,a,p,d,y,g,b,k=o.props,w=i.props,x=i.type;if("svg"==x?_="http://www.w3.org/2000/svg":"math"==x?_="http://www.w3.org/1998/Math/MathML":_||(_="http://www.w3.org/1999/xhtml"),null!=u)for(c=0;c2&&(l.children=arguments.length>3?t.call(arguments,2):i),b(n.type,l,o||n.key,r||n.ref,null)}function K(t){function n(t){var e,i;return this.getChildContext||(e=new Set,(i={})[n.__c]=this,this.getChildContext=function(){return i},this.componentWillUnmount=function(){e=null},this.shouldComponentUpdate=function(t){this.props.value!=t.value&&e.forEach((function(t){t.__e=!0,U(t)}))},this.sub=function(t){e.add(t);var n=t.componentWillUnmount;t.componentWillUnmount=function(){e&&e.delete(t),n&&n.call(t)}}),t.children}return n.__c="__cC"+a++,n.__=t,n.Provider=n.__l=(n.Consumer=function(t,n){return t.children(n)}).contextType=n,n}t=p.slice,n={__e:function(t,n,e,i){for(var o,r,_;n=n.__;)if((o=n.__c)&&!o.__)try{if((r=o.constructor)&&null!=r.getDerivedStateFromError&&(o.setState(r.getDerivedStateFromError(t)),_=o.__d),null!=o.componentDidCatch&&(o.componentDidCatch(t,i||{}),_=o.__d),_)return o.__E=o}catch(n){t=n}throw t}},e=0,i=function(t){return null!=t&&null==t.constructor},x.prototype.setState=function(t,n){var e;e=null!=this.__s&&this.__s!=this.state?this.__s:this.__s=y({},this.state),"function"==typeof t&&(t=t(y({},e),this.props)),t&&y(e,t),null!=t&&this.__v&&(n&&this._sb.push(n),U(this))},x.prototype.forceUpdate=function(t){this.__v&&(this.__e=!0,t&&this.__h.push(t),U(this))},x.prototype.render=w,o=[],_="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,u=function(t,n){return t.__v.__b-n.__v.__b},E.__r=0,l=/(PointerCapture)$|Capture$/i,f=0,s=M(!1),c=M(!0),a=0;var Z,z,G,J,Q=0,X=[],Y=n,tt=Y.__b,nt=Y.__r,et=Y.diffed,it=Y.__c,ot=Y.unmount,rt=Y.__;function _t(t,n){Y.__h&&Y.__h(z,t,Q||n),Q=0;var e=z.__H||(z.__H={__:[],__h:[]});return t>=e.__.length&&e.__.push({}),e.__[t]}function ut(t){return Q=1,lt(Ct,t)}function lt(t,n,e){var i=_t(Z++,2);if(i.t=t,!i.__c&&(i.__=[e?e(n):Ct(void 0,n),function(t){var n=i.__N?i.__N[0]:i.__[0],e=i.t(n,t);n!==e&&(i.__N=[e,i.__[1]],i.__c.setState({}))}],i.__c=z,!z.__f)){var o=function(t,n,e){if(!i.__c.__H)return!0;var o=i.__c.__H.__.filter((function(t){return!!t.__c}));if(o.every((function(t){return!t.__N})))return!r||r.call(this,t,n,e);var _=i.__c.props!==t;return o.forEach((function(t){if(t.__N){var n=t.__[0];t.__=t.__N,t.__N=void 0,n!==t.__[0]&&(_=!0)}})),r&&r.call(this,t,n,e)||_};z.__f=!0;var r=z.shouldComponentUpdate,_=z.componentWillUpdate;z.componentWillUpdate=function(t,n,e){if(this.__e){var i=r;r=void 0,o(t,n,e),r=i}_&&_.call(this,t,n,e)},z.shouldComponentUpdate=o}return i.__N||i.__}function ft(t,n){var e=_t(Z++,3);!Y.__s&&St(e.__H,n)&&(e.__=t,e.u=n,z.__H.__h.push(e))}function st(t,n){var e=_t(Z++,4);!Y.__s&&St(e.__H,n)&&(e.__=t,e.u=n,z.__h.push(e))}function ct(t){return Q=5,ht((function(){return{current:t}}),[])}function at(t,n,e){Q=6,st((function(){if("function"==typeof t){var e=t(n());return function(){t(null),e&&"function"==typeof e&&e()}}if(t)return t.current=n(),function(){return t.current=null}}),null==e?e:e.concat(t))}function ht(t,n){var e=_t(Z++,7);return St(e.__H,n)&&(e.__=t(),e.__H=n,e.__h=t),e.__}function pt(t,n){return Q=8,ht((function(){return t}),n)}function dt(t){var n=z.context[t.__c],e=_t(Z++,9);return e.c=t,n?(null==e.__&&(e.__=!0,n.sub(z)),n.props.value):t.__}function vt(t,n){Y.useDebugValue&&Y.useDebugValue(n?n(t):t)}function yt(t){var n=_t(Z++,10),e=ut();return n.__=t,z.componentDidCatch||(z.componentDidCatch=function(t,i){n.__&&n.__(t,i),e[1](t)}),[e[0],function(){e[1](void 0)}]}function mt(){var t=_t(Z++,11);if(!t.__){for(var n=z.__v;null!==n&&!n.__m&&null!==n.__;)n=n.__;var e=n.__m||(n.__m=[0,0]);t.__="P"+e[0]+"-"+e[1]++}return t.__}function gt(){for(var t;t=X.shift();)if(t.__P&&t.__H)try{t.__H.__h.forEach(wt),t.__H.__h.forEach(xt),t.__H.__h=[]}catch(i){t.__H.__h=[],Y.__e(i,t.__v)}}Y.__b=function(t){z=null,tt&&tt(t)},Y.__=function(t,n){t&&n.__k&&n.__k.__m&&(t.__m=n.__k.__m),rt&&rt(t,n)},Y.__r=function(t){nt&&nt(t),Z=0;var n=(z=t.__c).__H;n&&(G===z?(n.__h=[],z.__h=[],n.__.forEach((function(t){t.__N&&(t.__=t.__N),t.u=t.__N=void 0}))):(n.__h.forEach(wt),n.__h.forEach(xt),n.__h=[],Z=0)),G=z},Y.diffed=function(t){et&&et(t);var n=t.__c;n&&n.__H&&(n.__H.__h.length&&(1!==X.push(n)&&J===Y.requestAnimationFrame||((J=Y.requestAnimationFrame)||kt)(gt)),n.__H.__.forEach((function(t){t.u&&(t.__H=t.u),t.u=void 0}))),G=z=null},Y.__c=function(t,n){n.some((function(t){try{t.__h.forEach(wt),t.__h=t.__h.filter((function(t){return!t.__||xt(t)}))}catch(r){n.some((function(t){t.__h&&(t.__h=[])})),n=[],Y.__e(r,t.__v)}})),it&&it(t,n)},Y.unmount=function(t){ot&&ot(t);var n,e=t.__c;e&&e.__H&&(e.__H.__.forEach((function(t){try{wt(t)}catch(t){n=t}})),e.__H=void 0,n&&Y.__e(n,e.__v))};var bt="function"==typeof requestAnimationFrame;function kt(t){var n,e=function(){clearTimeout(i),bt&&cancelAnimationFrame(n),setTimeout(t)},i=setTimeout(e,35);bt&&(n=requestAnimationFrame(e))}function wt(t){var n=z,e=t.__c;"function"==typeof e&&(t.__c=void 0,e()),z=n}function xt(t){var n=z;t.__c=t.__(),z=n}function St(t,n){return!t||t.length!==n.length||n.some((function(n,e){return n!==t[e]}))}function Ct(t,n){return"function"==typeof n?n(t):n}var Ut=function(t,n,e,i){var o;n[0]=0;for(var r=1;r=5&&((o||!t&&5===i)&&(_.push(i,0,o,e),i=6),t&&(_.push(i,t,0,e),i=6)),o=""},l=0;l"===n?(i=1,o=""):o=n+o[0]:r?n===r?r="":o+=n:'"'===n||"'"===n?r=n:">"===n?(u(),i=1):i&&("="===n?(i=5,e=o,o=""):"/"===n&&(i<5||">"===t[l][f+1])?(u(),3===i&&(_=_[0]),i=_,(_=_[0]).push(2,0,i),i=0):" "===n||"\t"===n||"\n"===n||"\r"===n?(u(),i=2):o+=n),3===i&&"!--"===o&&(i=4,_=_[0])}return u(),_}(t)),n),arguments,[])).length>1?n:n[0]}var At=Pt.bind(g);var Nt={};function Ht(t,n){for(var e in n)t[e]=n[e];return t}function $t(t,n,e){var i,o=/(?:\?([^#]*))?(#.*)?$/,r=t.match(o),_={};if(r&&r[1])for(var u=r[1].split("&"),l=0;ln.rank?-1:t.index-n.index}function Dt(t,n){return t.index=n,t.rank=function(t){return t.props.default?0:Mt(t.props.path).map(Ft).join("")}(t),t.props}function Mt(t){return t.replace(/(^\/+|\/+$)/g,"").split("/")}function Ft(t){return":"==t.charAt(0)?1+"*+?".indexOf(t.charAt(t.length-1))||4:5}var Rt={},Wt=[],Ot=[],Lt=null,It={url:Vt()},qt=K(It);function jt(){var t=dt(qt);if(t===It){var n=ut()[1];ft((function(){return Ot.push(n),function(){return Ot.splice(Ot.indexOf(n),1)}}),[])}return[t,Bt]}function Vt(){var t;return""+((t=Lt&&Lt.location?Lt.location:Lt&&Lt.getCurrentLocation?Lt.getCurrentLocation():"undefined"!=typeof location?location:Rt).pathname||"")+(t.search||"")}function Bt(t,n){return void 0===n&&(n=!1),"string"!=typeof t&&t.url&&(n=t.replace,t=t.url),function(t){for(var n=Wt.length;n--;)if(Wt[n].canRoute(t))return!0;return!1}(t)&&function(t,n){void 0===n&&(n="push"),Lt&&Lt[n]?Lt[n](t):"undefined"!=typeof history&&history[n+"State"]&&history[n+"State"](null,null,t)}(t,n?"replace":"push"),Kt(t)}function Kt(t){for(var n=!1,e=0;e1){un--;return}let t,n=!1;while(void 0!==rn){let e=rn;rn=void 0;ln++;while(void 0!==e){const i=e.o;e.o=void 0;e.f&=-3;if(!(8&e.f)&&hn(e))try{e.c()}catch(_){if(!n){t=_;n=!0}}e=i}}ln=0;un--;if(n)throw t}function en(t){if(un>0)return t();un++;try{return t()}finally{nn()}}let on,rn;function _n(t){const n=on;on=void 0;try{return t()}finally{on=n}}let un=0,ln=0,fn=0;function sn(t){if(void 0===on)return;let n=t.n;if(void 0===n||n.t!==on){n={i:0,S:t,p:on.s,n:void 0,t:on,e:void 0,x:void 0,r:n};if(void 0!==on.s)on.s.n=n;on.s=n;t.n=n;if(32&on.f)t.S(n);return n}else if(-1===n.i){n.i=0;if(void 0!==n.n){n.n.p=n.p;if(void 0!==n.p)n.p.n=n.n;n.p=on.s;n.n=void 0;on.s.n=n;on.s=n}return n}}function cn(t,n){this.v=t;this.i=0;this.n=void 0;this.t=void 0;this.W=null==n?void 0:n.watched;this.Z=null==n?void 0:n.unwatched}cn.prototype.brand=tn;cn.prototype.h=function(){return!0};cn.prototype.S=function(t){const n=this.t;if(n!==t&&void 0===t.e){t.x=n;this.t=t;if(void 0!==n)n.e=t;else _n(()=>{var t;null==(t=this.W)||t.call(this)})}};cn.prototype.U=function(t){if(void 0!==this.t){const n=t.e,e=t.x;if(void 0!==n){n.x=e;t.e=void 0}if(void 0!==e){e.e=n;t.x=void 0}if(t===this.t){this.t=e;if(void 0===e)_n(()=>{var t;null==(t=this.Z)||t.call(this)})}}};cn.prototype.subscribe=function(t){return wn(()=>{const n=this.value,e=on;on=void 0;try{t(n)}finally{on=e}})};cn.prototype.valueOf=function(){return this.value};cn.prototype.toString=function(){return this.value+""};cn.prototype.toJSON=function(){return this.value};cn.prototype.peek=function(){const t=on;on=void 0;try{return this.value}finally{on=t}};Object.defineProperty(cn.prototype,"value",{get(){const t=sn(this);if(void 0!==t)t.i=this.i;return this.v},set(t){if(t!==this.v){if(ln>100)throw new Error("Cycle detected");this.v=t;this.i++;fn++;un++;try{for(let t=this.t;void 0!==t;t=t.x)t.t.N()}finally{nn()}}}});function an(t,n){return new cn(t,n)}function hn(t){for(let n=t.s;void 0!==n;n=n.n)if(n.S.i!==n.i||!n.S.h()||n.S.i!==n.i)return!0;return!1}function pn(t){for(let n=t.s;void 0!==n;n=n.n){const e=n.S.n;if(void 0!==e)n.r=e;n.S.n=n;n.i=-1;if(void 0===n.n){t.s=n;break}}}function dn(t){let n,e=t.s;while(void 0!==e){const t=e.p;if(-1===e.i){e.S.U(e);if(void 0!==t)t.n=e.n;if(void 0!==e.n)e.n.p=t}else n=e;e.S.n=e.r;if(void 0!==e.r)e.r=void 0;e=t}t.s=n}function vn(t,n){cn.call(this,void 0);this.x=t;this.s=void 0;this.g=fn-1;this.f=4;this.W=null==n?void 0:n.watched;this.Z=null==n?void 0:n.unwatched}vn.prototype=new cn;vn.prototype.h=function(){this.f&=-3;if(1&this.f)return!1;if(32==(36&this.f))return!0;this.f&=-5;if(this.g===fn)return!0;this.g=fn;this.f|=1;if(this.i>0&&!hn(this)){this.f&=-2;return!0}const t=on;try{pn(this);on=this;const t=this.x();if(16&this.f||this.v!==t||0===this.i){this.v=t;this.f&=-17;this.i++}}catch(t){this.v=t;this.f|=16;this.i++}on=t;dn(this);this.f&=-2;return!0};vn.prototype.S=function(t){if(void 0===this.t){this.f|=36;for(let t=this.s;void 0!==t;t=t.n)t.S.S(t)}cn.prototype.S.call(this,t)};vn.prototype.U=function(t){if(void 0!==this.t){cn.prototype.U.call(this,t);if(void 0===this.t){this.f&=-33;for(let t=this.s;void 0!==t;t=t.n)t.S.U(t)}}};vn.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(let t=this.t;void 0!==t;t=t.x)t.t.N()}};Object.defineProperty(vn.prototype,"value",{get(){if(1&this.f)throw new Error("Cycle detected");const t=sn(this);this.h();if(void 0!==t)t.i=this.i;if(16&this.f)throw this.v;return this.v}});function yn(t,n){return new vn(t,n)}function mn(t){const n=t.u;t.u=void 0;if("function"==typeof n){un++;const e=on;on=void 0;try{n()}catch(i){t.f&=-2;t.f|=8;gn(t);throw i}finally{on=e;nn()}}}function gn(t){for(let n=t.s;void 0!==n;n=n.n)n.S.U(n);t.x=void 0;t.s=void 0;mn(t)}function bn(t){if(on!==this)throw new Error("Out-of-order effect");dn(this);on=t;this.f&=-2;if(8&this.f)gn(this);nn()}function kn(t){this.x=t;this.u=void 0;this.s=void 0;this.o=void 0;this.f=32}kn.prototype.c=function(){const t=this.S();try{if(8&this.f)return;if(void 0===this.x)return;const n=this.x();if("function"==typeof n)this.u=n}finally{t()}};kn.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1;this.f&=-9;mn(this);pn(this);un++;const t=on;on=this;return bn.bind(this,t)};kn.prototype.N=function(){if(!(2&this.f)){this.f|=2;this.o=rn;rn=this}};kn.prototype.d=function(){this.f|=8;if(!(1&this.f))gn(this)};kn.prototype.dispose=function(){this.d()};function wn(t){const n=new kn(t);try{n.c()}catch(t){n.d();throw t}return n.d.bind(n)}let xn,Sn,Cn,Un=[],En=[];wn((function(){xn=this.N}))();function Pn(t,e){n[t]=e.bind(null,n[t]||(()=>{}))}function An(t){if(Cn)Cn();Cn=t&&t.S()}function Nn({data:t}){const n=$n(t);n.value=t;const[e,o]=ht(()=>{let t=this,e=this.__v;while(e=e.__)if(e.__c){e.__c.__$f|=4;break}const o=yn(()=>{let t=n.value.value;return 0===t?0:!0===t?"":t||""}),r=yn(()=>!Array.isArray(o.value)&&!i(o.value)),_=wn((function(){this.N=On;if(r.value){const n=o.value;if(t.__v&&t.__v.__e&&3===t.__v.__e.nodeType)t.__v.__e.data=n}})),u=this.__$u.d;this.__$u.d=function(){_();u.call(this)};return[r,o]},[]);return e.value?o.peek():o.value}Nn.displayName="_st";Object.defineProperties(cn.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Nn},props:{configurable:!0,get(){return{data:this}}},__b:{configurable:!0,value:1}});Pn("__b",(t,n)=>{if("string"==typeof n.type){let t,e=n.props;for(let i in e){if("children"===i)continue;let o=e[i];if(o instanceof cn){if(!t)n.__np=t={};t[i]=o;e[i]=o.peek()}}}t(n)});Pn("__r",(t,n)=>{if(n.type!==w){An();let t,e=n.__c;if(e){e.__$f&=-2;t=e.__$u;if(void 0===t)e.__$u=t=function(t){let n;wn((function(){n=this}));n.c=()=>{e.__$f|=1;e.setState({})};return n}()}Sn=e;An(t)}t(n)});Pn("__e",(t,n,e,i)=>{An();Sn=void 0;t(n,e,i)});Pn("diffed",(t,n)=>{An();Sn=void 0;let e;if("string"==typeof n.type&&(e=n.__e)){let t=n.__np,i=n.props;if(t){let n=e.U;if(n)for(let e in n){let i=n[e];if(void 0!==i&&!(e in t)){i.d();n[e]=void 0}}else{n={};e.U=n}for(let o in t){let r=n[o],_=t[o];if(void 0===r){r=Hn(e,o,_,i);n[o]=r}else r.o(_,i)}}}t(n)});function Hn(t,n,e,i){const o=n in t&&void 0===t.ownerSVGElement,r=an(e);return{o:(t,n)=>{r.value=t;i=n},d:wn((function(){this.N=On;const e=r.value.value;if(i[n]!==e){i[n]=e;if(o)t[n]=e;else if(e)t.setAttribute(n,e);else t.removeAttribute(n)}}))}}Pn("unmount",(t,n)=>{if("string"==typeof n.type){let t=n.__e;if(t){const n=t.U;if(n){t.U=void 0;for(let t in n){let e=n[t];if(e)e.d()}}}}else{let t=n.__c;if(t){const n=t.__$u;if(n){t.__$u=void 0;n.d()}}}t(n)});Pn("__h",(t,n,e,i)=>{if(i<3||9===i)n.__$f|=2;t(n,e,i)});x.prototype.shouldComponentUpdate=function(t,n){const e=this.__$u,i=e&&void 0!==e.s;for(let o in n)return!0;if(this.__f||"boolean"==typeof this.u&&!0===this.u){const t=2&this.__$f;if(!(i||t||4&this.__$f))return!0;if(1&this.__$f)return!0}else{if(!(i||4&this.__$f))return!0;if(3&this.__$f)return!0}for(let o in t)if("__source"!==o&&t[o]!==this.props[o])return!0;for(let o in this.props)if(!(o in t))return!0;return!1};function $n(t,n){return ht(()=>an(t,n),[])}function Tn(t,n){const e=ct(t);e.current=t;Sn.__$f|=4;return ht(()=>yn(()=>e.current(),n),[])}const Dn="undefined"==typeof requestAnimationFrame?setTimeout:function(t){const n=()=>{clearTimeout(e);cancelAnimationFrame(i);t()},e=setTimeout(n,35),i=requestAnimationFrame(n)},Mn=t=>{queueMicrotask(()=>{queueMicrotask(t)})};function Fn(){en(()=>{let t;while(t=Un.shift())xn.call(t)})}function Rn(){if(1===Un.push(this))(n.requestAnimationFrame||Dn)(Fn)}function Wn(){en(()=>{let t;while(t=En.shift())xn.call(t)})}function On(){if(1===En.push(this))(n.requestAnimationFrame||Mn)(Wn)}function Ln(t){const n=ct(t);n.current=t;ft(()=>wn((function(){this.N=Rn;return n.current()})),[])}export{x as Component,w as Fragment,Xt as Link,Yt as Route,Qt as Router,cn as Signal,en as batch,B as cloneElement,yn as computed,K as createContext,g as createElement,k as createRef,wn as effect,$t as exec,Vt as getCurrentUrl,g as h,At as html,V as hydrate,i as isValidElement,n as options,j as render,Bt as route,an as signal,H as toChildArray,_n as untracked,pt as useCallback,Tn as useComputed,dt as useContext,vt as useDebugValue,ft as useEffect,yt as useErrorBoundary,mt as useId,at as useImperativeHandle,st as useLayoutEffect,ht as useMemo,lt as useReducer,ct as useRef,jt as useRouter,$n as useSignal,Ln as useSignalEffect,ut as useState}; \ No newline at end of file diff --git a/tutorials/mqtt/mqtt-dashboard/dashboard/index.html b/tutorials/mqtt/mqtt-dashboard/dashboard/index.html index 63a38dbd..c93bd251 100644 --- a/tutorials/mqtt/mqtt-dashboard/dashboard/index.html +++ b/tutorials/mqtt/mqtt-dashboard/dashboard/index.html @@ -6,8 +6,11 @@ + + + diff --git a/tutorials/mqtt/mqtt-dashboard/dashboard/main.css b/tutorials/mqtt/mqtt-dashboard/dashboard/main.css index f921cd87..70fff3c2 100644 --- a/tutorials/mqtt/mqtt-dashboard/dashboard/main.css +++ b/tutorials/mqtt/mqtt-dashboard/dashboard/main.css @@ -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: }.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.right-4{right:1rem}.top-0{top:0}.top-4{top:1rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-auto{margin-top:auto;margin-bottom:auto}.mb-4{margin-bottom:1rem}.mt-3{margin-top:.75rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-11{width:2.75rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-60{width:15rem}.w-64{width:16rem}.w-96{width:24rem}.w-full{width:100%}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.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}.transform{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))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-auto{overflow:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-slate-200{--tw-border-opacity:1;border-color:rgb(226 232 240/var(--tw-border-opacity))}.border-transparent{border-color:#0000}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity))}.bg-slate-300{--tw-bg-opacity:1;background-color:rgb(203 213 225/var(--tw-bg-opacity))}.bg-stone-100{--tw-bg-opacity:1;background-color:rgb(245 245 244/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.bg-zinc-100{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity))}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pr-3{padding-right:.75rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-green-900{--tw-text-opacity:1;color:rgb(20 83 45/var(--tw-text-opacity))}.text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-900{--tw-text-opacity:1;color:rgb(113 63 18/var(--tw-text-opacity))}.text-zinc-900{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity))}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-0,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-inset{--tw-ring-inset:inset}.ring-green-300{--tw-ring-opacity:1;--tw-ring-color:rgb(134 239 172/var(--tw-ring-opacity))}.ring-red-300{--tw-ring-opacity:1;--tw-ring-color:rgb(252 165 165/var(--tw-ring-opacity))}.ring-yellow-300{--tw-ring-opacity:1;--tw-ring-color:rgb(253 224 71/var(--tw-ring-opacity))}.ring-zinc-300{--tw-ring-opacity:1;--tw-ring-color:rgb(212 212 216/var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.placeholder\:text-gray-400::-moz-placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.placeholder\:text-gray-400::placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.hover\:bg-blue-500:hover{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.hover\:bg-stone-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 244/var(--tw-bg-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-0:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-blue-400:disabled{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity))}.disabled\:bg-gray-100:disabled{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.disabled\:text-gray-500:disabled{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,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;-webkit-tap-highlight-color:transparent}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-feature-settings:normal;font-variation-settings:normal;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-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-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}dialog{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: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.right-4{right:1rem}.top-0{top:0}.top-4{top:1rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-auto{margin-top:auto;margin-bottom:auto}.mb-4{margin-bottom:1rem}.mt-3{margin-top:.75rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-11{width:2.75rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-60{width:15rem}.w-64{width:16rem}.w-96{width:24rem}.w-full{width:100%}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.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}.transform{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))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.overflow-auto{overflow:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-slate-200{--tw-border-opacity:1;border-color:rgb(226 232 240/var(--tw-border-opacity))}.border-transparent{border-color:#0000}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity))}.bg-slate-300{--tw-bg-opacity:1;background-color:rgb(203 213 225/var(--tw-bg-opacity))}.bg-stone-100{--tw-bg-opacity:1;background-color:rgb(245 245 244/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.bg-zinc-100{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity))}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pr-3{padding-right:.75rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-green-900{--tw-text-opacity:1;color:rgb(20 83 45/var(--tw-text-opacity))}.text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-900{--tw-text-opacity:1;color:rgb(113 63 18/var(--tw-text-opacity))}.text-zinc-900{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity))}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-0,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-inset{--tw-ring-inset:inset}.ring-green-300{--tw-ring-opacity:1;--tw-ring-color:rgb(134 239 172/var(--tw-ring-opacity))}.ring-red-300{--tw-ring-opacity:1;--tw-ring-color:rgb(252 165 165/var(--tw-ring-opacity))}.ring-yellow-300{--tw-ring-opacity:1;--tw-ring-color:rgb(253 224 71/var(--tw-ring-opacity))}.ring-zinc-300{--tw-ring-opacity:1;--tw-ring-color:rgb(212 212 216/var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.placeholder\:text-gray-400::-moz-placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.placeholder\:text-gray-400::placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.hover\:bg-blue-500:hover{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.hover\:bg-stone-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 244/var(--tw-bg-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-0:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-blue-400:disabled{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity))}.disabled\:bg-gray-100:disabled{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.disabled\:text-gray-500:disabled{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/tutorials/mqtt/mqtt-dashboard/dashboard/main.js b/tutorials/mqtt/mqtt-dashboard/dashboard/main.js index c9ddeb00..a32e1ae1 100644 --- a/tutorials/mqtt/mqtt-dashboard/dashboard/main.js +++ b/tutorials/mqtt/mqtt-dashboard/dashboard/main.js @@ -1,18 +1,16 @@ 'use strict'; -import {h, html, render, useEffect, useRef, useState} from './bundle.js'; +import {h, html, render, useEffect, useRef, useSignal} from './bundle.js'; -const Logo = props => html``; const DefaultTopic = 'mg_mqtt_dashboard'; const DefaultUrl = location.protocol == 'https:' ? 'wss://broker.hivemq.com:8884/mqtt' : 'ws://broker.hivemq.com:8000/mqtt'; -const DefaultDeviceConfig = {pin_map: [], pin_state: [], log_level: 0, pin_count: 0}; -const Delay = (ms, val) => new Promise(resolve => setTimeout(resolve, ms, val)); +// const Delay = (ms, val) => new Promise(resolve => setTimeout(resolve, ms, val)); const handleFetchError = r => r.ok || alert(`Error: ${r.statusText}`); -const LabelClass = 'text-sm truncate text-gray-500 font-medium my-auto whitespace-nowrap'; +const LabelClass = 'text-sm truncate font-medium my-auto whitespace-nowrap'; const BadgeClass = 'flex-inline text-sm rounded-md rounded px-2 py-0.5 ring-1 ring-inset'; const InputClass = '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 rounded border'; -const TitleClass = 'font-semibold xuppercase xtext-gray-600'; +const TitleClass = 'font-semibold'; const Colors = { green: 'bg-green-100 text-green-900 ring-green-300', yellow: 'bg-yellow-100 text-yellow-900 ring-yellow-300', @@ -22,109 +20,46 @@ const Colors = { let MqttClient; -const Icons = { - logo: props => html` `, - activity: props => html``, - refresh: props => html``, - info: props => html``, - login: props => html``, - logout: props => html``, - menu: props => html``, - upload: props => html``, - monitor: props => html``, - settings: props => html``, - download: props => html``, - file: props => html``, - check: props => html``, - rollback: props => html``, - save: props => html``, - bolt: props => html``, - delete: props => html``, -}; +const Help = () => html` +
+
This is a simple demonstration of the functional device dashboard + that manages a fleet of devices via an MQTT server. Source code + is available on GitHub. +
+ For the sake of simplicity, this dashboard does not implement authentication. + No external storage is used either to keep device list: for that, retained + MQTT messages are utilised. When a device goes online, it publishes its + state to the {root_topic}/{device_id}/status topic + - LED status and firmware version. + +
+ The last will message triggers + the "offline" message to the same topic. This is how this web page + is able to track online/offline status of devices. + +
+ See developer console for the list of MQTT messages exchanged with + the MQTT server. + +`; -export function Button({title, onclick, disabled, extraClass, icon, ref, colors, hovercolor, disabledcolor}) { - const [spin, setSpin] = useState(false); +export function Button({title, onclick, disabled, extraClass, ref, colors}) { + const sigSpin = useSignal(false); const cb = function(ev) { const res = onclick ? onclick() : null; if (res && typeof (res.catch) === 'function') { - setSpin(true); - res.catch(() => false).then(() => setSpin(false)); + sigSpin.value = true; + res.catch(() => false).then(() => sigSpin.value = false); } }; if (!colors) colors = 'bg-blue-600 hover:bg-blue-500 disabled:bg-blue-400'; return html` -`; }; -function Header({topic, setTopic, url, setUrl, connected}) { +function Header({sigTopic, sigUrl, connected}) { const forbiddenChars = ['$', '*', '+', '#', '/']; const onClick = () => { const isValidTopic = val => !forbiddenChars.some(char => val.includes(char)); if (isValidTopic(topic)) { localStorage.setItem('topic', topic) - setTopic(topic); + sigTopic.value = topic; window.location.reload(); } else { setSaveResult('Error: The topic cannot contain these characters: ' + forbiddenChars); @@ -150,7 +85,7 @@ function Header({topic, setTopic, url, setUrl, connected}) { }; return html` -
+

IoT Fleet Management Dashboard

@@ -158,15 +93,15 @@ function Header({topic, setTopic, url, setUrl, connected}) {
MQTT Server - setUrl(ev.target.value)} class=${InputClass} /> + sigUrl.value = ev.target.value} class=${InputClass} />
Root Topic - setTopic(ev.target.value)} class=${InputClass} /> + sigTopic.value = ev.target.value} class=${InputClass} /> - <${Button} icon=${Icons.bolt} onclick=${onClick} title=${connected ? 'Disconnect' : 'Connect'} /> + <${Button} extraClass="w-32" onclick=${onClick} title=${connected ? 'Disconnect' : 'Connect'} /> `; @@ -174,252 +109,173 @@ function Header({topic, setTopic, url, setUrl, connected}) { function Sidebar({devices, onclick}) { const Td = props => html` - ${ - props.text}`; + ${props.text}`; const Device = ({d}) => html`
onclick(d.id)}> ${d.id} - ${d.online ? 'online' : 'offline'} + ${d.online ? 'online' : 'offline'} `; return html`
-
+
Device List ${(devices ? devices : []).map(d => h(Device, {d}))} `; }; -function FirmwareStatus({title, info, children}) { - const state = - ['UNAVAILABLE', 'FIRST_BOOT', 'NOT_COMMITTED', - 'COMMITTED'][(info.status || 0) % 4]; - const valid = info.status > 0; +export function File({accept, fn, ...rest}) { + const btn = useRef(null); + const input = useRef(null); + + const oncancel = function(ev) { input.resolve(); input.resolve = null; }; + const onchange = function(ev) { + if (!ev.target.files[0]) { input.resolve(); input.resolve = null; return; } + const f = ev.target.files[0]; + const reader = new FileReader(); + reader.readAsArrayBuffer(f); + reader.onload = function() { + fn && fn(reader.result, f.type).then(() => { + input.resolve(); + }).finally(() => { + input.resolve = null; + }); + }; + btn.current && btn.current.base.click(); + ev.target.value = ''; + ev.preventDefault(); + }; + const onclick = function() { + if (!input.fn) input.current.click(); + return new Promise(resolve => { input.resolve = resolve; }); + }; + return html` -
-
- ${title} - -
-
Status: ${state} -
CRC32: ${valid ? info.crc32.toString(16) : 'n/a'} -
Size: ${valid ? info.size : 'n/a'} -
Flashed at: ${ - valid ? new Date(info.timestamp * 1000).toLocaleString() : 'n/a'} - ${children} - - `; + + + <${Button} onclick=${onclick} ref=${btn} ...${rest} /> +`; }; -function FirmwareUpdatePanel({no, name, current, previous, updated_at, refresh}) { - const [color, setColor] = useState(Colors.green); - const [otaStatus, setOtaStatus] = useState('up-to-date'); - const Descr = ({label, value}) => html` -
- ${label} - ${value} - `; - const onprogress = (len, total) => { - setColor(Colors.info); - setOtaStatus(`uploading, ${parseInt(len * 100 /total)}% done...`); - } - const setparam = (key, val) => fetch('api/settings/set', { - method: 'POST', - body: JSON.stringify({key, val}), - }).then(handleFetchError); - const onupload = function(ok, fileName, fileLength) { - if (ok) { - return setparam(`ecu.${no}.new`, fileName).then(refresh); - } else { - setColor(Colors.red), setOtaStatus('upload failed'); - } - }; - const onfileselect = function(fileName, fileLength) { - const ok = fileName.startsWith(name); - if (!ok) alert(`Firmware file name must be ${name}.VERSION.BIN_OR_HEX`); - return ok; - } - const update = rollback => new Promise(function(resolve, reject) { - const opts = {method: 'POST', body: JSON.stringify({no, rollback})}; - const fail = () => (reject(), setOtaStatus('update error'), setColor(Colors.red)); - const finish = () => fetch('api/ecu/update/end', opts) - .then(r => r.ok ? resolve() : reject()) - .then(refresh); - const write = () => fetch('api/ecu/update/write', opts).then(r => { - if (!r.ok) fail(); - //if (r.ok) r.json().then(r => console.log('Goo', r)); - if (r.ok) r.json().then(r => r == 1 ? write() : finish()); - }); - fetch('api/ecu/update/begin', opts).then(r => r.ok ? write() : fail()); +function arrayBufferToBase64Async(buffer) { + return new Promise((resolve, reject) => { + const blob = new Blob([buffer]); + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = reader.result; // reader.result is like "data:application/octet-stream;base64,AAAA..." + resolve(dataUrl.split(",", 2)[1]); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); }); - const onflash = ev => update(false); - const onrollback = ev => update(true); +} - useEffect(function() { - //setOtaStatus(anew ? 'update pending' : 'up-to-date'); - //setColor(anew ? Colors.yellow : Colors.green); - }, []); +function FirmwareUpdatePanel({device, rpc, connected}) { + + const fn = function (fileData) { + // Split file in chunks + let chunkSize = 4096, offset = 0; + const chunks = Array.from({ length: Math.ceil(fileData.byteLength / chunkSize) }, (_, i) => + fileData.slice(i * chunkSize, i * chunkSize + chunkSize) + ); + function next(resolve, reject) { + const chunk = chunks.shift(); + if (chunks.length == 0) resolve(); + return arrayBufferToBase64Async(chunk) + .then(encoded => rpc('ota.upload', { + offset: offset, + total: fileData.byteLength, + chunk: encoded, + })) + .then(response => { + if (response.result == 'ok') { + next(resolve, reject); + offset += chunkSize; + } else { + reject(); + } + }); + }; + return new Promise((resolve, reject) => next(resolve, reject)); + }; return html` -
+
Firmware Update -
Status:${otaStatus}
- <${Descr} label="Current firmware:" value=${current || 'n/a'} /> - <${Descr} label="Updated at:" value=${updated_at || 'n/a'} /> -
- Previous firmware: - - ${previous} - <${Button} title="rollback" icon=${Icons.bolt} onclick=${onrollback} - extraClass="w-24" disabled=${!previous} /> - +
+
Current firmware version +
${device.firmware_version || '??'} -
- New firmware: - - <${UploadFileButton} title="upload" url="api/fw/upload" - onfileselect=${onfileselect} - onprogress=${onprogress} oncomplete=${onupload} /> - <${Button} title="flash" icon=${Icons.bolt} onclick=${onflash} - extraClass="w-24" disabled=${!previous} /> - + +
+
Upload new firmware + <${File} title="..." disabled=${!device.online || !connected} fn=${fn} /> `; }; -function DeviceSettingsPanel({device, publishFn, connected}) { - const [config, setConfig] = useState({}); - const logOptions = [[0, 'Disable'], [1, 'Error'], [2, 'Info'], [3, 'Debug']]; +function LedControlPanel({device, rpc, connected}) { + const ontoggle = () => rpc('state.set', {led_status: !device.led_status}); - const onSave = ev => publishFn('config.set', config).then(r => { - //setDeviceConfig(device.id, config); - }).catch(e => alert('Failure!')); + return html` +
+
+
LED Control Panel + +
+
Toggle LED + <${Toggle} onclick=${ontoggle} disabled=${!device.online || !connected} value=${device.led_status} /> + + `; +}; +function DeviceDashboard({device, rpc, connected}) { // To delete device, set an empty retained message const onforget = function(ev) { MqttClient.publish(device.topic, '', {retain: true}); location.reload(); }; - const onloglevelchange = ev => setConfig(x => Object.assign({}, x, {log_level: parseInt(ev.target.value)})); - const onpinschange = ev => { - const pin_map = ev.target.value.split(/\s*,\s*/).map(x => parseInt(x)); - setConfig(x => Object.assign({}, x, { - pin_map, // Send an updated pin map - pin_count: pin_map.length, // And pin count - pin_state:[] // Remove pin_state from the request, as pin_map change can invalidate it - })); - } - //console.log('sp', device, config); - useEffect(() => setConfig(device.config) , [device]); - - return html` -
-
-
Settings - -
-
- Status - ${connected ? 'online' : 'offline'} - -
- Device ID - ${device.id} - -
- Log Level - - - - -
- <${Button} title="Forget this device" - disabled=${device.online || !connected} icon=${Icons.delete} - onclick=${onforget}/> - <${Button} icon=${Icons.save} onclick=${onSave} title="Save Settings" disabled=${!device.online} /> - - - `; -}; - -function DeviceControlPanel({device, config, setConfig, publishFn, connected}) { - const onclick = function(i) { // Send request to toggle pin i - let configCopy = Object.assign({}, config); - configCopy.pin_state[i] = !!configCopy.pin_state[i] ? 0 : 1; - return publishFn('config.set', configCopy).catch(e => alert('Failure!')); - }; - - const Pin = i => html` -
-
- Pin ${config.pin_map[i]} - <${Toggle} onclick=${ev => onclick(i)} - disabled=${!device.online || !connected} - value=${config.pin_state[i]} /> - - `; - - return html` -
-
-
Pin Control Panel - -
- ${(config.pin_map || []).map((_, i) => Pin(i))} -
- - `; -}; - -function DeviceDashboard({device, setDeviceConfig, publishFn, connected}) { - const cfg = device && device.config ? device.config : DefaultDeviceConfig; - const [localConfig, setLocalConfig] = useState(cfg); - useEffect(() => setLocalConfig(cfg), [device]); - - if (!device || !localConfig) { + if (!device) { return html` -
+
No device selected. Click on a device on a sidebar `; } return html` -
- <${DeviceSettingsPanel} device=${device} config=${localConfig} setConfig=${setLocalConfig} publishFn=${publishFn} connected=${connected} /> - <${DeviceControlPanel} device=${device} config=${localConfig} setConfig=${setLocalConfig} publishFn=${publishFn} connected=${connected} /> - <${FirmwareUpdatePanel} deviceID=${device.id} publishFn=${publishFn} - disabled=${!device.online} - info=${[localConfig.crnt_fw, localConfig.prev_fw]} /> -
`; +
+
+ Device ${device.id} + <${Button} title="Forget this device" onclick=${onforget}/> + +
+ <${LedControlPanel} device=${device} rpc=${rpc} connected=${connected} /> + <${FirmwareUpdatePanel} device=${device} rpc=${rpc} connected=${connected} /> + + `; } const App = function() { - const [devices, setDevices] = useState([]); - const [currentDevID, setCurrentDevID] = useState(localStorage.getItem('currentDevID') || ''); - const [loading, setLoading] = useState(true); - const [url, setUrl] = useState(localStorage.getItem('url') || DefaultUrl); - const [topic, setTopic] = useState(localStorage.getItem('topic') || DefaultTopic); - const [error, setError] = useState(null); - const [connected, setConnected] = useState(false); + const sigDevices = useSignal([]); + const sigCurrentDevID = useSignal(localStorage.getItem('currentDevID') || ''); + const sigUrl = useSignal(localStorage.getItem('url') || DefaultUrl); + const sigTopic = useSignal(localStorage.getItem('topic') || DefaultTopic); + const sigError = useSignal(null); + const sigLoading = useSignal(true); + const sigConnected = useSignal(false); const responseHandlers = useRef({}); + const getDeviceByID = (deviceID) => sigDevices.value.find(d => d.id === deviceID); + + function addResponseHandler(correlationId, handler) { responseHandlers[correlationId] = handler; } @@ -428,19 +284,19 @@ const App = function() { delete responseHandlers[correlationId]; } - const onRefresh = ev => window.location.reload(); + const onRefresh = () => window.location.reload(); const initConn = () => { - MqttClient = mqtt.connect(url, {connectTimeout: 5000, reconnectPeriod: 0}); + MqttClient = mqtt.connect(sigUrl.value, {connectTimeout: 5000, reconnectPeriod: 0}); MqttClient.on('connect', () => { //console.log('Connected to the broker'); - setLoading(false); - setError(null); // Reset error state upon successful connection - setConnected(true); + sigLoading.value = false; + sigError.value = null; // Reset error state upon successful connection + sigConnected.value = true; - const statusTopic = topic + '/+/status' - const txTopic = topic + '/+/tx' + const statusTopic = sigTopic.value + '/+/status' + const txTopic = sigTopic.value + '/+/tx' const subscribe = (topic) => { MqttClient.subscribe(topic, (err) => { @@ -458,7 +314,7 @@ const App = function() { }); 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; try { @@ -469,19 +325,18 @@ const App = function() { } if (topic.endsWith('/status')) { - let device = {topic: topic, id: topic.split('/')[1], config: DefaultDeviceConfig}; - const params = response.params; - if (!params) { + if (!response.params) { console.error('Invalid response'); return; } - device.online = params.status === 'online'; - //setConnected(device.online); - if (device.online) { - device.config = params; - if (!device.config.pins) device.config.pins = []; - } - setDevice(device) + let device = Object.assign(response.params, {topic: topic, id: topic.split('/')[1]}); + device.online = response.params.status === 'online'; + const devices = sigDevices.value.filter(d => d.id !== device.id); + devices.push(device); + devices.sort((a, b) => a.online && !b.online ? -1 + :!a.online && b.online ? 1 + : a.id < b.id ? -1 : 1); + sigDevices.value = devices; } else if (topic.endsWith('/tx')) { if (!response.id) { console.error('Invalid response'); @@ -497,14 +352,14 @@ const App = function() { MqttClient.on('error', (err) => { console.error('Connection error:', err); - setError('Connection cannot be established.'); + sigError.value = 'Connection cannot be established.'; }); MqttClient.on('close', () => { - if (!connected) { + if (!sigConnected.value) { console.error('Failed to connect to the broker.'); - setError('Connection cannot be established.'); - setLoading(false); + sigError.value = 'Connection cannot be established.'; + sigLoading.value = false; } }); }; @@ -527,60 +382,30 @@ const App = function() { resolve(messageData); }); - if (currentDevID) { - const rxTopic = topic + `/${currentDevID}/rx`; + if (sigCurrentDevID.value) { + const rxTopic = sigTopic.value + `/${sigCurrentDevID.value}/rx`; const rpcPayload = {method: methodName, id: randomID}; if (parameters) { rpcPayload.params = parameters; } + console.log(`Sending message to ${rxTopic}: ${JSON.stringify(rpcPayload)}`); MqttClient.publish(rxTopic, JSON.stringify(rpcPayload)); } }); }; - const getDeviceByID = (deviceID) => 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); + sigCurrentDevID.value = device.id; localStorage.setItem('currentDevID', device.id); } }; - if (error) { + if (sigError.value) { return html`
- <${Header}/> + <${Header} sigTopic=${sigTopic} sigUrl=${sigUrl} />

Connection Error

@@ -588,8 +413,7 @@ const App = function() { Unable to connect to the MQTT broker.

- <${Button} title="Retry" onclick=${onRefresh} - icon=${Icons.refresh} class="absolute top-4 right-4" /> + <${Button} title="Retry" onclick=${onRefresh} class="absolute top-4 right-4" />
@@ -598,19 +422,17 @@ const App = function() { return html`
- <${Header} - topic=${topic} setTopic=${setTopic} url=${url} - setUrl=${setUrl} connected=${connected} - /> + <${Header} sigTopic=${sigTopic} sigUrl=${sigUrl} connected=${sigConnected.value} />
-
- <${Sidebar} devices=${devices} onclick=${onDeviceClick} /> +
+ <${Sidebar} devices=${sigDevices.value} onclick=${onDeviceClick} />
<${DeviceDashboard} - device=${getDeviceByID(currentDevID)} connected=${connected} - setDeviceConfig=${setDeviceConfig} publishFn=${handlePublish} /> + device=${getDeviceByID(sigCurrentDevID.value)} connected=${sigConnected.value} + rpc=${handlePublish} /> + <${Help} /> `; }; diff --git a/tutorials/mqtt/mqtt-dashboard/device/main.c b/tutorials/mqtt/mqtt-dashboard/device/main.c index 0d64b35c..1ce8142a 100644 --- a/tutorials/mqtt/mqtt-dashboard/device/main.c +++ b/tutorials/mqtt/mqtt-dashboard/device/main.c @@ -5,6 +5,7 @@ #include "hal.h" // Handle interrupts, like Ctrl-C +char *g_firmware_version = "1.0.0"; static int s_signo; static void signal_handler(int signo) { s_signo = signo; @@ -22,13 +23,15 @@ int main(int argc, char *argv[]) { g_device_id = strdup(argv[++i]); } else if (strcmp(argv[i], "-t") == 0 && argv[i + 1] != NULL) { g_root_topic = argv[++i]; + } else if (strcmp(argv[i], "-f") == 0 && argv[i + 1] != NULL) { + g_firmware_version = 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 mqtt://SERVER:PORT] [-i DEVICE_ID] [-t TOPIC_NAME] [-v " - "DEBUG_LEVEL]", + "DEBUG_LEVEL] [-f FIRMWARE_VERSION]", argv[0], argv[i])); return 1; } diff --git a/tutorials/mqtt/mqtt-dashboard/device/net.c b/tutorials/mqtt/mqtt-dashboard/device/net.c index 82cf85ee..37542816 100644 --- a/tutorials/mqtt/mqtt-dashboard/device/net.c +++ b/tutorials/mqtt/mqtt-dashboard/device/net.c @@ -2,6 +2,8 @@ // All rights reserved #include "net.h" +#include +#include "mongoose.h" char *g_mqtt_server_url = MQTT_SERVER_URL; char *g_root_topic = MQTT_ROOT_TOPIC; @@ -11,15 +13,12 @@ static uint8_t s_qos = 1; // MQTT QoS static struct mg_connection *s_conn; // MQTT Client connection static struct mg_rpc *s_rpc = NULL; // List of registered RPC methods -#define MAX_PINS 20 -struct device_config { - int log_level; // Device logging level, 0-4 - int pin_count; // Number of pins to handle - uint16_t pin_map[MAX_PINS]; // Pins to handle - bool pin_state[MAX_PINS]; // State of the GPIO pins +struct device_state { + bool led_status; + char firmware_version[20]; }; -static struct device_config s_device_config; +static struct device_state s_device_state; // Device ID generation function. Create an ID that is unique // for a given device, and does not change between device restarts. @@ -53,40 +52,19 @@ static void set_device_id(void) { g_device_id = strdup(buf); } -static size_t print_shorts(void (*out)(char, void *), void *ptr, va_list *ap) { - uint16_t *array = va_arg(*ap, uint16_t *); - int i, len = 0, num_elems = va_arg(*ap, int); - for (i = 0; i < num_elems; i++) { - len += mg_xprintf(out, ptr, "%s%hu", i ? "," : "", array[i]); - } - return len; -} - -static size_t print_bools(void (*out)(char, void *), void *ptr, va_list *ap) { - bool *array = va_arg(*ap, bool *); - int i, len = 0, num_elems = va_arg(*ap, int); - 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) { char topic[100]; 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:%d,%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(("pin_count")), s_device_config.pin_count, // - MG_ESC(("pin_map")), print_shorts, s_device_config.pin_map, - s_device_config.pin_count, // - MG_ESC(("pin_state")), print_bools, s_device_config.pin_state, - s_device_config.pin_count); + mg_xprintf( + mg_pfn_iobuf, &io, + "{%m:%m,%m:{%m:%m,%m:%s,%m:%m}}", // + MG_ESC("method"), MG_ESC("status.notify"), MG_ESC("params"), // + MG_ESC("status"), MG_ESC("online"), // + MG_ESC("led_status"), s_device_state.led_status ? "true" : "false", // + MG_ESC("firmware_version"), MG_ESC(s_device_state.firmware_version)); memset(&pub_opts, 0, sizeof(pub_opts)); mg_snprintf(topic, sizeof(topic), "%s/%s/status", g_root_topic, g_device_id); @@ -121,32 +99,9 @@ static void subscribe(struct mg_connection *c) { mg_free(rx_topic); } -static void rpc_config_set(struct mg_rpc_req *r) { - struct device_config dc = s_device_config; - dc.pin_count = (int) mg_json_get_long(r->frame, "$.params.pin_count", -1); - dc.log_level = (int) mg_json_get_long(r->frame, "$.params.log_level", -1); - - if (dc.log_level < 0 || dc.log_level > MG_LL_VERBOSE) { - mg_rpc_err(r, -32602, "Log level must be from 0 to 4"); - } else if (dc.pin_count <= 0 || dc.pin_count > MAX_PINS) { - mg_rpc_err(r, -32602, "Pin count must be from 1 to %d", MAX_PINS); - } else { - int i, val; - for (i = 0; i < dc.pin_count; i++) { - char path[50]; - mg_snprintf(path, sizeof(path), "$.params.pin_map[%d]", i); - dc.pin_map[i] = (uint16_t) mg_json_get_long(r->frame, path, 0); - mg_snprintf(path, sizeof(path), "$.params.pin_state[%d]", i); - if ((val = (int) mg_json_get_long(r->frame, path, -1)) >= 0) { - gpio_write(dc.pin_map[i], val); - } - dc.pin_state[i] = gpio_read(dc.pin_map[i]); - // MG_INFO(("%d %d %d", i, dc.pin_map[i], dc.pin_state[i])); - } - mg_log_set(dc.log_level); - s_device_config = dc; - mg_rpc_ok(r, "true"); - } +static void rpc_state_set(struct mg_rpc_req *r) { + mg_json_get_bool(r->frame, "$.params.led_status", &s_device_state.led_status); + mg_rpc_ok(r, "true"); } static void rpc_ota_upload(struct mg_rpc_req *r) { @@ -244,29 +199,24 @@ static void timer_sntp(void *param) { // SNTP timer function. Sync up time } } - void web_init(struct mg_mgr *mgr) { - int i, ping_interval_ms = MQTT_KEEPALIVE_SEC * 1000 - 500; + int ping_interval_ms = MQTT_KEEPALIVE_SEC * 1000 - 500; + int flags = MG_TIMER_REPEAT; set_device_id(); - s_device_config.log_level = (int) mg_log_level; - s_device_config.pin_count = 5; - s_device_config.pin_map[0] = 10; - s_device_config.pin_map[1] = 11; - s_device_config.pin_map[2] = 12; - s_device_config.pin_map[3] = 13; - s_device_config.pin_map[4] = 25; - for (i = 0; i < s_device_config.pin_count; i++) { - s_device_config.pin_state[i] = gpio_read(s_device_config.pin_map[i]); - } + + // Initialize device state + s_device_state.led_status = false; + mg_snprintf(s_device_state.firmware_version, + sizeof(s_device_state.firmware_version), "%s", + g_firmware_version); // Configure JSON-RPC functions we're going to handle - mg_rpc_add(&s_rpc, mg_str("config.set"), rpc_config_set, NULL); + mg_rpc_add(&s_rpc, mg_str("state.set"), rpc_state_set, NULL); mg_rpc_add(&s_rpc, mg_str("ota.upload"), rpc_ota_upload, NULL); - - mg_timer_add(mgr, 3000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_reconnect, - mgr); - mg_timer_add(mgr, ping_interval_ms, MG_TIMER_REPEAT, timer_ping, mgr); - mg_timer_add(mgr, 2000, MG_TIMER_REPEAT, timer_sntp, mgr); + + mg_timer_add(mgr, 3000, flags | MG_TIMER_RUN_NOW, timer_reconnect, mgr); + mg_timer_add(mgr, ping_interval_ms, flags, timer_ping, mgr); + mg_timer_add(mgr, 2000, flags, timer_sntp, mgr); } void web_free(void) { diff --git a/tutorials/mqtt/mqtt-dashboard/device/net.h b/tutorials/mqtt/mqtt-dashboard/device/net.h index ab33a85a..f774a017 100644 --- a/tutorials/mqtt/mqtt-dashboard/device/net.h +++ b/tutorials/mqtt/mqtt-dashboard/device/net.h @@ -17,6 +17,7 @@ extern "C" { extern char *g_mqtt_server_url; extern char *g_device_id; extern char *g_root_topic; +extern char *g_firmware_version; void web_init(struct mg_mgr *mgr); void web_free(void);