Initial commit: HospitalCCTV Web Monitor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 10:10:55 +09:00
commit 9cebb20146
25 changed files with 3868 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg-info/
dist/
build/
*.egg
# Virtual environments
venv/
env/
.venv/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
Thumbs.db
.DS_Store
# Claude
.claude/

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
fastapi==0.83.0
uvicorn==0.16.0
jinja2==3.0.3
paho-mqtt==1.6.1

4
src/common/const.py Normal file
View File

@@ -0,0 +1,4 @@
# Hospital Fall Monitor Web Server
SERVICE_PORT = 50777
SW_VERSION = "0.0.1"

View File

@@ -0,0 +1,57 @@
/* Root element */
.json-document {
/* padding: 1em 2em; */
}
/* Syntax highlighting for JSON objects */
ul.json-dict, ol.json-array {
list-style-type: none;
margin: 0 0 0 1px;
border-left: 1px dotted #ccc;
padding-left: 2em;
}
.json-string {
color: #0B7500;
}
.json-literal {
/* color: #1A01CC; */
/* font-weight: bold; */
}
/* Toggle button */
a.json-toggle {
position: relative;
color: inherit;
text-decoration: none;
}
a.json-toggle:focus {
outline: none;
}
a.json-toggle:before {
font-size: 1.1em;
color: #c0c0c0;
content: "\25BC"; /* down arrow */
position: absolute;
display: inline-block;
width: 1em;
text-align: center;
line-height: 1em;
left: -1.2em;
}
a.json-toggle:hover:before {
color: #aaa;
}
a.json-toggle.collapsed:before {
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
transform: rotate(-90deg);
}
/* Collapsable placeholder links */
a.json-placeholder {
color: #aaa;
padding: 0 1em;
text-decoration: none;
}
a.json-placeholder:hover {
text-decoration: underline;
}

481
src/static/css/style.css Normal file
View File

@@ -0,0 +1,481 @@
body {
background-color: #f1f1f0;
height: 100%;
width: 98%;
overflow-x:auto;
overflow-y:auto;
margin: 8px;
}
p {
font-size:15px;
}
.header {
position: relative;
margin-bottom: 55px;
}
h {
font-weight:bold;
font-size:30px;
}
hr {
margin: 40px 0px 40px 0px;
opacity: 50%;
}
/* 상단 메뉴바 설정 */
.apply_btn {
font-weight:bold;
width: 90px;
height:27px;
position: relative;
}
/* 메시지 박스 라인 수 및 서버 주소 설정 */
.set_info {
font-weight:bold;
position: relative;
padding: 12px;
line-height: 15px;
border-radius: 0.2rem;
border: 1px solid gray;
margin: 20px 0px 60px 0px;
}
.input_num {
width:50px;
height:23px;
margin-right: 10px;
position: relative;
display: inline-block;
}
.input_text {
width:150px;
height:23px;
margin-right: 10px;
position: relative;
display: inline-block;
}
/* msg box line 설정 */
.set_msg_line {
display: inline-block;
margin-bottom: 24px;
float: left;
margin-right: 50px;
}
.msg_box_line {
position: relative;
display: inline-block;
}
.line_apply {
display: inline-block;
}
/* server address 설정 */
.set_mqtt_server {
display: inline-block;
}
.set_server {
margin-bottom: 12px;
}
.server_address_txt {
display: inline-block;
}
.server_address_txt_input {
display: inline-block;
}
.server_address_btn {
display: inline-block;
}
#server_connect {
font-weight:bold;
width: 114px;
height:27px;
margin-right: 30px;
position: relative;
}
#mqtt_user_id {
width: 50px
}
#mqtt_user_pw {
width: 50px
}
/* server preset 설정 */
.fieldset_server {
padding-bottom: 0px;
}
#applied_server_txt {
display: inline-block;
color: orangered;
}
.preset_btn {
display: inline-block;
margin: 10px;
}
.preset_btn_box {
height: 27px;
}
/* 메시지 영역 */
.AI_message {
margin-bottom: 50px;
}
.message {
margin-bottom: 20px;
}
.clear_btn {
margin-left: 10px;
margin-right: 30px;
width: 13px;
height:13px;
}
.msg_container {
margin-top: 5px;
font-size: small;
border: solid 1px;
background-color: white;
overflow-y: scroll;
height: 180px;
width: 98%;
resize: vertical;
}
.ai_images {
margin-top: 5px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
}
/* object별 이미지 영역 */
.object_div {
min-width: 200px;
max-width: 280px;
height: fit-content;
margin-bottom: 10px;
margin-right: 10px;
background-color: white;
border: solid 1px;
display: flex;
flex-direction: column;
align-items: center;
}
.object_info {
width: 100%;
padding: 8px;
background-color: #f1f1f0;
font-size: 13px;
}
.object_info_item {
margin-bottom: 2px;
}
.severity_critical {
color: red;
font-weight: bold;
}
.severity_medium {
color: orange;
font-weight: bold;
}
.severity_low {
color: green;
}
.object_img_container {
width: 100%;
height: 180px;
padding: 4px;
}
.object_img_container img {
width: 100%;
height: 100%;
object-fit: contain;
}
/* toggle message */
.toggle_msg_ul {
list-style-type: none;
margin: 0px;
padding: 0px;
}
.caret2::before {
cursor: pointer;
user-select: none;
content: "\25B6";
color: black;
display: inline-block;
margin-right: 6px;
}
.caret2-down::before {
-ms-transform: rotate(90deg);
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.nested2 {
list-style-type: none;
display: none;
padding-left: 20px;
}
.active {
display: block;
}
/* snapshot */
.snapshot {
width: auto;
height: auto;
margin-bottom: 15px;
}
.snapshot_set {
border: 1px solid gray;
padding: 0.5rem;
line-height: 1rem;
border-radius: 0.2rem;
width: 220px;
margin-bottom: 5px;
display: flex;
align-items: center;
height: 15px;
}
.snapshot_name {
font-weight:bold;
display: inline-block;
margin: auto;
}
.snapshot_btn{
width: 20px;
height: 20px;
margin-left: 10px;
}
.snapshot_img {
display: none;
width: 500px;
height: 300px;
}
/* PTZ 카메라 설정 */
.ptz_mode {
display: inline-block;
width: 100%
}
.ptz_mode_option {
display: inline-block;
left: 25%;
position: relative;
}
.ptz_mode_name {
display: inline-block;
font-weight: bold;
}
.ptz_mode_btn {
display: inline-block;
position: relative;
left: 36%;
}
.ptz_apply_btn {
font-weight: bold;
width: 120px;
height: 27px;
position: relative;
}
#get_cam_current {
display: block;
width: 50px;
margin-bottom: 10px
}
.ptz_control {
display: inline-block;
position: relative;
left: 20%
}
.ptz_txt_input {
display: inline-block;
}
.btn_setup {
display: inline-block;
position: relative;
left: 32%;
}
.direction_set {
display: inline-block;
float: left;
margin: 10px 0px 0px 0px;
}
.cam_direction_box {
display:grid;
grid-gap: 4px;
grid-template-columns: repeat(3, 30px);
grid-template-rows: repeat(3, 30px);
}
.cam_direction_box > div {
padding: 10px;
border-radius: 5px;
display: grid;
place-items: center;
font-family: sans-serif;
font-size: 24px;
font-weight: bold;
}
.cam_ptz_btn {
font-weight: bold;
}
.fields_btn {
display: inline-block;
float: left;
}
.zoom_set {
display: inline-block;
float: left;
margin: 10px 0px 0px 0px;
}
.zoom_in_out {
width: 30px;
height: 30px;
font-weight: bold;
}
.ptz_input_num {
width:50px;
height:23px;
margin-right: 20px;
position: relative;
display: inline-block;
}
.relative_movement_value {
display: inline-block;
position: relative;
left: 20%;
}
#continuous_mode_option {
height: 25px;
}
/* window scroll top/bottom */
#move_top_btn {
position: fixed;
bottom: 57px;
right: 16px;
width: 35px;
height: 35px;
background: whitesmoke;
}
#move_bottom_btn {
position: fixed;
bottom: 16px;
right: 16px;
width: 35px;
height: 35px;
background: whitesmoke;
}
/* tab css */
.btn {
padding:0;
background:transparent;
border:0;
outline:0
}
.clearfix::after {
display:block;
content:'';
clear:both
}
.tab_wrap .btn_tab {
float:left;
width:110px;
height:30px;
background:#f4f4f4;
border-radius:10px 10px 0 0;
text-align:center;
line-height:30px
}
.tab_wrap .btn_tab.act {
background:#191970;
font-weight:bold;
color: white;
}
.tab_wrap .content_area {
display:none;
width:100%;
min-height:200px;
padding:30px 10px 30px 10px;
background:#fff;
border-radius:0 0 10px 10px;
box-sizing:border-box
}
.tab_wrap .content_area.act {
display:block
}
.tab_wrap *[data-depth="1"] {
background:#f4f4f4
}

BIN
src/static/images/clear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

55
src/static/js/api.js Normal file
View File

@@ -0,0 +1,55 @@
/*
@File: api.js
@Date: 2026-02-23
@brief: PTZ API only (no AI service request needed)
*/
//PTZ API URL
let api_cam_ptz_info_url
let api_cam_ptz_continuous_url
let api_cam_ptz_absolute_url
let api_cam_ptz_relative_url
let api_cam_ptz_zoom_url
let api_cam_ptz_stop_url
//post data func
function postData(url = '', data = {}, body) {
return fetch(url, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
redirect: 'follow',
referrer: 'no-referrer',
body: JSON.stringify(body),
})
.then((response) => {
if(response.status == 422) {
throw new Error('Unprocessable Entity(Code: 422)')
}
return response.json()
})
.catch(error => console.log(error))
}
//get data func
function getData(url = '', data = {}) {
return fetch(url, {
method: 'GET',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
redirect: 'follow',
referrer: 'no-referrer',
})
.then((response) => {
return response.json()
})
.catch(error => console.log(error))
}

29
src/static/js/config.js Normal file
View File

