diff --git a/examples/mqtt-dashboard/dashboard/components.js b/examples/mqtt-dashboard/dashboard/components.js deleted file mode 120000 index dde5de4b5cd..00000000000 --- a/examples/mqtt-dashboard/dashboard/components.js +++ /dev/null @@ -1 +0,0 @@ -../../device-dashboard/web_root/components.js \ No newline at end of file diff --git a/examples/mqtt-dashboard/dashboard/history.min.js b/examples/mqtt-dashboard/dashboard/history.min.js deleted file mode 120000 index 13552193a7d..00000000000 --- a/examples/mqtt-dashboard/dashboard/history.min.js +++ /dev/null @@ -1 +0,0 @@ -../../device-dashboard/web_root/history.min.js \ No newline at end of file diff --git a/examples/mqtt-dashboard/dashboard/index.html b/examples/mqtt-dashboard/dashboard/index.html index 45f696f9f53..63a38dbd5ca 100644 --- a/examples/mqtt-dashboard/dashboard/index.html +++ b/examples/mqtt-dashboard/dashboard/index.html @@ -11,6 +11,5 @@ - diff --git a/examples/mqtt-dashboard/dashboard/main.css b/examples/mqtt-dashboard/dashboard/main.css index 134b49f8847..0ac9ea0e0e4 100644 --- a/examples/mqtt-dashboard/dashboard/main.css +++ b/examples/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: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.right-4{right:1rem}.top-0{top:0}.top-4{top:1rem}.isolate{isolation:isolate}.z-10{z-index:10}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mr-8{margin-right:2rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-2\.5{margin-top:.625rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-0{width:0}.w-11{width:2.75rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-96{width:24rem}.w-full{width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.flex-grow,.grow{flex-grow:1}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-5{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x:1.25rem}.translate-y-0{--tw-translate-y:0px}.translate-y-0,.translate-y-2{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-y-2{--tw-translate-y:0.5rem}.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}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.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}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(-1px*var(--tw-space-x-reverse));margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.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}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.truncate{overflow:hidden;white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity))}.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-200{--tw-bg-opacity:1;background-color:rgb(226 232 240/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))}.stroke-cyan-600{stroke:#0891b2}.stroke-1{stroke-width:1}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.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-3{padding-left:.75rem;padding-right:.75rem}.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-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pr-3{padding-right:.75rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.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}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-tight{letter-spacing:-.025em}.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-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}.text-green-900{--tw-text-opacity:1;color:rgb(20 83 45/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/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))}.opacity-0{opacity:0}.opacity-100{opacity:1}.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-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.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-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.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-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.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-opacity-5{--tw-ring-opacity:0.05}.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}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,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-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.hover\:bg-stone-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 244/var(--tw-bg-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.focus\:z-20:focus{z-index:20}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:outline-offset-0:focus{outline-offset:0}.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)}.focus-visible\:outline:focus-visible{outline-style:solid}.focus-visible\:outline-2:focus-visible{outline-width:2px}.focus-visible\:outline-offset-0:focus-visible{outline-offset:0}.focus-visible\:outline-blue-600:focus-visible{outline-color:#2563eb}.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 (prefers-color-scheme:dark){.dark\:border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity))}.dark\:bg-slate-900{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity))}.dark\:text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}}@media (min-width:640px){.sm\:flex{display:flex}.sm\:flex-1{flex:1 1 0%}.sm\:translate-x-0{--tw-translate-x:0px}.sm\:translate-x-0,.sm\:translate-x-2{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))}.sm\:translate-x-2{--tw-translate-x:0.5rem}.sm\:translate-y-0{--tw-translate-y:0px;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))}.sm\:items-start{align-items:flex-start}.sm\:items-end{align-items:flex-end}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:p-6{padding:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:leading-6{line-height:1.5rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file +/*! 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}.mr-8{margin-right:2rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.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-32{width:8rem}.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-none{flex:none}.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}.flex-wrap{flex-wrap:wrap}.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;white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.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-r{border-right-width:1px}.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-200{--tw-bg-opacity:1;background-color:rgb(226 232 240/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}.py-3{padding-top:.75rem;padding-bottom:.75rem}.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-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.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/examples/mqtt-dashboard/dashboard/main.js b/examples/mqtt-dashboard/dashboard/main.js index c633402a043..4202b762e6a 100644 --- a/examples/mqtt-dashboard/dashboard/main.js +++ b/examples/mqtt-dashboard/dashboard/main.js @@ -1,38 +1,171 @@ 'use strict'; import {h, html, render, useEffect, useRef, useState} from './bundle.js'; -import {Button, Colored, Icons, Notification, Setting, tipColors} from './components.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 = {pins: [], log_level: 0}; +const DefaultDeviceConfig = {pin_map: [], pin_state: [], log_level: 0, pin_count: 0}; +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 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 Colors = { + green: 'bg-green-100 text-green-900 ring-green-300', + yellow: 'bg-yellow-100 text-yellow-900 ring-yellow-300', + info: 'bg-zinc-100 text-zinc-900 ring-zinc-300', + red: 'bg-red-100 text-red-900 ring-red-300', +}; + 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``, +}; + +export function Button({title, onclick, disabled, extraClass, icon, ref, colors, hovercolor, disabledcolor}) { + const [spin, setSpin] = useState(false); + const cb = function(ev) { + const res = onclick ? onclick() : null; + if (res && typeof (res.catch) === 'function') { + setSpin(true); + res.catch(() => false).then(() => setSpin(false)); + } + }; + if (!colors) colors = 'bg-blue-600 hover:bg-blue-500 disabled:bg-blue-400'; + return html` +`; +}; + function Header({topic, setTopic, url, setUrl, connected}) { const forbiddenChars = ['$', '*', '+', '#', '/']; const onClick = () => { const isValidTopic = val => !forbiddenChars.some(char => val.includes(char)); if (isValidTopic(topic)) { localStorage.setItem('topic', topic) - setTopicFn(topic); + setTopic(topic); window.location.reload(); } else { setSaveResult('Error: The topic cannot contain these characters: ' + forbiddenChars); } }; + return html`
-

MQTT Dashboard

- powered by Mongoose +

Device Management Dashboard

- <${Setting} value=${url} addonLeft="MQTT Server" cls="py-1 w-96" disabled=${connected} /> - <${Setting} value=${topic} addonLeft="Root Topic" cls="py-1" disabled=${connected} /> - <${Button} icon=${Icons.link} onclick=${onClick} title=${connected ? 'Disconnect' : 'Connect'} /> +
+ MQTT Server + setUrl(ev.target.value)} class=${InputClass} /> + +
+ Root Topic + setTopic(ev.target.value)} class=${InputClass} /> + + <${Button} icon=${Icons.bolt} onclick=${onClick} title=${connected ? 'Disconnect' : 'Connect'} /> `; @@ -47,9 +180,7 @@ function Sidebar({devices, onclick}) {
onclick(d.id)}> ${d.id} - <${Colored} - colors=${d.online ? tipColors.green : tipColors.red} - text=${d.online ? 'online' : 'offline'} /> + ${d.online ? 'online' : 'offline'} `; return html` @@ -82,104 +213,88 @@ function FirmwareStatus({title, info, 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})) +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'); + } }; - - // 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 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()); }); - }; - - 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; - }; + fetch('api/ecu/update/begin', opts).then(r => r.ok ? write() : fail()); + }); + const onflash = ev => update(false); + const onrollback = ev => update(true); - if (props.clean) { - setStatusByID(null, props.id) - props.setCleanFn(false) - } + useEffect(function() { + //setOtaStatus(anew ? 'update pending' : 'up-to-date'); + //setColor(anew ? Colors.yellow : Colors.green); + }, []); return html` -
- - <${Button} title=${props.title} icon=${Icons.download} onclick=${ - onclick} ref=${btn} colors=${props.colors} disabled=${props.disabled} /> -
${ - status[props.id]} +
+
+ 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} /> + + +
+ 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} /> + + + `; }; -function FirmwareUpdate({publishFn, disabled, info, deviceID}) { +/* +function xFirmwareUpdatePanel({publishFn, disabled, info, deviceID}) { const [clean, setClean] = useState(false) const refresh = () => {}; useEffect(refresh, []); @@ -202,6 +317,17 @@ function FirmwareUpdate({publishFn, disabled, info, deviceID}) { }; const defaultInfo = {status: 0, crc32: 0, size: 0, timestamp: 0}; + + return html` +
+
+
Firmware Update + +
+ boo! + +`; + return html`
@@ -240,89 +366,112 @@ function FirmwareUpdate({publishFn, disabled, info, deviceID}) { `; }; +*/ -function DeviceControlPanel({device, setDeviceConfig, publishFn, connected}) { - const cfg = device && device.config ? device.config : DefaultDeviceConfig; - const [localConfig, setLocalConfig] = useState(cfg); - const [saveResult, setSaveResult] = useState(null); - - useEffect(() => setLocalConfig(cfg), [device]); - +function DeviceSettingsPanel({device, publishFn, connected}) { + const [config, setConfig] = useState(device.config); const logOptions = [[0, 'Disable'], [1, 'Error'], [2, 'Info'], [3, 'Debug']]; - const mksetfn = k => (v => setLocalConfig(x => Object.assign({}, x, {[k]: v}))); - const onSave = ev => publishFn('config.set', localConfig).then(r => { - setDeviceConfig(device.id, localConfig) - setSaveResult('Success!') - }).catch(e => { - setDeviceConfig(device.id, device.config) - setSaveResult('Failed!') - }) - - if (!device || !localConfig) { - return html` -
- No device selected. Click on a device on a sidebar - `; - } + const onSave = ev => publishFn('config.set', config).then(r => { + //setDeviceConfig(device.id, config); + }).catch(e => alert('Failure!')); // To delete device, set an empty retained message const onforget = function(ev) { MqttClient.publish(device.topic, '', {retain: true}); location.reload(); }; - - const mksetpin = (pin, val) => setLocalConfig(x => { - let pins = x.pins.slice(); - pins[pin] = val ? 0 : 1; - return Object.assign({}, x, {pins}); - }); - - const settingstyle = 'inline-block mr-8 my-1 bg-gray-100 rounded px-2'; - const Pin = (value, pin) => html` -
- <${Setting} - setfn=${ev => mksetpin(pin, value)} - type="switch" value=${value} xcls="py-1 w-32 overflow-auto" - disabled=${!device.online || !connected} title="Pin ${pin}" /> - `; + 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(config); return html` -
-
- -
-
- Device ${device.id} - ${html`<${Colored} - colors=${device.online ? tipColors.green : tipColors.red} - text=${ device.online ? 'online' : 'offline'} />`} - - <${Button} title="Forget this device" - disabled=${device.online || !connected} - icon=${Icons.fail} onclick=${onforget}/> +
+
+
Settings + +
+
+ Device ${device.id} + ${connected ? 'online' : 'offline'} + +
+ Log Level + + + -
- ${localConfig.pins.map((val, pin) => Pin(val, pin))} -
+
+ <${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} /> + + + `; +}; -
- <${Setting} title="Log Level" type="select" - value=${localConfig.log_level} setfn=${mksetfn('log_level')} - options=${logOptions} 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!')); + }; -
- ${saveResult && html`<${Notification} ok=${saveResult === 'Success!'} - text=${saveResult} close=${() => setSaveResult(null)} />`} - <${Button} icon=${Icons.save} onclick=${onSave} - title="Save Settings" disabled=${!device.online} /> - - + const Pin = i => html` +
+
+ Pin ${config.pin_map[i]} + <${Toggle} onclick=${ev => onclick(i)} + disabled=${!device.online || !connected} + value=${config.pin_state[i]} /> + `; - <${FirmwareUpdate} deviceID=${device.id} publishFn=${publishFn} + 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) { + 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]} />
`; @@ -521,7 +670,7 @@ const App = function() { />
<${Sidebar} devices=${devices} onclick=${onDeviceClick} /> - <${DeviceControlPanel} + <${DeviceDashboard} device=${getDeviceByID(currentDevID)} connected=${connected} setDeviceConfig=${setDeviceConfig} publishFn=${handlePublish} /> diff --git a/examples/mqtt-dashboard/device/Makefile b/examples/mqtt-dashboard/device/Makefile index ba864f72378..005a4c75d10 100644 --- a/examples/mqtt-dashboard/device/Makefile +++ b/examples/mqtt-dashboard/device/Makefile @@ -1,8 +1,8 @@ -PROG ?= ./example # Program we are building -DELETE ?= rm -rf # Command to remove files -OUT ?= -o $(PROG) # Compiler argument for output file -SOURCES = main.c net.c mongoose.c # Source code files -CFLAGS ?= -W -Wall -Wextra -g -I. # Build options +PROG ?= ./example # Program we are building +DELETE ?= rm -rf # Command to remove files +OUT ?= -o $(PROG) # Compiler argument for output file +SOURCES = main.c net.c hal.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 diff --git a/examples/mqtt-dashboard/device/main.c b/examples/mqtt-dashboard/device/main.c index 2eb15adc768..0d64b35cd6d 100644 --- a/examples/mqtt-dashboard/device/main.c +++ b/examples/mqtt-dashboard/device/main.c @@ -2,6 +2,7 @@ // All rights reserved #include "net.h" +#include "hal.h" // Handle interrupts, like Ctrl-C static int s_signo; @@ -9,22 +10,6 @@ static void signal_handler(int signo) { s_signo = signo; } -// Mocked device pins -static bool s_pins[NUM_PINS]; - -bool hal_gpio_write(int pin, bool status) { - bool ok = false; - if (pin >= 0 && pin < NUM_PINS) { - s_pins[pin] = status; - ok = true; - } - return ok; -} - -bool hal_gpio_read(int pin) { - return (pin >= 0 && pin < NUM_PINS) ? s_pins[pin] : false; -} - int main(int argc, char *argv[]) { struct mg_mgr mgr; int i; @@ -32,7 +17,7 @@ int main(int argc, char *argv[]) { // Parse command-line flags for (i = 1; i < argc; i++) { if (strcmp(argv[i], "-u") == 0 && argv[i + 1] != NULL) { - g_url = argv[++i]; + g_mqtt_server_url = argv[++i]; } else if (strcmp(argv[i], "-i") == 0 && argv[i + 1] != NULL) { g_device_id = strdup(argv[++i]); } else if (strcmp(argv[i], "-t") == 0 && argv[i + 1] != NULL) { diff --git a/examples/mqtt-dashboard/device/net.c b/examples/mqtt-dashboard/device/net.c index 2fec222b5e9..0dedefeb5aa 100644 --- a/examples/mqtt-dashboard/device/net.c +++ b/examples/mqtt-dashboard/device/net.c @@ -3,7 +3,7 @@ #include "net.h" -char *g_url = MQTT_SERVER_URL; +char *g_mqtt_server_url = MQTT_SERVER_URL; char *g_root_topic = MQTT_ROOT_TOPIC; char *g_device_id; @@ -12,8 +12,10 @@ static struct mg_connection *s_conn; // MQTT Client connection static struct mg_rpc *s_rpc = NULL; // List of registered RPC methods struct device_config { - int pins[NUM_PINS]; // State of the GPIO pins - int log_level; // Device logging level, 0-4 + int pin_count; // Number of pins to handle + int pin_map[MAX_PINS]; // Pins to handle + int pin_state[MAX_PINS]; // State of the GPIO pins + int log_level; // Device logging level, 0-4 }; static struct device_config s_device_config; @@ -75,12 +77,16 @@ static void publish_status(struct mg_connection *c) { // Print JSON notification into the io buffer mg_xprintf(mg_pfn_iobuf, &io, - "{%m:%m,%m:{%m:%m,%m:%d,%m:[%M],%m:%M,%m:%M}}", // + "{%m:%m,%m:{%m:%m,%m:%d,%m:%d,%m:[%M],%m:[%M],%m:%M,%m:%M}}", // MG_ESC("method"), MG_ESC("status.notify"), MG_ESC("params"), // MG_ESC("status"), MG_ESC("online"), // MG_ESC(("log_level")), s_device_config.log_level, // - MG_ESC(("pins")), print_ints, s_device_config.pins, NUM_PINS, // - MG_ESC(("crnt_fw")), print_fw_status, MG_FIRMWARE_CURRENT, // + MG_ESC(("pin_count")), s_device_config.pin_count, // + MG_ESC(("pin_map")), print_ints, s_device_config.pin_map, + s_device_config.pin_count, // + MG_ESC(("pin_state")), print_ints, s_device_config.pin_state, + s_device_config.pin_count, // + MG_ESC(("crnt_fw")), print_fw_status, MG_FIRMWARE_CURRENT, // MG_ESC(("prev_fw")), print_fw_status, MG_FIRMWARE_PREVIOUS); memset(&pub_opts, 0, sizeof(pub_opts)); @@ -90,7 +96,6 @@ static void publish_status(struct mg_connection *c) { pub_opts.qos = s_qos; pub_opts.retain = true; mg_mqtt_pub(c, &pub_opts); - MG_INFO(("%lu PUBLISHED %s -> %.*s", c->id, topic, io.len, io.buf)); mg_iobuf_free(&io); } @@ -103,7 +108,6 @@ static void publish_response(struct mg_connection *c, char *buf, size_t len) { pub_opts.message = mg_str_n(buf, len); pub_opts.qos = s_qos; mg_mqtt_pub(c, &pub_opts); - MG_INFO(("%lu PUBLISHED %s -> %.*s", c->id, topic, len, buf)); } static void subscribe(struct mg_connection *c) { @@ -120,20 +124,25 @@ static void subscribe(struct mg_connection *c) { 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 < NUM_PINS; i++) { - char path[20]; - mg_snprintf(path, sizeof(path), "$.params.pins[%lu]", i); - val = (int) mg_json_get_long(r->frame, path, -1); - if (val >= 0 && val != dc.pins[i]) { - dc.pins[i] = val; - hal_gpio_write((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] = (int) mg_json_get_long(r->frame, path, -1); + 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; @@ -201,7 +210,7 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) { 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, g_url)); + MG_INFO(("%lu CONNECTED to %s", c->id, g_mqtt_server_url)); subscribe(c); publish_status(c); } else if (ev == MG_EV_MQTT_MSG) { @@ -244,7 +253,7 @@ static void timer_reconnect(void *arg) { opts.keepalive = MQTT_KEEPALIVE_SEC; opts.retain = true; opts.message = mg_str(message); - s_conn = mg_mqtt_connect(mgr, g_url, &opts, fn, NULL); + s_conn = mg_mqtt_connect(mgr, g_mqtt_server_url, &opts, fn, NULL); } } @@ -257,8 +266,14 @@ void web_init(struct mg_mgr *mgr) { int i, ping_interval_ms = MQTT_KEEPALIVE_SEC * 1000 - 500; set_device_id(); s_device_config.log_level = (int) mg_log_level; - for (i = 0; i < NUM_PINS; i++) { - s_device_config.pins[i] = hal_gpio_read(i); + 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]); } // Configure JSON-RPC functions we're going to handle diff --git a/examples/mqtt-dashboard/device/net.h b/examples/mqtt-dashboard/device/net.h index d7d13fd64c6..ab33a85a881 100644 --- a/examples/mqtt-dashboard/device/net.h +++ b/examples/mqtt-dashboard/device/net.h @@ -3,6 +3,7 @@ #pragma once #include "mongoose.h" +#include "hal.h" #ifdef __cplusplus extern "C" { @@ -12,18 +13,14 @@ extern "C" { #define MQTT_KEEPALIVE_SEC 60 #define MQTT_SERVER_URL "mqtt://broker.hivemq.com:1883" #define MQTT_ROOT_TOPIC "mg_mqtt_dashboard" -#define NUM_PINS 30 -extern char *g_url; +extern char *g_mqtt_server_url; extern char *g_device_id; extern char *g_root_topic; void web_init(struct mg_mgr *mgr); void web_free(void); -bool hal_gpio_write(int pin, bool status); -bool hal_gpio_read(int pin); - #ifdef __cplusplus } #endif diff --git a/examples/rp2040/pico-w5500/CMakeLists.txt b/examples/rp2040/pico-w5500/CMakeLists.txt index b076f8feadb..1e74360de7e 100644 --- a/examples/rp2040/pico-w5500/CMakeLists.txt +++ b/examples/rp2040/pico-w5500/CMakeLists.txt @@ -3,12 +3,8 @@ include(pico-sdk/pico_sdk_init.cmake) project(firmware) pico_sdk_init() - -add_executable(firmware - main.c mongoose.c net.c packed_fs.c) - -target_include_directories(firmware PUBLIC - .) +add_executable(firmware main.c mongoose.c net.c packed_fs.c) +target_include_directories(firmware PUBLIC .) target_link_libraries(firmware pico_stdlib hardware_spi pico_rand) pico_add_extra_outputs(firmware) # create map/bin/hex file etc. @@ -19,7 +15,6 @@ pico_enable_stdio_uart(firmware 1) # to the UART, for remote testing # Mongoose build flags add_definitions(-DMG_ENABLE_TCPIP=1) add_definitions(-DMG_ENABLE_PACKED_FS=1) -add_definitions(-DMG_ENABLE_MBEDTLS=0) # TODO(cpq): enable add_definitions(-DMG_ENABLE_CUSTOM_RANDOM=1) add_definitions(-DMG_ENABLE_POSIX_FS=0) diff --git a/examples/rp2040/pico-w5500/Makefile b/examples/rp2040/pico-w5500/Makefile index 708039f4414..39705ca3641 100644 --- a/examples/rp2040/pico-w5500/Makefile +++ b/examples/rp2040/pico-w5500/Makefile @@ -4,6 +4,7 @@ ifeq ($(OS),Windows_NT) RM = cmd /C del /Q /F /S MKBUILD = if not exist build mkdir build endif +.PHONY: build all example: true diff --git a/examples/rp2040/pico-w5500/mbedtls_config.h b/examples/rp2040/pico-w5500/mbedtls_config.h deleted file mode 100644 index dae339567cf..00000000000 --- a/examples/rp2040/pico-w5500/mbedtls_config.h +++ /dev/null @@ -1,63 +0,0 @@ -/* Workaround for some mbedtls source files using INT_MAX without including limits.h */ -#include - -#define MBEDTLS_NO_PLATFORM_ENTROPY -#define MBEDTLS_ENTROPY_HARDWARE_ALT - -#define MBEDTLS_SSL_OUT_CONTENT_LEN 2048 - -#define MBEDTLS_ALLOW_PRIVATE_ACCESS -#define MBEDTLS_HAVE_TIME - -#define MBEDTLS_CIPHER_MODE_CBC -#define MBEDTLS_ECP_DP_SECP192R1_ENABLED -#define MBEDTLS_ECP_DP_SECP224R1_ENABLED -#define MBEDTLS_ECP_DP_SECP256R1_ENABLED -#define MBEDTLS_ECP_DP_SECP384R1_ENABLED -#define MBEDTLS_ECP_DP_SECP521R1_ENABLED -#define MBEDTLS_ECP_DP_SECP192K1_ENABLED -#define MBEDTLS_ECP_DP_SECP224K1_ENABLED -#define MBEDTLS_ECP_DP_SECP256K1_ENABLED -#define MBEDTLS_ECP_DP_BP256R1_ENABLED -#define MBEDTLS_ECP_DP_BP384R1_ENABLED -#define MBEDTLS_ECP_DP_BP512R1_ENABLED -#define MBEDTLS_ECP_DP_CURVE25519_ENABLED -#define MBEDTLS_KEY_EXCHANGE_RSA_ENABLED -#define MBEDTLS_PKCS1_V15 -#define MBEDTLS_SHA256_SMALLER -#define MBEDTLS_SSL_SERVER_NAME_INDICATION -#define MBEDTLS_AES_C -#define MBEDTLS_ASN1_PARSE_C -#define MBEDTLS_BIGNUM_C -#define MBEDTLS_CIPHER_C -#define MBEDTLS_CTR_DRBG_C -#define MBEDTLS_ENTROPY_C -#define MBEDTLS_ERROR_C -#define MBEDTLS_MD_C -#define MBEDTLS_MD5_C -#define MBEDTLS_OID_C -#define MBEDTLS_PKCS5_C -#define MBEDTLS_PK_C -#define MBEDTLS_PK_PARSE_C -#define MBEDTLS_PLATFORM_C -#define MBEDTLS_RSA_C -#define MBEDTLS_SHA1_C -#define MBEDTLS_SHA224_C -#define MBEDTLS_SHA256_C -#define MBEDTLS_SHA512_C -#define MBEDTLS_SSL_CLI_C -#define MBEDTLS_SSL_SRV_C -#define MBEDTLS_SSL_TLS_C -#define MBEDTLS_X509_CRT_PARSE_C -#define MBEDTLS_X509_USE_C -#define MBEDTLS_AES_FEWER_TABLES - -/* TLS 1.2 */ -#define MBEDTLS_SSL_PROTO_TLS1_2 -#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED -#define MBEDTLS_GCM_C -#define MBEDTLS_ECDH_C -#define MBEDTLS_ECP_C -#define MBEDTLS_ECDSA_C -#define MBEDTLS_ASN1_WRITE_C -