Initial commit: HospitalCCTV Web Monitor
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
27
.gitignore
vendored
Normal 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
@@ -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
@@ -0,0 +1,4 @@
|
|||||||
|
# Hospital Fall Monitor Web Server
|
||||||
|
SERVICE_PORT = 50777
|
||||||
|
|
||||||
|
SW_VERSION = "0.0.1"
|
||||||
57
src/static/css/jquery.json-viewer.css
Normal 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
@@ -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
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/static/images/down_arrow.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/static/images/picture.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/static/images/square_down.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/static/images/square_up.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/static/images/up_arrow.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/static/images/window_down.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src/static/images/window_up.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
55
src/static/js/api.js
Normal 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
@@ -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
@@ -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')
|
||||||
58
src/static/js/init_page.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
183
src/static/js/jquery.json-viewer.js
Normal 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(/"/g, '\\"');
|
||||||
|
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
72
src/static/js/mqttws31-min.js
vendored
Normal 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
136
src/static/js/ptz.js
Normal 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}`) }
|
||||||
|
}
|
||||||
353
src/static/js/web_rabbitmq.js
Normal 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
@@ -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
@@ -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)
|
||||||