@@ -0,0 +1,29 @@
//DEFAULT SERVER INFO - 기본값 비움
const DEFAULT_MQTT_WEB_IP = ""
const DEFAULT_MQTT_WEB_PORT = ""
const DEFAULT_ENGINE_SERVER_IP = ""
const DEFAULT_ENGINE_SERVER_PORT = ""
const DEFAULT_MQTT_USER_ID = ""
const DEFAULT_MQTT_USER_PW = ""
//SERVER PRESET - FERMAT만 유지
const FERMAT = {
"SERVICE_NAME": "FERMAT",
"MQTT_WEB_IP": "192.168.200.216",
"MQTT_WEB_PORT": 50274,
"MQTT_USER_ID": "admin",
"MQTT_USER_PW": "12341234",
"ENGINE_SERVER_IP": "192.168.200.216",
"ENGINE_SERVER_PORT": 50770
}
//MQTT TOPIC
const FALL_TOPIC = '/hospital/ai1'
//PTZ API URL
const API_CAM_PTZ_INFO = "/api/services/CAM/PTZ/Info"
const API_CAM_PTZ_CONTINUOUS = "/api/services/CAM/PTZ/Continuous"
const API_CAM_PTZ_ABSOLUTE = "/api/services/CAM/PTZ/Absolute"
const API_CAM_PTZ_RELATIVE = "/api/services/CAM/PTZ/Relative"
const API_CAM_PTZ_ZOOM = "/api/services/CAM/PTZ/Zoom"
const API_CAM_PTZ_STOP = "/api/services/CAM/PTZ/Stop"

44
src/static/js/const.js Normal file
View File

@@ -0,0 +1,44 @@
//DEFAULT box line
const DEFAULT_MSG_BOX_LINE = "100"
//arrow img path
const DONW_ARROW_IMG_PATH = "static/images/down_arrow.png"
const UP_ARROW_IMG_PATH = "static/images/up_arrow.png"
//전역변수 - 설정 영역
const MSG_BOX_LINE = $('#line_num')
const MQTT_SERVER_ADDRESS = $('#mqtt_server_address')
const MQTT_USER_ID = $('#mqtt_user_id')
const MQTT_USER_PW = $('#mqtt_user_pw')
const ENGINE_SERVER_ADDRESS = $('#engine_server_address')
const SERVER_CONNECT = $('#server_connect')
const APPLIED_SERVER_TXT = $('#applied_server_txt')
//전역변수 - 낙상 탐지 모니터
const FALL_MSG_CONTAINER = $('#fall_msg_container')
const FALL_CLEAR = $('#fall_clear')
const FALL_OBJECTS_IMAGES = $('#fall_objects_images')
const FALL_SNAPSHOT_BTN = $('#fall_snapshot_btn')
const FALL_SNAPSHOT_IMG = $('#fall_snapshot_img')
//전역변수 - 공통
const MSG_CONTAINER = $('.msg_container')
const SNAPSHOT_BTN = $('.snapshot_btn')
const SNAPSHOT_IMG = $('.snapshot_img')
//전역변수 - PTZ
const CAM_VALUE_P = $('#cam_value_p')
const CAM_VALUE_T = $('#cam_value_t')
const CAM_VALUE_Z = $('#cam_value_z')
const RELATIVE_MODE_P = $('#relative_movement_p')
const RELATIVE_MODE_T = $('#relative_movement_t')
const RELATIVE_MODE_Z = $('#relative_movement_z')
const CONTINUOUS_MODE_OPTION = $('#continuous_mode_option')
const CONTINUOUS_MODE_TIME = $('#continuous_mode_time')
//전역변수 - UI
const MOVE_TOP_BTN = $('#move_top_btn')
const MOVE_BOTTOM_BTN = $('#move_bottom_btn')
const STATUS_BAR = $('#status_bar')

View File

@@ -0,0 +1,58 @@
/*
@File: init_page.js
@Date: 2026-02-23
@brief: Page initialization
*/
bindingTabEvent('.tab_wrap');
init()
function init(){
//기본값 비움 - localStorage에 값이 없으면 빈값 유지
if(localStorage.getItem('line_num')==null) {
localStorage.setItem('line_num', DEFAULT_MSG_BOX_LINE)
}
let ls_line_num = parseInt(localStorage.getItem('line_num'),10)
let ls_mqtt_server_address = localStorage.getItem('mqtt_server_address') || ''
let ls_mqtt_server_id = localStorage.getItem('mqtt_user_id') || ''
let ls_engine_server_address = localStorage.getItem('engine_server_address') || ''
MSG_BOX_LINE.val(ls_line_num);
MQTT_SERVER_ADDRESS.val(ls_mqtt_server_address);
MQTT_USER_ID.val(ls_mqtt_server_id);
ENGINE_SERVER_ADDRESS.val(ls_engine_server_address);
}
function findParent(el, className){
let check = el.parentNode.classList.contains(className);
if(check === true){
return el.parentNode;
}else{
return findParent(el.parentNode, className);
}
}
function bindingTabEvent(wrap){
let wrapEl = document.querySelectorAll(wrap);
wrapEl.forEach(function(tabArea){
let btn = tabArea.querySelectorAll('.btn_tab');
btn.forEach(function(item){
item.addEventListener('click', function(){
let parent = findParent(this, 'tab_area');
let idx = this.dataset['idx'];
let depth = this.dataset['depth'];
let btnArr = parent.querySelectorAll('.btn_tab[data-depth="'+ depth +'"]');
let contentArr = parent.querySelectorAll('.content_area[data-depth="'+ depth +'"]');
btnArr.forEach(function(btn){ btn.classList.remove('act'); });
this.classList.add('act');
contentArr.forEach(function(content){ content.classList.remove('act'); });
parent.querySelector('.content_area[data-idx="'+ idx +'"][data-depth="'+ depth +'"]').classList.add('act');
});
});
});
}

View File

@@ -0,0 +1,183 @@
/**
* jQuery json-viewer
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
* @link: https://github.com/abodelot/jquery.json-viewer
*/
(function($) {
/**
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
* @return boolean
*/
function isCollapsable(arg) {
return arg instanceof Object && Object.keys(arg).length > 0;
}
/**
* Check if a string looks like a URL, based on protocol
* This doesn't attempt to validate URLs, there's no use and syntax can be too complex
* @return boolean
*/
function isUrl(string) {
var protocols = ['http', 'https', 'ftp', 'ftps'];
for (var i = 0; i < protocols.length; ++i) {
if (string.startsWith(protocols[i] + '://')) {
return true;
}
}
return false;
}
/**
* Return the input string html escaped
* @return string
*/
function htmlEscape(s) {
return s.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&apos;')
.replace(/"/g, '&quot;');
}
/**
* Transform a json object into html representation
* @return string
*/
function json2html(json, options) {
var html = '';
if (typeof json === 'string') {
// Escape tags and quotes
json = htmlEscape(json);
if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}
} else if (typeof json === 'number' || typeof json === 'bigint') {
html += '<span class="json-literal">' + json + '</span>';
} else if (typeof json === 'boolean') {
html += '<span class="json-literal">' + json + '</span>';
} else if (json === null) {
html += '<span class="json-literal">null</span>';
} else if (json instanceof Array) {
if (json.length > 0) {
html += '[<ol class="json-array">';
for (var i = 0; i < json.length; ++i) {
html += '<li>';
// Add toggle button if item is collapsable
if (isCollapsable(json[i])) {
html += '<a href class="json-toggle"></a>';
}
html += json2html(json[i], options);
// Add comma if item is not last
if (i < json.length - 1) {
html += ',';
}
html += '</li>';
}
html += '</ol>]';
} else {
html += '[]';
}
} else if (typeof json === 'object') {
// Optional support different libraries for big numbers
// json.isLosslessNumber: package lossless-json
// json.toExponential(): packages bignumber.js, big.js, decimal.js, decimal.js-light, others?
if (options.bigNumbers && (typeof json.toExponential === 'function' || json.isLosslessNumber)) {
html += '<span class="json-literal">' + json.toString() + '</span>';
} else {
var keyCount = Object.keys(json).length;
if (keyCount > 0) {
html += '<ul class="json-dict">';
for (var key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
// define a parameter of the json value first to prevent get null from key when the key changed by the function `htmlEscape(key)`
let jsonElement = json[key];
key = htmlEscape(key);
var keyRepr = options.withQuotes ?
'<span class="json-string">"' + key + '"</span>' : key;
html += '<li>';
// Add toggle button if item is collapsable
if (isCollapsable(jsonElement)) {
html += '<a href class="json-toggle">' + keyRepr + '</a>';
} else {
html += keyRepr;
}
html += ': ' + json2html(jsonElement, options);
// Add comma if item is not last
if (--keyCount > 0) {
html += ',';
}
html += '</li>';
}
}
html += '</ul>';
} else {
html += '{}';
}
}
}
return html;
}
/**
* jQuery plugin method
* @param json: a javascript object
* @param options: an optional options hash
*/
$.fn.jsonViewer = function(json, options) {
// Merge user options with default options
options = Object.assign({}, {
collapsed: false,
rootCollapsable: false,
withQuotes: false,
withLinks: true,
bigNumbers: false
}, options);
// jQuery chaining
return this.each(function() {
// Transform to HTML
var html = json2html(json, options);
if (options.rootCollapsable && isCollapsable(json)) {
html = '<a href class="json-toggle"></a>' + html;
}
// Insert HTML in target DOM element
$(this).html(html);
$(this).addClass('json-document');
// Bind click on toggle buttons
$(this).off('click');
$(this).on('click', 'a.json-toggle', function() {
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
target.toggle();
if (target.is(':visible')) {
target.siblings('.json-placeholder').remove();
} else {
var count = target.children('li').length;
var placeholder = count + (count > 1 ? ' items' : ' item');
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
}
return false;
});
// Simulate click on toggle button when placeholder is clicked
$(this).on('click', 'a.json-placeholder', function() {
$(this).siblings('a.json-toggle').click();
return false;
});
if (options.collapsed == true) {
// Trigger click to collapse all nodes
$(this).find('a.json-toggle').click();
}
});
};
})(jQuery);

4
src/static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

72
src/static/js/mqttws31-min.js vendored Normal file
View File

