From 8810d564cce6dd97ba2620e4004ef944f7b1cba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EB=B3=91=EC=A4=80?= Date: Sat, 3 Aug 2024 04:42:58 +0900 Subject: [PATCH] update: full implementation of device info section --- web/index.html | 129 +++++++++++++++++++++++++++++++++++------------- web/protocol.js | 9 +++- web/script.js | 82 ++++++++++++++++++------------ web/strings.js | 1 + web/style.css | 94 +++++++++++++++++++++++++++++++++++ web/ui.js | 77 ++++++++++++++++++++++++++++- 6 files changed, 322 insertions(+), 70 deletions(-) diff --git a/web/index.html b/web/index.html index 093f876..468e481 100644 --- a/web/index.html +++ b/web/index.html @@ -1,37 +1,98 @@ - - - - - - - - FSK-EEM Console - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + FSK-EEM Console + + + + + + + + + + + + + +
+
+

에너지미터 관리

+
+
+

장치 정보

+
+
+ 장치 연결 + ID 설정 + 시계 동기화 +
+
+ + + ID: N/A + + + + - GB / - GB +  (- %) + + + + N/A + +
+
+
+
+
+
+

로그 파일 관리

+
+
+ 새로고침 + 전체 다운로드 + 전체 삭제 +
+
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + diff --git a/web/protocol.js b/web/protocol.js index 4ddcebd..fa4d714 100644 --- a/web/protocol.js +++ b/web/protocol.js @@ -24,6 +24,9 @@ const RESP = { const USB_VID = 0x1999; const USB_PID = 0x0512; +const DEVICE_ID_INVALID = 0xFFFF; +const DEVICE_ID_BROADCAST = 0xFFFE; + const QUERY_TIMEOUT = 500; /****************************************************************************** @@ -35,12 +38,12 @@ const QUERY_TIMEOUT = 500; * QUERY: 5-byte decimal integer string(00000 ~ 65535) for a new device id * RESPONSE: $OK or $ERROR *****************************************************************************/ -async function cmd_set_id(new_id) { +async function cmd_set_id(id) { if (!await check_connection()) { return false; } - let query = `${CMD.SET_ID} ${String(new_id).padStart(5, '0')}` + let query = `${CMD.SET_ID} ${String(id).padStart(5, '0')}` let res = await transceive(query, RESP.OK); if (!res) { @@ -120,6 +123,8 @@ async function cmd_load_list() { toastr.success('파일 목록 수신 완료'); ui_load_list(res); + + await cmd_load_info(); } /****************************************************************************** diff --git a/web/script.js b/web/script.js index ddbf585..58acda0 100644 --- a/web/script.js +++ b/web/script.js @@ -7,25 +7,43 @@ document.addEventListener("DOMContentLoaded", (_e) => { return; } - document.getElementById("connect").addEventListener("click", connect); + document.getElementById("connect").addEventListener("click", check_connection); + document.getElementById("set-id").addEventListener("click", ui_cmd_set_id); + document.getElementById("set-rtc").addEventListener("click", cmd_set_rtc); document.getElementById("load-list").addEventListener("click", cmd_load_list); }); +/************************************************************************************ + * format bytes to human readable text + ***********************************************************************************/ +function format_byte(bytes, decimals = 2) { + if (!+bytes) { + return '0 Byte'; + } + + const k = 1000; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; +} /************************************************************************************ * new Date().format() ***********************************************************************************/ -var dateFormat = function () { - var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, +var dateFormat = function() { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, timezoneClip = /[^-+\dA-Z]/g, - pad = function (val, len) { + pad = function(val, len) { val = String(val); len = len || 2; while (val.length < len) val = '0' + val; return val; }; - return function (date, mask, utc) { + return function(date, mask, utc) { var dF = dateFormat; if (arguments.length == 1 && Object.prototype.toString.call(date) == '[object String]' && !/\d/.test(date)) { mask = date; @@ -38,7 +56,7 @@ var dateFormat = function () { mask = mask.slice(4); utc = true; } - var _ = utc ? 'getUTC' : 'get', + var _ = utc ? 'getUTC' : 'get', d = date[_ + 'Date'](), D = date[_ + 'Day'](), m = date[_ + 'Month'](), @@ -49,40 +67,40 @@ var dateFormat = function () { L = date[_ + 'Milliseconds'](), o = utc ? 0 : date.getTimezoneOffset(), flags = { - d: d, - dd: pad(d), - ddd: dF.i18n.dayNames[D], + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], dddd: dF.i18n.dayNames[D + 7], - m: m + 1, - mm: pad(m + 1), - mmm: dF.i18n.monthNames[m], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], mmmm: dF.i18n.monthNames[m + 12], - yy: String(y).slice(2), + yy: String(y).slice(2), yyyy: y, - h: H % 12 || 12, - hh: pad(H % 12 || 12), - H: H, - HH: pad(H), - M: M, - MM: pad(M), - s: s, - ss: pad(s), - l: pad(L, 3), - L: pad(L > 99 ? Math.round(L / 10) : L), - t: H < 12 ? 'a' : 'p', - tt: H < 12 ? 'am' : 'pm', - T: H < 12 ? 'A' : 'P', - TT: H < 12 ? '오전' : '오후', - Z: utc ? 'UTC' : (String(date).match(timezone) || ['']).pop().replace(timezoneClip, ''), - o: (o > 0 ? '-' : '+') + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), - S: ['th', 'st', 'nd', 'rd'][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? 'a' : 'p', + tt: H < 12 ? 'am' : 'pm', + T: H < 12 ? 'A' : 'P', + TT: H < 12 ? '오전' : '오후', + Z: utc ? 'UTC' : (String(date).match(timezone) || ['']).pop().replace(timezoneClip, ''), + o: (o > 0 ? '-' : '+') + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ['th', 'st', 'nd', 'rd'][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] }; - return mask.replace(token, function ($0) { + return mask.replace(token, function($0) { return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); }); }; }(); -dateFormat.masks = {'default':'ddd mmm dd yyyy HH:MM:ss'}; +dateFormat.masks = { 'default': 'ddd mmm dd yyyy HH:MM:ss' }; dateFormat.i18n = { dayNames: [ '일', '월', '화', '수', '목', '금', '토', diff --git a/web/strings.js b/web/strings.js index 4d94ff9..7643652 100644 --- a/web/strings.js +++ b/web/strings.js @@ -1,3 +1,4 @@ const html_strings = { no_webserial: 'WebSerial을 지원하지 않는 브라우저입니다.
크롬 등 다른 최신 브라우저를 사용하세요.', + id_error: 'ID 값이 올바르지 않습니다.
ID는 0 ~ 65533 이내의 정수입니다.', }; diff --git a/web/style.css b/web/style.css index e69de29..7b89b69 100644 --- a/web/style.css +++ b/web/style.css @@ -0,0 +1,94 @@ +@import url("https://statics.goorm.io/fonts/GoormSans/v1.0.0/GoormSans.min.css"); + +html { + font-family: 'Goorm Sans', sans-serif; + background-color: rgb(243, 244, 246); + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +body { + overflow-y: auto; +} + +i.fa-fw { + margin-right: 0.5rem; +} + +div#container, div#header { + padding: 1rem; + padding-top: 0rem; + display: flex; + max-width: none; + flex-wrap: wrap; +} + +div#container { + justify-content: space-evenly; +} + +article { + margin-top: 1.5rem; + padding: 1rem; + background-color: white; + border-radius: 10px; + min-width: 320px; + width: -webkit-fill-available; +} + +article h1 { + font-size: 1.5rem; + margin: 0px; + color: rgb(59,130,246); +} + +article h2 { + font-size: 1.3rem; + margin: 0px; + color: #333333; +} + +article .content { + padding-top: 1.5rem; + padding-left: 1rem; + line-height: 2rem; +} + +article .content .section { + margin-top: 0.5rem; + margin-left: 1rem; + margin-bottom: 2rem; +} + +.disabled { + opacity: 30%; + pointer-events: none; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type=number] { + appearance: textfield; + -moz-appearance: textfield; +} + +.btn { + padding: 4px 12px 2px 12px; +} + +.connected { + color: green; +} + +span.device-info { + margin-left: 0.5rem; + margin-right: 2rem; +} diff --git a/web/ui.js b/web/ui.js index db284b3..c918343 100644 --- a/web/ui.js +++ b/web/ui.js @@ -17,20 +17,24 @@ async function check_connection() { }] }); + document.getElementById("connection").style.color = "green"; toastr.success('장치 연결 성공'); // disconnect event handler port.addEventListener("disconnect", (_e) => { port = undefined; connection = false; + clock = undefined; + document.getElementById("device-id").innerText = "N/A"; + document.getElementById("connection").style.color = "red"; toastr.error(`장치 연결 해제`); }); await port.open({ baudRate: 9600 }); connection = true; - await cmd_load_info(); + await cmd_load_list(); return true; } catch (e) { @@ -44,11 +48,65 @@ async function check_connection() { } } +/****************************************************************************** + * $SET-ID user prompt handler + *****************************************************************************/ +async function ui_cmd_set_id() { + if (!await check_connection()) { + return false; + } + + const { isConfirmed, value } = await Swal.fire({ + title: "장치 ID 설정", + input: "text", + showCancelButton: true, + confirmButtonText: "확인", + cancelButtonText: "취소", + customClass: { confirmButton: "btn green", cancelButton: "btn yellow" }, + preConfirm: (value) => { + if (!value || !Number.isInteger(Number(value)) || Number(value) < 0 || Number(value) >= DEVICE_ID_BROADCAST) { + return Swal.showValidationMessage(html_strings.id_error); + } + } + }); + + if (!isConfirmed) { + return false; + } + + cmd_set_id(Number(value)); +} + /****************************************************************************** * $LOAD-INFO response UI handler *****************************************************************************/ function ui_load_info(res) { - // TODO + res = res.text.replace(RESP.OK, '').split(' '); + + let id = res[0]; + let total_sector = Number(res[1]); + let free_sector = Number(res[2]); + let sector_size = Number(res[3]); + let rtc = res[4]; + + document.getElementById("device-id").innerText = id; + + let total = total_sector * sector_size; + let free = free_sector * sector_size; + let used = total - free; + let usage = used / total * 100; + + document.getElementById("storage-free").innerText = format_byte(used); + document.getElementById("storage-total").innerText = format_byte(total); + document.getElementById("storage-percent").innerText = usage.toFixed(2); + + let date = rtc.substring(0, 8); + let time = rtc.substring(9).replace(/-/g, ':'); + let century = Math.floor(new Date().getFullYear() / 100); + + clock = new Date(Date.parse(`${century}${date}T${time}`)); + + document.getElementById("device-clock").innerHTML = clock.format("yyyy-mm-dd HH:MM:ss").replace(' ', ' '); } /****************************************************************************** @@ -80,6 +138,7 @@ function ui_load_list(res) { } }); + console.log(res); // TODO } @@ -87,6 +146,7 @@ function ui_load_list(res) { * $LOAD-ALL response UI handler *****************************************************************************/ function ui_load_all(res) { + console.log(res); // TODO } @@ -94,9 +154,22 @@ function ui_load_all(res) { * $LOAD-ONE response UI handler *****************************************************************************/ function ui_load_one(res) { + console.log(res); // TODO } +/****************************************************************************** + * device clock updater + *****************************************************************************/ +let clock = undefined; + +setInterval(() => { + if (clock) { + clock.setSeconds(clock.getSeconds() + 1); + document.getElementById("device-clock").innerHTML = clock.format("yyyy-mm-dd HH:MM:ss").replace(' ', ' '); + } +}, 1000); + /****************************************************************************** * Alert windows *****************************************************************************/