@@ -0,0 +1,72 @@
/*******************************************************************************
* Copyright (c) 2013, 2014 IBM Corp.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
*******************************************************************************/
"undefined"===typeof Paho&&(Paho={});
Paho.MQTT=function(u){function y(a,b,c){b[c++]=a>>8;b[c++]=a%256;return c}function r(a,b,c,h){h=y(b,c,h);F(a,c,h);return h+b}function m(a){for(var b=0,c=0;c<a.length;c++){var h=a.charCodeAt(c);2047<h?(55296<=h&&56319>=h&&(c++,b++),b+=3):127<h?b+=2:b++}return b}function F(a,b,c){for(var h=0;h<a.length;h++){var e=a.charCodeAt(h);if(55296<=e&&56319>=e){var d=a.charCodeAt(++h);if(isNaN(d))throw Error(f(g.MALFORMED_UNICODE,[e,d]));e=(e-55296<<10)+(d-56320)+65536}127>=e?b[c++]=e:(2047>=e?b[c++]=e>>6&31|
192:(65535>=e?b[c++]=e>>12&15|224:(b[c++]=e>>18&7|240,b[c++]=e>>12&63|128),b[c++]=e>>6&63|128),b[c++]=e&63|128)}return b}function G(a,b,c){for(var h="",e,d=b;d<b+c;){e=a[d++];if(!(128>e)){var p=a[d++]-128;if(0>p)throw Error(f(g.MALFORMED_UTF,[e.toString(16),p.toString(16),""]));if(224>e)e=64*(e-192)+p;else{var t=a[d++]-128;if(0>t)throw Error(f(g.MALFORMED_UTF,[e.toString(16),p.toString(16),t.toString(16)]));if(240>e)e=4096*(e-224)+64*p+t;else{var l=a[d++]-128;if(0>l)throw Error(f(g.MALFORMED_UTF,
[e.toString(16),p.toString(16),t.toString(16),l.toString(16)]));if(248>e)e=262144*(e-240)+4096*p+64*t+l;else throw Error(f(g.MALFORMED_UTF,[e.toString(16),p.toString(16),t.toString(16),l.toString(16)]));}}}65535<e&&(e-=65536,h+=String.fromCharCode(55296+(e>>10)),e=56320+(e&1023));h+=String.fromCharCode(e)}return h}var A=function(a,b){for(var c in a)if(a.hasOwnProperty(c))if(b.hasOwnProperty(c)){if(typeof a[c]!==b[c])throw Error(f(g.INVALID_TYPE,[typeof a[c],c]));}else{var h="Unknown property, "+c+
". Valid properties are:";for(c in b)b.hasOwnProperty(c)&&(h=h+" "+c);throw Error(h);}},q=function(a,b){return function(){return a.apply(b,arguments)}},g={OK:{code:0,text:"AMQJSC0000I OK."},CONNECT_TIMEOUT:{code:1,text:"AMQJSC0001E Connect timed out."},SUBSCRIBE_TIMEOUT:{code:2,text:"AMQJS0002E Subscribe timed out."},UNSUBSCRIBE_TIMEOUT:{code:3,text:"AMQJS0003E Unsubscribe timed out."},PING_TIMEOUT:{code:4,text:"AMQJS0004E Ping timed out."},INTERNAL_ERROR:{code:5,text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"},
CONNACK_RETURNCODE:{code:6,text:"AMQJS0006E Bad Connack return code:{0} {1}."},SOCKET_ERROR:{code:7,text:"AMQJS0007E Socket error:{0}."},SOCKET_CLOSE:{code:8,text:"AMQJS0008I Socket closed."},MALFORMED_UTF:{code:9,text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."},UNSUPPORTED:{code:10,text:"AMQJS0010E {0} is not supported by this browser."},INVALID_STATE:{code:11,text:"AMQJS0011E Invalid state {0}."},INVALID_TYPE:{code:12,text:"AMQJS0012E Invalid type {0} for {1}."},INVALID_ARGUMENT:{code:13,text:"AMQJS0013E Invalid argument {0} for {1}."},
UNSUPPORTED_OPERATION:{code:14,text:"AMQJS0014E Unsupported operation."},INVALID_STORED_DATA:{code:15,text:"AMQJS0015E Invalid data in local storage key={0} value={1}."},INVALID_MQTT_MESSAGE_TYPE:{code:16,text:"AMQJS0016E Invalid MQTT message type {0}."},MALFORMED_UNICODE:{code:17,text:"AMQJS0017E Malformed Unicode string:{0} {1}."}},J={0:"Connection Accepted",1:"Connection Refused: unacceptable protocol version",2:"Connection Refused: identifier rejected",3:"Connection Refused: server unavailable",
4:"Connection Refused: bad user name or password",5:"Connection Refused: not authorized"},f=function(a,b){var c=a.text;if(b)for(var h,e,d=0;d<b.length;d++)if(h="{"+d+"}",e=c.indexOf(h),0<e)var g=c.substring(0,e),c=c.substring(e+h.length),c=g+b[d]+c;return c},B=[0,6,77,81,73,115,100,112,3],C=[0,4,77,81,84,84,4],n=function(a,b){this.type=a;for(var c in b)b.hasOwnProperty(c)&&(this[c]=b[c])};n.prototype.encode=function(){var a=(this.type&15)<<4,b=0,c=[],h=0;void 0!=this.messageIdentifier&&(b+=2);switch(this.type){case 1:switch(this.mqttVersion){case 3:b+=
B.length+3;break;case 4:b+=C.length+3}b+=m(this.clientId)+2;if(void 0!=this.willMessage){var b=b+(m(this.willMessage.destinationName)+2),e=this.willMessage.payloadBytes;e instanceof Uint8Array||(e=new Uint8Array(g));b+=e.byteLength+2}void 0!=this.userName&&(b+=m(this.userName)+2);void 0!=this.password&&(b+=m(this.password)+2);break;case 8:for(var a=a|2,d=0;d<this.topics.length;d++)c[d]=m(this.topics[d]),b+=c[d]+2;b+=this.requestedQos.length;break;case 10:a|=2;for(d=0;d<this.topics.length;d++)c[d]=
m(this.topics[d]),b+=c[d]+2;break;case 6:a|=2;break;case 3:this.payloadMessage.duplicate&&(a|=8);a=a|=this.payloadMessage.qos<<1;this.payloadMessage.retained&&(a|=1);var h=m(this.payloadMessage.destinationName),g=this.payloadMessage.payloadBytes,b=b+(h+2)+g.byteLength;g instanceof ArrayBuffer?g=new Uint8Array(g):g instanceof Uint8Array||(g=new Uint8Array(g.buffer))}var f=b,d=Array(1),l=0;do{var z=f%128,f=f>>7;0<f&&(z|=128);d[l++]=z}while(0<f&&4>l);f=d.length+1;b=new ArrayBuffer(b+f);l=new Uint8Array(b);
l[0]=a;l.set(d,1);if(3==this.type)f=r(this.payloadMessage.destinationName,h,l,f);else if(1==this.type){switch(this.mqttVersion){case 3:l.set(B,f);f+=B.length;break;case 4:l.set(C,f),f+=C.length}a=0;this.cleanSession&&(a=2);void 0!=this.willMessage&&(a=a|4|this.willMessage.qos<<3,this.willMessage.retained&&(a|=32));void 0!=this.userName&&(a|=128);void 0!=this.password&&(a|=64);l[f++]=a;f=y(this.keepAliveInterval,l,f)}void 0!=this.messageIdentifier&&(f=y(this.messageIdentifier,l,f));switch(this.type){case 1:f=
r(this.clientId,m(this.clientId),l,f);void 0!=this.willMessage&&(f=r(this.willMessage.destinationName,m(this.willMessage.destinationName),l,f),f=y(e.byteLength,l,f),l.set(e,f),f+=e.byteLength);void 0!=this.userName&&(f=r(this.userName,m(this.userName),l,f));void 0!=this.password&&r(this.password,m(this.password),l,f);break;case 3:l.set(g,f);break;case 8:for(d=0;d<this.topics.length;d++)f=r(this.topics[d],c[d],l,f),l[f++]=this.requestedQos[d];break;case 10:for(d=0;d<this.topics.length;d++)f=r(this.topics[d],
c[d],l,f)}return b};var H=function(a,b,c){this._client=a;this._window=b;this._keepAliveInterval=1E3*c;this.isReset=!1;var h=(new n(12)).encode(),e=function(a){return function(){return d.apply(a)}},d=function(){this.isReset?(this.isReset=!1,this._client._trace("Pinger.doPing","send PINGREQ"),this._client.socket.send(h),this.timeout=this._window.setTimeout(e(this),this._keepAliveInterval)):(this._client._trace("Pinger.doPing","Timed out"),this._client._disconnected(g.PING_TIMEOUT.code,f(g.PING_TIMEOUT)))};
this.reset=function(){this.isReset=!0;this._window.clearTimeout(this.timeout);0<this._keepAliveInterval&&(this.timeout=setTimeout(e(this),this._keepAliveInterval))};this.cancel=function(){this._window.clearTimeout(this.timeout)}},D=function(a,b,c,f,e){this._window=b;c||(c=30);this.timeout=setTimeout(function(a,b,c){return function(){return a.apply(b,c)}}(f,a,e),1E3*c);this.cancel=function(){this._window.clearTimeout(this.timeout)}},k=function(a,b,c,h,e){if(!("WebSocket"in u&&null!==u.WebSocket))throw Error(f(g.UNSUPPORTED,
["WebSocket"]));if(!("localStorage"in u&&null!==u.localStorage))throw Error(f(g.UNSUPPORTED,["localStorage"]));if(!("ArrayBuffer"in u&&null!==u.ArrayBuffer))throw Error(f(g.UNSUPPORTED,["ArrayBuffer"]));this._trace("Paho.MQTT.Client",a,b,c,h,e);this.host=b;this.port=c;this.path=h;this.uri=a;this.clientId=e;this._localKey=b+":"+c+("/mqtt"!=h?":"+h:"")+":"+e+":";this._msg_queue=[];this._sentMessages={};this._receivedMessages={};this._notify_msg_sent={};this._message_identifier=1;this._sequence=0;for(var d in localStorage)0!=
d.indexOf("Sent:"+this._localKey)&&0!=d.indexOf("Received:"+this._localKey)||this.restore(d)};k.prototype.host;k.prototype.port;k.prototype.path;k.prototype.uri;k.prototype.clientId;k.prototype.socket;k.prototype.connected=!1;k.prototype.maxMessageIdentifier=65536;k.prototype.connectOptions;k.prototype.hostIndex;k.prototype.onConnectionLost;k.prototype.onMessageDelivered;k.prototype.onMessageArrived;k.prototype.traceFunction;k.prototype._msg_queue=null;k.prototype._connectTimeout;k.prototype.sendPinger=
null;k.prototype.receivePinger=null;k.prototype.receiveBuffer=null;k.prototype._traceBuffer=null;k.prototype._MAX_TRACE_ENTRIES=100;k.prototype.connect=function(a){var b=this._traceMask(a,"password");this._trace("Client.connect",b,this.socket,this.connected);if(this.connected)throw Error(f(g.INVALID_STATE,["already connected"]));if(this.socket)throw Error(f(g.INVALID_STATE,["already connected"]));this.connectOptions=a;a.uris?(this.hostIndex=0,this._doConnect(a.uris[0])):this._doConnect(this.uri)};
k.prototype.subscribe=function(a,b){this._trace("Client.subscribe",a,b);if(!this.connected)throw Error(f(g.INVALID_STATE,["not connected"]));var c=new n(8);c.topics=[a];c.requestedQos=void 0!=b.qos?[b.qos]:[0];b.onSuccess&&(c.onSuccess=function(a){b.onSuccess({invocationContext:b.invocationContext,grantedQos:a})});b.onFailure&&(c.onFailure=function(a){b.onFailure({invocationContext:b.invocationContext,errorCode:a})});b.timeout&&(c.timeOut=new D(this,window,b.timeout,b.onFailure,[{invocationContext:b.invocationContext,
errorCode:g.SUBSCRIBE_TIMEOUT.code,errorMessage:f(g.SUBSCRIBE_TIMEOUT)}]));this._requires_ack(c);this._schedule_message(c)};k.prototype.unsubscribe=function(a,b){this._trace("Client.unsubscribe",a,b);if(!this.connected)throw Error(f(g.INVALID_STATE,["not connected"]));var c=new n(10);c.topics=[a];b.onSuccess&&(c.callback=function(){b.onSuccess({invocationContext:b.invocationContext})});b.timeout&&(c.timeOut=new D(this,window,b.timeout,b.onFailure,[{invocationContext:b.invocationContext,errorCode:g.UNSUBSCRIBE_TIMEOUT.code,
errorMessage:f(g.UNSUBSCRIBE_TIMEOUT)}]));this._requires_ack(c);this._schedule_message(c)};k.prototype.send=function(a){this._trace("Client.send",a);if(!this.connected)throw Error(f(g.INVALID_STATE,["not connected"]));wireMessage=new n(3);wireMessage.payloadMessage=a;0<a.qos?this._requires_ack(wireMessage):this.onMessageDelivered&&(this._notify_msg_sent[wireMessage]=this.onMessageDelivered(wireMessage.payloadMessage));this._schedule_message(wireMessage)};k.prototype.disconnect=function(){this._trace("Client.disconnect");
if(!this.socket)throw Error(f(g.INVALID_STATE,["not connecting or connected"]));wireMessage=new n(14);this._notify_msg_sent[wireMessage]=q(this._disconnected,this);this._schedule_message(wireMessage)};k.prototype.getTraceLog=function(){if(null!==this._traceBuffer){this._trace("Client.getTraceLog",new Date);this._trace("Client.getTraceLog in flight messages",this._sentMessages.length);for(var a in this._sentMessages)this._trace("_sentMessages ",a,this._sentMessages[a]);for(a in this._receivedMessages)this._trace("_receivedMessages ",
a,this._receivedMessages[a]);return this._traceBuffer}};k.prototype.startTrace=function(){null===this._traceBuffer&&(this._traceBuffer=[]);this._trace("Client.startTrace",new Date,"@VERSION@")};k.prototype.stopTrace=function(){delete this._traceBuffer};k.prototype._doConnect=function(a){this.connectOptions.useSSL&&(a=a.split(":"),a[0]="wss",a=a.join(":"));this.connected=!1;this.socket=4>this.connectOptions.mqttVersion?new WebSocket(a,["mqttv3.1"]):new WebSocket(a,["mqtt"]);this.socket.binaryType=
"arraybuffer";this.socket.onopen=q(this._on_socket_open,this);this.socket.onmessage=q(this._on_socket_message,this);this.socket.onerror=q(this._on_socket_error,this);this.socket.onclose=q(this._on_socket_close,this);this.sendPinger=new H(this,window,this.connectOptions.keepAliveInterval);this.receivePinger=new H(this,window,this.connectOptions.keepAliveInterval);this._connectTimeout=new D(this,window,this.connectOptions.timeout,this._disconnected,[g.CONNECT_TIMEOUT.code,f(g.CONNECT_TIMEOUT)])};k.prototype._schedule_message=
function(a){this._msg_queue.push(a);this.connected&&this._process_queue()};k.prototype.store=function(a,b){var c={type:b.type,messageIdentifier:b.messageIdentifier,version:1};switch(b.type){case 3:b.pubRecReceived&&(c.pubRecReceived=!0);c.payloadMessage={};for(var h="",e=b.payloadMessage.payloadBytes,d=0;d<e.length;d++)h=15>=e[d]?h+"0"+e[d].toString(16):h+e[d].toString(16);c.payloadMessage.payloadHex=h;c.payloadMessage.qos=b.payloadMessage.qos;c.payloadMessage.destinationName=b.payloadMessage.destinationName;
b.payloadMessage.duplicate&&(c.payloadMessage.duplicate=!0);b.payloadMessage.retained&&(c.payloadMessage.retained=!0);0==a.indexOf("Sent:")&&(void 0===b.sequence&&(b.sequence=++this._sequence),c.sequence=b.sequence);break;default:throw Error(f(g.INVALID_STORED_DATA,[key,c]));}localStorage.setItem(a+this._localKey+b.messageIdentifier,JSON.stringify(c))};k.prototype.restore=function(a){var b=localStorage.getItem(a),c=JSON.parse(b),h=new n(c.type,c);switch(c.type){case 3:for(var b=c.payloadMessage.payloadHex,
e=new ArrayBuffer(b.length/2),e=new Uint8Array(e),d=0;2<=b.length;){var k=parseInt(b.substring(0,2),16),b=b.substring(2,b.length);e[d++]=k}b=new Paho.MQTT.Message(e);b.qos=c.payloadMessage.qos;b.destinationName=c.payloadMessage.destinationName;c.payloadMessage.duplicate&&(b.duplicate=!0);c.payloadMessage.retained&&(b.retained=!0);h.payloadMessage=b;break;default:throw Error(f(g.INVALID_STORED_DATA,[a,b]));}0==a.indexOf("Sent:"+this._localKey)?(h.payloadMessage.duplicate=!0,this._sentMessages[h.messageIdentifier]=
h):0==a.indexOf("Received:"+this._localKey)&&(this._receivedMessages[h.messageIdentifier]=h)};k.prototype._process_queue=function(){for(var a=null,b=this._msg_queue.reverse();a=b.pop();)this._socket_send(a),this._notify_msg_sent[a]&&(this._notify_msg_sent[a](),delete this._notify_msg_sent[a])};k.prototype._requires_ack=function(a){var b=Object.keys(this._sentMessages).length;if(b>this.maxMessageIdentifier)throw Error("Too many messages:"+b);for(;void 0!==this._sentMessages[this._message_identifier];)this._message_identifier++;
a.messageIdentifier=this._message_identifier;this._sentMessages[a.messageIdentifier]=a;3===a.type&&this.store("Sent:",a);this._message_identifier===this.maxMessageIdentifier&&(this._message_identifier=1)};k.prototype._on_socket_open=function(){var a=new n(1,this.connectOptions);a.clientId=this.clientId;this._socket_send(a)};k.prototype._on_socket_message=function(a){this._trace("Client._on_socket_message",a.data);this.receivePinger.reset();a=this._deframeMessages(a.data);for(var b=0;b<a.length;b+=
1)this._handleMessage(a[b])};k.prototype._deframeMessages=function(a){a=new Uint8Array(a);if(this.receiveBuffer){var b=new Uint8Array(this.receiveBuffer.length+a.length);b.set(this.receiveBuffer);b.set(a,this.receiveBuffer.length);a=b;delete this.receiveBuffer}try{for(var b=0,c=[];b<a.length;){var h;a:{var e=a,d=b,k=d,t=e[d],l=t>>4,z=t&15,d=d+1,v=void 0,E=0,m=1;do{if(d==e.length){h=[null,k];break a}v=e[d++];E+=(v&127)*m;m*=128}while(0!=(v&128));v=d+E;if(v>e.length)h=[null,k];else{var w=new n(l);switch(l){case 2:e[d++]&
1&&(w.sessionPresent=!0);w.returnCode=e[d++];break;case 3:var k=z>>1&3,r=256*e[d]+e[d+1],d=d+2,u=G(e,d,r),d=d+r;0<k&&(w.messageIdentifier=256*e[d]+e[d+1],d+=2);var q=new Paho.MQTT.Message(e.subarray(d,v));1==(z&1)&&(q.retained=!0);8==(z&8)&&(q.duplicate=!0);q.qos=k;q.destinationName=u;w.payloadMessage=q;break;case 4:case 5:case 6:case 7:case 11:w.messageIdentifier=256*e[d]+e[d+1];break;case 9:w.messageIdentifier=256*e[d]+e[d+1],d+=2,w.returnCode=e.subarray(d,v)}h=[w,v]}}var x=h[0],b=h[1];if(null!==
x)c.push(x);else break}b<a.length&&(this.receiveBuffer=a.subarray(b))}catch(y){this._disconnected(g.INTERNAL_ERROR.code,f(g.INTERNAL_ERROR,[y.message,y.stack.toString()]));return}return c};k.prototype._handleMessage=function(a){this._trace("Client._handleMessage",a);try{switch(a.type){case 2:this._connectTimeout.cancel();if(this.connectOptions.cleanSession){for(var b in this._sentMessages){var c=this._sentMessages[b];localStorage.removeItem("Sent:"+this._localKey+c.messageIdentifier)}this._sentMessages=
{};for(b in this._receivedMessages){var h=this._receivedMessages[b];localStorage.removeItem("Received:"+this._localKey+h.messageIdentifier)}this._receivedMessages={}}if(0===a.returnCode)this.connected=!0,this.connectOptions.uris&&(this.hostIndex=this.connectOptions.uris.length);else{this._disconnected(g.CONNACK_RETURNCODE.code,f(g.CONNACK_RETURNCODE,[a.returnCode,J[a.returnCode]]));break}a=[];for(var e in this._sentMessages)this._sentMessages.hasOwnProperty(e)&&a.push(this._sentMessages[e]);a=a.sort(function(a,
b){return a.sequence-b.sequence});e=0;for(var d=a.length;e<d;e++)if(c=a[e],3==c.type&&c.pubRecReceived){var k=new n(6,{messageIdentifier:c.messageIdentifier});this._schedule_message(k)}else this._schedule_message(c);if(this.connectOptions.onSuccess)this.connectOptions.onSuccess({invocationContext:this.connectOptions.invocationContext});this._process_queue();break;case 3:this._receivePublish(a);break;case 4:if(c=this._sentMessages[a.messageIdentifier])if(delete this._sentMessages[a.messageIdentifier],
localStorage.removeItem("Sent:"+this._localKey+a.messageIdentifier),this.onMessageDelivered)this.onMessageDelivered(c.payloadMessage);break;case 5:if(c=this._sentMessages[a.messageIdentifier])c.pubRecReceived=!0,k=new n(6,{messageIdentifier:a.messageIdentifier}),this.store("Sent:",c),this._schedule_message(k);break;case 6:h=this._receivedMessages[a.messageIdentifier];localStorage.removeItem("Received:"+this._localKey+a.messageIdentifier);h&&(this._receiveMessage(h),delete this._receivedMessages[a.messageIdentifier]);
var m=new n(7,{messageIdentifier:a.messageIdentifier});this._schedule_message(m);break;case 7:c=this._sentMessages[a.messageIdentifier];delete this._sentMessages[a.messageIdentifier];localStorage.removeItem("Sent:"+this._localKey+a.messageIdentifier);if(this.onMessageDelivered)this.onMessageDelivered(c.payloadMessage);break;case 9:if(c=this._sentMessages[a.messageIdentifier]){c.timeOut&&c.timeOut.cancel();a.returnCode.indexOf=Array.prototype.indexOf;if(-1!==a.returnCode.indexOf(128)){if(c.onFailure)c.onFailure(a.returnCode)}else if(c.onSuccess)c.onSuccess(a.returnCode);
delete this._sentMessages[a.messageIdentifier]}break;case 11:if(c=this._sentMessages[a.messageIdentifier])c.timeOut&&c.timeOut.cancel(),c.callback&&c.callback(),delete this._sentMessages[a.messageIdentifier];break;case 13:this.sendPinger.reset();break;case 14:this._disconnected(g.INVALID_MQTT_MESSAGE_TYPE.code,f(g.INVALID_MQTT_MESSAGE_TYPE,[a.type]));break;default:this._disconnected(g.INVALID_MQTT_MESSAGE_TYPE.code,f(g.INVALID_MQTT_MESSAGE_TYPE,[a.type]))}}catch(l){this._disconnected(g.INTERNAL_ERROR.code,
f(g.INTERNAL_ERROR,[l.message,l.stack.toString()]))}};k.prototype._on_socket_error=function(a){this._disconnected(g.SOCKET_ERROR.code,f(g.SOCKET_ERROR,[a.data]))};k.prototype._on_socket_close=function(){this._disconnected(g.SOCKET_CLOSE.code,f(g.SOCKET_CLOSE))};k.prototype._socket_send=function(a){if(1==a.type){var b=this._traceMask(a,"password");this._trace("Client._socket_send",b)}else this._trace("Client._socket_send",a);this.socket.send(a.encode());this.sendPinger.reset()};k.prototype._receivePublish=
function(a){switch(a.payloadMessage.qos){case "undefined":case 0:this._receiveMessage(a);break;case 1:var b=new n(4,{messageIdentifier:a.messageIdentifier});this._schedule_message(b);this._receiveMessage(a);break;case 2:this._receivedMessages[a.messageIdentifier]=a;this.store("Received:",a);a=new n(5,{messageIdentifier:a.messageIdentifier});this._schedule_message(a);break;default:throw Error("Invaild qos="+wireMmessage.payloadMessage.qos);}};k.prototype._receiveMessage=function(a){if(this.onMessageArrived)this.onMessageArrived(a.payloadMessage)};
k.prototype._disconnected=function(a,b){this._trace("Client._disconnected",a,b);this.sendPinger.cancel();this.receivePinger.cancel();this._connectTimeout&&this._connectTimeout.cancel();this._msg_queue=[];this._notify_msg_sent={};this.socket&&(this.socket.onopen=null,this.socket.onmessage=null,this.socket.onerror=null,this.socket.onclose=null,1===this.socket.readyState&&this.socket.close(),delete this.socket);if(this.connectOptions.uris&&this.hostIndex<this.connectOptions.uris.length-1)this.hostIndex++,
this._doConnect(this.connectOptions.uris[this.hostIndex]);else if(void 0===a&&(a=g.OK.code,b=f(g.OK)),this.connected){if(this.connected=!1,this.onConnectionLost)this.onConnectionLost({errorCode:a,errorMessage:b})}else if(4===this.connectOptions.mqttVersion&&!1===this.connectOptions.mqttVersionExplicit)this._trace("Failed to connect V4, dropping back to V3"),this.connectOptions.mqttVersion=3,this.connectOptions.uris?(this.hostIndex=0,this._doConnect(this.connectOptions.uris[0])):this._doConnect(this.uri);
else if(this.connectOptions.onFailure)this.connectOptions.onFailure({invocationContext:this.connectOptions.invocationContext,errorCode:a,errorMessage:b})};k.prototype._trace=function(){if(this.traceFunction){for(var a in arguments)"undefined"!==typeof arguments[a]&&(arguments[a]=JSON.stringify(arguments[a]));a=Array.prototype.slice.call(arguments).join("");this.traceFunction({severity:"Debug",message:a})}if(null!==this._traceBuffer){a=0;for(var b=arguments.length;a<b;a++)this._traceBuffer.length==
this._MAX_TRACE_ENTRIES&&this._traceBuffer.shift(),0===a?this._traceBuffer.push(arguments[a]):"undefined"===typeof arguments[a]?this._traceBuffer.push(arguments[a]):this._traceBuffer.push(" "+JSON.stringify(arguments[a]))}};k.prototype._traceMask=function(a,b){var c={},f;for(f in a)a.hasOwnProperty(f)&&(c[f]=f==b?"******":a[f]);return c};var I=function(a,b,c,h){var e;if("string"!==typeof a)throw Error(f(g.INVALID_TYPE,[typeof a,"host"]));if(2==arguments.length){h=b;e=a;var d=e.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/);
if(d)a=d[4]||d[2],b=parseInt(d[7]),c=d[8];else throw Error(f(g.INVALID_ARGUMENT,[a,"host"]));}else{3==arguments.length&&(h=c,c="/mqtt");if("number"!==typeof b||0>b)throw Error(f(g.INVALID_TYPE,[typeof b,"port"]));if("string"!==typeof c)throw Error(f(g.INVALID_TYPE,[typeof c,"path"]));e="ws://"+(-1!=a.indexOf(":")&&"["!=a.slice(0,1)&&"]"!=a.slice(-1)?"["+a+"]":a)+":"+b+c}for(var p=d=0;p<h.length;p++){var m=h.charCodeAt(p);55296<=m&&56319>=m&&p++;d++}if("string"!==typeof h||65535<d)throw Error(f(g.INVALID_ARGUMENT,
[h,"clientId"]));var l=new k(e,a,b,c,h);this._getHost=function(){return a};this._setHost=function(){throw Error(f(g.UNSUPPORTED_OPERATION));};this._getPort=function(){return b};this._setPort=function(){throw Error(f(g.UNSUPPORTED_OPERATION));};this._getPath=function(){return c};this._setPath=function(){throw Error(f(g.UNSUPPORTED_OPERATION));};this._getURI=function(){return e};this._setURI=function(){throw Error(f(g.UNSUPPORTED_OPERATION));};this._getClientId=function(){return l.clientId};this._setClientId=
function(){throw Error(f(g.UNSUPPORTED_OPERATION));};this._getOnConnectionLost=function(){return l.onConnectionLost};this._setOnConnectionLost=function(a){if("function"===typeof a)l.onConnectionLost=a;else throw Error(f(g.INVALID_TYPE,[typeof a,"onConnectionLost"]));};this._getOnMessageDelivered=function(){return l.onMessageDelivered};this._setOnMessageDelivered=function(a){if("function"===typeof a)l.onMessageDelivered=a;else throw Error(f(g.INVALID_TYPE,[typeof a,"onMessageDelivered"]));};this._getOnMessageArrived=
function(){return l.onMessageArrived};this._setOnMessageArrived=function(a){if("function"===typeof a)l.onMessageArrived=a;else throw Error(f(g.INVALID_TYPE,[typeof a,"onMessageArrived"]));};this._getTrace=function(){return l.traceFunction};this._setTrace=function(a){if("function"===typeof a)l.traceFunction=a;else throw Error(f(g.INVALID_TYPE,[typeof a,"onTrace"]));};this.connect=function(a){a=a||{};A(a,{timeout:"number",userName:"string",password:"string",willMessage:"object",keepAliveInterval:"number",
cleanSession:"boolean",useSSL:"boolean",invocationContext:"object",onSuccess:"function",onFailure:"function",hosts:"object",ports:"object",mqttVersion:"number"});void 0===a.keepAliveInterval&&(a.keepAliveInterval=60);if(4<a.mqttVersion||3>a.mqttVersion)throw Error(f(g.INVALID_ARGUMENT,[a.mqttVersion,"connectOptions.mqttVersion"]));void 0===a.mqttVersion?(a.mqttVersionExplicit=!1,a.mqttVersion=4):a.mqttVersionExplicit=!0;if(void 0===a.password&&void 0!==a.userName)throw Error(f(g.INVALID_ARGUMENT,
[a.password,"connectOptions.password"]));if(a.willMessage){if(!(a.willMessage instanceof x))throw Error(f(g.INVALID_TYPE,[a.willMessage,"connectOptions.willMessage"]));a.willMessage.stringPayload;if("undefined"===typeof a.willMessage.destinationName)throw Error(f(g.INVALID_TYPE,[typeof a.willMessage.destinationName,"connectOptions.willMessage.destinationName"]));}"undefined"===typeof a.cleanSession&&(a.cleanSession=!0);if(a.hosts){if(!(a.hosts instanceof Array))throw Error(f(g.INVALID_ARGUMENT,[a.hosts,
"connectOptions.hosts"]));if(1>a.hosts.length)throw Error(f(g.INVALID_ARGUMENT,[a.hosts,"connectOptions.hosts"]));for(var b=!1,d=0;d<a.hosts.length;d++){if("string"!==typeof a.hosts[d])throw Error(f(g.INVALID_TYPE,[typeof a.hosts[d],"connectOptions.hosts["+d+"]"]));if(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/.test(a.hosts[d]))if(0==d)b=!0;else{if(!b)throw Error(f(g.INVALID_ARGUMENT,[a.hosts[d],"connectOptions.hosts["+d+"]"]));}else if(b)throw Error(f(g.INVALID_ARGUMENT,[a.hosts[d],"connectOptions.hosts["+
d+"]"]));}if(b)a.uris=a.hosts;else{if(!a.ports)throw Error(f(g.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));if(!(a.ports instanceof Array))throw Error(f(g.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));if(a.hosts.length!=a.ports.length)throw Error(f(g.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));a.uris=[];for(d=0;d<a.hosts.length;d++){if("number"!==typeof a.ports[d]||0>a.ports[d])throw Error(f(g.INVALID_TYPE,[typeof a.ports[d],"connectOptions.ports["+d+"]"]));var b=a.hosts[d],h=
a.ports[d];e="ws://"+(-1!=b.indexOf(":")?"["+b+"]":b)+":"+h+c;a.uris.push(e)}}}l.connect(a)};this.subscribe=function(a,b){if("string"!==typeof a)throw Error("Invalid argument:"+a);b=b||{};A(b,{qos:"number",invocationContext:"object",onSuccess:"function",onFailure:"function",timeout:"number"});if(b.timeout&&!b.onFailure)throw Error("subscribeOptions.timeout specified with no onFailure callback.");if("undefined"!==typeof b.qos&&0!==b.qos&&1!==b.qos&&2!==b.qos)throw Error(f(g.INVALID_ARGUMENT,[b.qos,
"subscribeOptions.qos"]));l.subscribe(a,b)};this.unsubscribe=function(a,b){if("string"!==typeof a)throw Error("Invalid argument:"+a);b=b||{};A(b,{invocationContext:"object",onSuccess:"function",onFailure:"function",timeout:"number"});if(b.timeout&&!b.onFailure)throw Error("unsubscribeOptions.timeout specified with no onFailure callback.");l.unsubscribe(a,b)};this.send=function(a,b,c,d){var e;if(0==arguments.length)throw Error("Invalid argument.length");if(1==arguments.length){if(!(a instanceof x)&&
"string"!==typeof a)throw Error("Invalid argument:"+typeof a);e=a;if("undefined"===typeof e.destinationName)throw Error(f(g.INVALID_ARGUMENT,[e.destinationName,"Message.destinationName"]));}else e=new x(b),e.destinationName=a,3<=arguments.length&&(e.qos=c),4<=arguments.length&&(e.retained=d);l.send(e)};this.disconnect=function(){l.disconnect()};this.getTraceLog=function(){return l.getTraceLog()};this.startTrace=function(){l.startTrace()};this.stopTrace=function(){l.stopTrace()};this.isConnected=function(){return l.connected}};
I.prototype={get host(){return this._getHost()},set host(a){this._setHost(a)},get port(){return this._getPort()},set port(a){this._setPort(a)},get path(){return this._getPath()},set path(a){this._setPath(a)},get clientId(){return this._getClientId()},set clientId(a){this._setClientId(a)},get onConnectionLost(){return this._getOnConnectionLost()},set onConnectionLost(a){this._setOnConnectionLost(a)},get onMessageDelivered(){return this._getOnMessageDelivered()},set onMessageDelivered(a){this._setOnMessageDelivered(a)},
get onMessageArrived(){return this._getOnMessageArrived()},set onMessageArrived(a){this._setOnMessageArrived(a)},get trace(){return this._getTrace()},set trace(a){this._setTrace(a)}};var x=function(a){var b;if("string"===typeof a||a instanceof ArrayBuffer||a instanceof Int8Array||a instanceof Uint8Array||a instanceof Int16Array||a instanceof Uint16Array||a instanceof Int32Array||a instanceof Uint32Array||a instanceof Float32Array||a instanceof Float64Array)b=a;else throw f(g.INVALID_ARGUMENT,[a,"newPayload"]);
this._getPayloadString=function(){return"string"===typeof b?b:G(b,0,b.length)};this._getPayloadBytes=function(){if("string"===typeof b){var a=new ArrayBuffer(m(b)),a=new Uint8Array(a);F(b,a,0);return a}return b};var c=void 0;this._getDestinationName=function(){return c};this._setDestinationName=function(a){if("string"===typeof a)c=a;else throw Error(f(g.INVALID_ARGUMENT,[a,"newDestinationName"]));};var h=0;this._getQos=function(){return h};this._setQos=function(a){if(0===a||1===a||2===a)h=a;else throw Error("Invalid argument:"+
a);};var e=!1;this._getRetained=function(){return e};this._setRetained=function(a){if("boolean"===typeof a)e=a;else throw Error(f(g.INVALID_ARGUMENT,[a,"newRetained"]));};var d=!1;this._getDuplicate=function(){return d};this._setDuplicate=function(a){d=a}};x.prototype={get payloadString(){return this._getPayloadString()},get payloadBytes(){return this._getPayloadBytes()},get destinationName(){return this._getDestinationName()},set destinationName(a){this._setDestinationName(a)},get qos(){return this._getQos()},
set qos(a){this._setQos(a)},get retained(){return this._getRetained()},set retained(a){this._setRetained(a)},get duplicate(){return this._getDuplicate()},set duplicate(a){this._setDuplicate(a)}};return{Client:I,Message:x}}(window);

2143
src/static/js/mqttws31.js Normal file

File diff suppressed because it is too large Load Diff

136
src/static/js/ptz.js Normal file
View File

@@ -0,0 +1,136 @@
/*
@File: ptz.js
@Date: 2026-02-23
@brief: PTZ Camera Control
*/
//get ptz cam info(current angle)
function get_cam_current() {
return getData(api_cam_ptz_info_url, {answer: 42})
.then(data => {
CAM_VALUE_P.val(Number(data.info.current_angle.pan))
CAM_VALUE_T.val(Number(data.info.current_angle.tilt))
CAM_VALUE_Z.val(Number(data.info.current_angle.zoom))
})
.catch(error => console.error(error));
}
//request ptz cam absolute mode
function cam_ptz_apply_btn() {
try {
let ptz_absolute_request_body = {
"axis": {
"pan": Number(CAM_VALUE_P.val()),
"tilt": Number(CAM_VALUE_T.val()),
"zoom": Number(CAM_VALUE_Z.val())
}
}
postData(api_cam_ptz_absolute_url, {answer: 42}, ptz_absolute_request_body)
.then(data => JSON.stringify(data))
.catch(error => alert(error));
}
catch (error) {
alert(`Error: ${error.name}`)
}
}
//request ptz cam relative mode(tilt+)
function cam_t_plus_btn() {
try {
let relative_mode = { "axis": { "tilt": Number(RELATIVE_MODE_T.val()) } }
postData(api_cam_ptz_relative_url, {answer: 42}, relative_mode)
.then(data => { if(data.error !== null) alert(data.error) })
.catch(error => alert(error));
}
catch (error) { alert(`Error: ${error.name}`) }
}
//request ptz cam relative mode(tilt-)
function cam_t_minus_btn() {
try {
let relative_mode = { "axis": { "tilt": Number(RELATIVE_MODE_T.val()) * -1 } }
postData(api_cam_ptz_relative_url, {answer: 42}, relative_mode)
.then(data => { if(data.error !== null) alert(data.error) })
.catch(error => alert(error));
}
catch (error) { alert(`Error: ${error.name}`) }
}
//request ptz cam relative mode(pan+)
function cam_p_plus_btn() {
try {
let relative_mode = { "axis": { "pan": Number(RELATIVE_MODE_P.val()) } }
postData(api_cam_ptz_relative_url, {answer: 42}, relative_mode)
.then(data => { if(data.error !== null) alert(data.error) })
.catch(error => alert(error));
}
catch (error) { alert(`Error: ${error.name}`) }
}
//request ptz cam relative mode(pan-)
function cam_p_minus_btn() {
try {
let relative_mode = { "axis": { "pan": Number(RELATIVE_MODE_P.val()) * -1 } }
postData(api_cam_ptz_relative_url, {answer: 42}, relative_mode)
.then(data => { if(data.error !== null) alert(data.error) })
.catch(error => alert(error));
}
catch (error) { alert(`Error: ${error.name}`) }
}
//request ptz cam relative mode(center)
function cam_center_btn() {
return getData(api_cam_ptz_info_url, {answer: 42})
.then(data => {
let ptz_absolute_request_body = {
"axis": {
"pan": data.info.init.pan,
"tilt": data.info.init.tilt,
"zoom": data.info.init.zoom
}
}
postData(api_cam_ptz_absolute_url, {answer: 42}, ptz_absolute_request_body)
.then(data => JSON.stringify(data))
.catch(error => alert(error));
})
}
//request ptz cam zoom in
function cam_zoom_in_btn() {
return getData(api_cam_ptz_info_url, {answer: 42})
.then(data => {
let zoom_mode = { "zoom": data.info.current_angle.zoom + Number(RELATIVE_MODE_Z.val()) }
postData(api_cam_ptz_zoom_url, {answer: 42}, zoom_mode)
})
.catch(error => console.error(error));
}
//request ptz cam zoom out
function cam_zoom_out_btn() {
return getData(api_cam_ptz_info_url, {answer: 42})
.then(data => {
let zoom_mode = { "zoom": data.info.current_angle.zoom - Number(RELATIVE_MODE_Z.val()) }
postData(api_cam_ptz_zoom_url, {answer: 42}, zoom_mode)
})
.catch(error => console.error(error));
}
//request ptz cam continuous mode
function continuous_apply_btn() {
try {
let continuous_mode = {
"mode": CONTINUOUS_MODE_OPTION.val(),
"time": Number(CONTINUOUS_MODE_TIME.val())
}
postData(api_cam_ptz_continuous_url, {answer: 42}, continuous_mode)
.then(data => { if(data.error !== null) alert(data.error) })
.catch(error => alert(error));
}
catch (error) { alert(`Error: ${error.name}`) }
}

View File

@@ -0,0 +1,353 @@
/*
@File: web_rabbitmq.js
@Date: 2026-02-23
@brief: Hospital Fall Detection MQTT Handler
*/
//default set
let max_line
let flag = 1
//snapshot btn
let fall_snap_flag = 1
//line apply btn
function line_apply_btn() {
localStorage.setItem('line_num', MSG_BOX_LINE.val())
max_line = MSG_BOX_LINE.val()
}
//connect, disconnect btn
function connect_btn() {
let conn_mqtt_client = slice_server_port(MQTT_SERVER_ADDRESS.val())
set_api_url(ENGINE_SERVER_ADDRESS.val())
localStorage.setItem('mqtt_server_address', MQTT_SERVER_ADDRESS.val())
localStorage.setItem('engine_server_address', ENGINE_SERVER_ADDRESS.val())
if(flag==1) {
mqtt_client = new Paho.MQTT.Client(
conn_mqtt_client[0],
conn_mqtt_client[1],
"/ws",
`myclientid_${parseInt(Math.random() * 100, 10)}`
);
let options = {
timeout: 3,
keepAliveInterval: 30,
onSuccess: mqtt_on_connect,
onFailure: mqtt_on_failure,
userName: MQTT_USER_ID.val(),
password: MQTT_USER_PW.val()
};
if (location.protocol == "https:") {
options.useSSL = true;
}
console.log(`CONNECT TO ${conn_mqtt_client[0]}:${conn_mqtt_client[1]}`);
mqtt_client.connect(options);
mqtt_client.onMessageArrived = mqtt_on_message_arrived
mqtt_client.onConnectionLost = mqtt_on_connection_lost
flag=0
SERVER_CONNECT.html("DISCONNECT")
}
else {
mqtt_disconnect()
}
}
//server preset btn - FERMAT only
function fermat_btn() {
if(flag == 0) {
mqtt_disconnect()
all_clear(FALL_MSG_CONTAINER, FALL_OBJECTS_IMAGES, FALL_SNAPSHOT_IMG, FALL_SNAPSHOT_BTN)
}
APPLIED_SERVER_TXT.text(FERMAT.SERVICE_NAME)
MQTT_SERVER_ADDRESS.val(`${FERMAT.MQTT_WEB_IP}:${FERMAT.MQTT_WEB_PORT}`)
MQTT_USER_ID.val(FERMAT.MQTT_USER_ID)
MQTT_USER_PW.val(FERMAT.MQTT_USER_PW)
ENGINE_SERVER_ADDRESS.val(`${FERMAT.ENGINE_SERVER_IP}:${FERMAT.ENGINE_SERVER_PORT}`)
}
function set_api_url(server_address) {
//ptz api url
api_cam_ptz_info_url = `http://${server_address}${API_CAM_PTZ_INFO}`
api_cam_ptz_continuous_url = `http://${server_address}${API_CAM_PTZ_CONTINUOUS}`
api_cam_ptz_absolute_url = `http://${server_address}${API_CAM_PTZ_ABSOLUTE}`
api_cam_ptz_relative_url = `http://${server_address}${API_CAM_PTZ_RELATIVE}`
api_cam_ptz_zoom_url = `http://${server_address}${API_CAM_PTZ_ZOOM}`
api_cam_ptz_stop_url = `http://${server_address}${API_CAM_PTZ_STOP}`
}
function mqtt_on_connect() {
console.log("CONNECTION SUCCESS");
mqtt_client.subscribe(FALL_TOPIC, {qos: 1})
set_attr_disabled(MQTT_SERVER_ADDRESS, true)
set_attr_disabled(MQTT_USER_ID, true)
set_attr_disabled(MQTT_USER_PW, true)
set_attr_disabled(ENGINE_SERVER_ADDRESS, true)
con_status_bar("connect")
}
function mqtt_on_failure(responseObject) {
console.log(`CONNECTION FAILURE - ${responseObject.errorMessage}`);
SERVER_CONNECT.html("CONNECT")
flag=1
set_attr_disabled(MQTT_SERVER_ADDRESS, false)
set_attr_disabled(MQTT_USER_ID, false)
set_attr_disabled(MQTT_USER_PW, false)
set_attr_disabled(ENGINE_SERVER_ADDRESS, false)
con_status_bar("failure")
}
function mqtt_on_connection_lost(responseObject) {
console.log(`CONNECTION LOST - ${responseObject.errorMessage}`);
SERVER_CONNECT.html("CONNECT")
flag=1
set_attr_disabled(MQTT_SERVER_ADDRESS, false)
set_attr_disabled(MQTT_USER_ID, false)
set_attr_disabled(MQTT_USER_PW, false)
set_attr_disabled(ENGINE_SERVER_ADDRESS, false)
con_status_bar("lost")
}
function mqtt_disconnect() {
mqtt_client.disconnect();
SERVER_CONNECT.html("CONNECT")
flag=1
all_clear(FALL_MSG_CONTAINER, FALL_OBJECTS_IMAGES, FALL_SNAPSHOT_IMG, FALL_SNAPSHOT_BTN)
set_attr_disabled(MQTT_SERVER_ADDRESS, false)
set_attr_disabled(MQTT_USER_ID, false)
set_attr_disabled(MQTT_USER_PW, false)
set_attr_disabled(ENGINE_SERVER_ADDRESS, false)
con_status_bar()
}
//MQTT 메시지 수신 - 조건 없이 무조건 표시
function mqtt_on_message_arrived(message) {
if(message.destinationName == FALL_TOPIC) {
try {
let msg = JSON.parse(message.payloadString)
let msg_copy = JSON.parse(message.payloadString)
//1. 토글 메시지 (JSON 원본) 표시
create_toggle_msg(FALL_MSG_CONTAINER, msg_copy)
set_container_scrollbar(FALL_MSG_CONTAINER)
//2. objects 배열 순회 - 사람별 이미지 표시
remove_child(FALL_OBJECTS_IMAGES[0])
if(msg.objects && msg.objects.length > 0) {
for(let i = 0; i < msg.objects.length; i++) {
let obj = msg.objects[i]
create_object_div(obj, FALL_OBJECTS_IMAGES)
}
}
//3. 전체 프레임 스냅샷 표시
if(msg.visual_data && msg.visual_data.has_image && msg.visual_data.base64_str) {
set_attr_disabled(FALL_SNAPSHOT_BTN, false)
remove_child(FALL_SNAPSHOT_IMG[0])
get_base64_image(msg.visual_data.base64_str, msg.visual_data.format, FALL_SNAPSHOT_IMG)
}
} catch (error) {
console.log(error.message)
}
}
}
//object별 이미지 div 생성
function create_object_div(obj, container) {
let obj_div = $('<div>').attr('class', 'object_div').appendTo(container)
//object 정보 표시
let info_div = $('<div>').attr('class', 'object_info').appendTo(obj_div)
$('<div>').attr('class', 'object_info_item').text(`tracking_id: ${obj.tracking_id}`).appendTo(info_div)
$('<div>').attr('class', 'object_info_item').text(`status: ${obj.status}`).appendTo(info_div)
if(obj.status_detail) {
$('<div>').attr('class', 'object_info_item').text(`detail: ${obj.status_detail}`).appendTo(info_div)
}
if(obj.severity) {
let severity_div = $('<div>').attr('class', `object_info_item severity_${obj.severity.toLowerCase()}`).text(`severity: ${obj.severity}`).appendTo(info_div)
}
//object 이미지 표시
if(obj.visual_data && obj.visual_data.has_image && obj.visual_data.base64_str) {
let img_container = $('<div>').attr('class', 'object_img_container').appendTo(obj_div)
get_base64_image(obj.visual_data.base64_str, obj.visual_data.format, img_container)
}
}
//base64 이미지 표시
function get_base64_image(base64_str, format, container) {
try {
let img_format = format || 'jpg'
let base64img = `data:image/${img_format};base64,${base64_str}`
let img = new Image()
img.onload = function() {
img.style.width = "100%"
img.style.height = "100%"
img.style.objectFit = "contain"
container.append(img)
}
img.src = base64img
} catch (error) {
console.log(error.message)
}
}
//server address -> server, port 분리
function slice_server_port(address) {
let split_address = address.split(":")
let server_ip = split_address[0]
let server_port = parseInt(split_address[1])
return [server_ip, server_port]
}
//remove child elements
function remove_child(parent) {
while(parent.hasChildNodes()) {
parent.removeChild(parent.firstChild)
}
}
//set container scroll bar to bottom & limit line
function set_container_scrollbar(container) {
container.scrollTop(container[0].scrollHeight)
$(document).on('change', container, function() {
this.scrollTop = this.scrollHeight;
let msg_line = container.children().length
let msg_line_count = 0;
for(let i = 0; i < msg_line; i++) {
if(msg_line > 0) {
msg_line_count++;
}
}
if (msg_line_count > max_line) {
container.find('ul:first').remove();
}
}).find(container).change();
}
//toggle message 생성
function create_toggle_msg(container, items) {
let msg_timestamp = items.header ? items.header.timestamp : ''
let toggler
let toggle_ul = $('<ul>').attr('class', 'toggle_msg_ul').appendTo(container)
let toggle_li = $('<li>').appendTo(toggle_ul)
let summary_text = ''
if(items.summary) {
summary_text = ` alerts: ${items.summary.active_alerts_count}, objects: ${items.summary.total_objects_count}`
}
toggler = $('<span>').attr('class','caret2').appendTo(toggle_li).text(`timestamp: '${msg_timestamp}'${summary_text}`)
let toggle_ul_2 = $('<ul>').attr('class','nested2').appendTo(toggle_li)
let toggle_li_2 = $('<li>').appendTo(toggle_ul_2)
toggle_li_2.jsonViewer(items)
for (var i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function() {
this.parentElement.querySelector(".nested2").classList.toggle("active");
this.classList.toggle("caret2-down");
});
}
}
function set_snapshot_up(btn, img) {
btn.attr('src', UP_ARROW_IMG_PATH)
img.show()
}
function set_snapshot_down(btn, img) {
btn.attr('src', DONW_ARROW_IMG_PATH)
img.hide()
}
function set_attr_disabled(btn, bool) {
btn.attr("disabled", bool)
}
function all_clear(msg_container, img, snapshot_img, snapshot_btn) {
msg_container.empty();
img.empty();
snapshot_img.empty()
set_snapshot_down(snapshot_btn, snapshot_img)
set_attr_disabled(snapshot_btn, true)
}
/* message clear btn */
FALL_CLEAR.click(function(){
all_clear(FALL_MSG_CONTAINER, FALL_OBJECTS_IMAGES, FALL_SNAPSHOT_IMG, FALL_SNAPSHOT_BTN)
})
/* snapshot btn */
FALL_SNAPSHOT_BTN.click(function(){
if(fall_snap_flag == 1) {
set_snapshot_up(FALL_SNAPSHOT_BTN, FALL_SNAPSHOT_IMG)
fall_snap_flag = 0
}
else {
set_snapshot_down(FALL_SNAPSHOT_BTN, FALL_SNAPSHOT_IMG)
fall_snap_flag = 1
}
})
/* window scroll */
MOVE_TOP_BTN.click(function(){
window.scrollTo({ top: 0, behavior: "smooth" });
})
MOVE_BOTTOM_BTN.click(function(){
window.scrollTo({ top: $('body').prop('scrollHeight'), behavior: "smooth" });
})
//connection status bar
function con_status_bar(status) {
if(status == "connect") {
STATUS_BAR.css({
"backgroundColor": "lightgreen",
"height": "25px",
"margin": "0px 0px 10px 0px",
"textAlign": "center"})
.text(`Connected to ${MQTT_SERVER_ADDRESS.val()}`)
}
else if(status == "failure") {
STATUS_BAR.css({
"backgroundColor": "red",
"height": "25px",
"margin": "0px 0px 10px 0px",
"textAlign": "center"})
.text(`Failed to connect to ${MQTT_SERVER_ADDRESS.val()}`)
}
else if(status == "lost") {
STATUS_BAR.css({
"backgroundColor": "red",
"height": "25px",
"margin": "0px 0px 10px 0px",
"textAlign": "center"})
.text(`Lost connection to ${MQTT_SERVER_ADDRESS.val()}`)
}
else {
STATUS_BAR.css({
"backgroundColor": "red",
"height": "25px",
"margin": "0px 0px 10px 0px",
"textAlign": "center"})
.text(`Disconnected to ${MQTT_SERVER_ADDRESS.val()}`)
}
}

189
src/templates/index.html Normal file
View File

@@ -0,0 +1,189 @@
<!---
@File: index.html
@Date: 2026-02-23
@brief: Hospital CCTV Fall Detection Monitor
-->
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>Hospital Fall Monitor</title>
<script src="static/js/jquery.min.js"></script>
<script src="static/js/mqttws31-min.js"></script>
<script src="static/js/mqttws31.js"></script>
<script src="static/js/config.js"></script>
<script src="static/js/jquery.json-viewer.js"></script>
<link rel="stylesheet" href="static/css/style.css" type="text/css">
<link rel="stylesheet" href="static/css/jquery.json-viewer.css" type="text/css">
</head>
<body>
<div id="status_bar"></div>
<div class="header">
<h>Hospital Fall Monitor - MQTT {{sw_version}}</h>
</div>
<div class="tab_wrap tab_area">
<div class="btn_area clearfix">
<button class="btn btn_tab act" data-depth="0" data-idx="1"> MONITOR </button>
<button class="btn btn_tab" data-depth="0" data-idx="2"> PTZ CAM </button>
</div>
<!-- MONITOR 탭 -->
<div class="content_area act" data-depth="0" data-idx="1">
<div class="set_info">
<div class="set_msg_line">
<div class="msg_box_line"> MESSAGE BOX LINE: </div>
<input id="line_num" type="text" class="input_num">
<div class="line_apply">
<button onclick="line_apply_btn()" id="line_apply_btn" class="apply_btn"> APPLY </button>
</div>
</div>
<div class="set_mqtt_server">
<div class="set_server">
<div class="server_address_txt"> MQTT SERVER ADDRESS: </div>
<div class="server_address_txt_input">
<input type="text" id="mqtt_server_address" class="input_text">
</div>
<div class="server_address_txt"> MQTT USER ID: </div>
<div class="server_address_txt_input">
<input type="text" id="mqtt_user_id" class="input_text">
</div>
<div class="server_address_txt"> MQTT USER PW: </div>
<div class="server_address_txt_input">
<input type="password" id="mqtt_user_pw" class="input_text">
</div>
<div class="server_address_txt"> AI ENGINE SERVER ADDRESS: </div>
<div class="server_address_txt_input">
<input type="text" id="engine_server_address" class="input_text">
</div>
<div class="server_address_btn">
<button onclick="connect_btn()" id="server_connect"> CONNECT </button>
</div>
</div>
<fieldset class="fieldset_server">
<legend> Server Preset:
<div id="applied_server_txt"></div>
</legend>
<div class="preset_btn">
<button onclick="fermat_btn()" id="fermat_btn" class="preset_btn_box"> FERMAT </button>
</div>
</fieldset>
</div>
</div>
<!-- 낙상 탐지 모니터 영역 -->
<div class="AI_message" id="FALL">
<div class="message"> CCTV Fall Detection Message
<input class="clear_btn" id="fall_clear" type="image" src="static/images/clear.png">
<div class="msg_container" id="fall_msg_container"></div>
<div class="ai_images" id="fall_objects_images"></div>
<div class="snapshot">
<div class="snapshot_set">
<div class="snapshot_name"> Full Frame Snapshot </div>
<input class="snapshot_btn" id="fall_snapshot_btn" type="image" src="static/images/down_arrow.png">
</div>
<div class="snapshot_img" id="fall_snapshot_img"></div>
</div>
</div>
</div>
</div>
<!-- PTZ CAM 탭 -->
<div class="content_area" data-depth="0" data-idx="2">
<div class="tab_area">
<div class="btn_area clearfix">
<button class="btn btn_tab act" data-depth="1" data-idx="0">Setup</button>
</div>
<div class="content_area act" data-depth="1" data-idx="0">
<P>PTZ Setup</P>
<div class="ptz_setup">
<hr>
<div class="ptz_mode">
<div class="ptz_mode_name"> Absolute Mode </div>
<div class="ptz_control">
<button onclick="get_cam_current()" id="get_cam_current">Get</button>
<div class="ptz_txt_input">
P: <input type="number" class="ptz_input_num" id="cam_value_p">
T: <input type="number" class="ptz_input_num" id="cam_value_t">
Z: <input type="number" class="ptz_input_num" id="cam_value_z">
</div>
</div>
<div class="ptz_mode_btn">
<button onclick="cam_ptz_apply_btn()" class="ptz_apply_btn" id="cam_ptz_apply_btn"> APPLY </button>
</div>
</div>
<hr>
<div class="ptz_mode">
<div class="ptz_mode_name"> Relative Mode </div>
<div class="relative_movement_value">
P: <input type="number" class="ptz_input_num" id="relative_movement_p" value="10">
T: <input type="number" class="ptz_input_num" id="relative_movement_t" value="10">
Z: <input type="number" class="ptz_input_num" id="relative_movement_z" min="0" value="0.1">
</div>
<div class="btn_setup">
<fieldset class="fields_btn">
<legend> Direction </legend>
<div class="direction_set">
<div class="cam_direction_box">
<div></div>
<button onclick="cam_t_plus_btn()" id="cam_t_plus" class="cam_ptz_btn"> T+ </button>
<div></div>
<button onclick="cam_p_plus_btn()" id="cam_p_plus" class="cam_ptz_btn"> P+ </button>
<button onclick="cam_center_btn()" id="cam_center" class="cam_ptz_btn"></button>
<button onclick="cam_p_minus_btn()" id="cam_p_minus" class="cam_ptz_btn"> P- </button>
<div></div>
<button onclick="cam_t_minus_btn()" id="cam_t_minus" class="cam_ptz_btn"> T- </button>
<div></div>
</div>
</div>
</fieldset>
<fieldset class="fields_btn">
<legend> Zoom </legend>
<div class="zoom_set">
<div class="cam_zoom_box">
<div class="zoom_in_out_set">
<button onclick="cam_zoom_in_btn()" id="cam_zoom_in" class="zoom_in_out"> + </button>
<button onclick="cam_zoom_out_btn()" id="cam_zoom_out" class="zoom_in_out"> - </button>
</div>
</div>
</div>
</fieldset>
</div>
</div>
<hr>
<div class="ptz_mode">
<div class="ptz_mode_name"> Continuous Mode </div>
<div class="ptz_mode_option">
<div>
mode: <select id="continuous_mode_option">
<option value="up">up</option>
<option value="down">down</option>
<option value="left">left</option>
<option value="right">right</option>
<option value="stop">stop</option>
</select>
</div>
<div>
time: <input type="number" class="ptz_input_num" id="continuous_mode_time" value=0>
</div>
</div>
<div class="ptz_mode_btn">
<button onclick="continuous_apply_btn()" class="ptz_apply_btn" id="continuous_apply_btn"> APPLY </button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<input id="move_top_btn" type="image" src="static/images/square_up.png">
<input id="move_bottom_btn" type="image" src="static/images/square_down.png">
<script src="static/js/const.js"></script>
<script src="static/js/web_rabbitmq.js"></script>
<script src="static/js/ptz.js"></script>
<script src="static/js/init_page.js"></script>
<script src="static/js/api.js"></script>
</body>
</html>

29
src/web_server.py Normal file
View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
"""
@File: web_server.py
@Date: 2026-02-23
@brief: Hospital CCTV Fall Detection MQTT Web Monitor
"""
import uvicorn
from fastapi import FastAPI
from fastapi import Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from common.const import SERVICE_PORT, SW_VERSION
app = FastAPI()
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "sw_version": SW_VERSION})
if __name__ == '__main__':
uvicorn.run("web_server:app", host='0.0.0.0', port=SERVICE_PORT)