| @@ -7,7 +7,9 @@ | |||
| "build": "ng build", | |||
| "watch": "ng build --watch --configuration development", | |||
| "test": "ng test", | |||
| "camera-server": "node dist/lot-web-ui/server/server.js" | |||
| "camera-server": "node dist/lot-web-ui/server/server.js", | |||
| "prettier:write": "prettier --write src", | |||
| "prettier:check": "prettier --check src" | |||
| }, | |||
| "private": true, | |||
| "dependencies": { | |||
| @@ -29,8 +31,10 @@ | |||
| "express-ws": "^5.0.2", | |||
| "leaflet": "^1.9.4", | |||
| "leaflet.markercluster": "^1.5.3", | |||
| "moment": "^2.30.1", | |||
| "ngx-mqtt": "^17.0.0", | |||
| "ngx-toastr": "^17.0.2", | |||
| "prettier": "^3.3.3", | |||
| "rtsp-relay": "^1.8.0", | |||
| "rxjs": "~7.8.0", | |||
| "rxjs-websockets": "^9.0.0", | |||
| @@ -3,10 +3,12 @@ import { RouterTestingModule } from '@angular/router/testing'; | |||
| import { AppComponent } from './app.component'; | |||
| describe('AppComponent', () => { | |||
| beforeEach(() => TestBed.configureTestingModule({ | |||
| imports: [RouterTestingModule], | |||
| declarations: [AppComponent] | |||
| })); | |||
| beforeEach(() => | |||
| TestBed.configureTestingModule({ | |||
| imports: [RouterTestingModule], | |||
| declarations: [AppComponent], | |||
| }), | |||
| ); | |||
| it('should create the app', () => { | |||
| const fixture = TestBed.createComponent(AppComponent); | |||
| @@ -24,6 +26,8 @@ describe('AppComponent', () => { | |||
| const fixture = TestBed.createComponent(AppComponent); | |||
| fixture.detectChanges(); | |||
| const compiled = fixture.nativeElement as HTMLElement; | |||
| expect(compiled.querySelector('.content span')?.textContent).toContain('iot-web-ui app is running!'); | |||
| expect(compiled.querySelector('.content span')?.textContent).toContain( | |||
| 'iot-web-ui app is running!', | |||
| ); | |||
| }); | |||
| }); | |||
| @@ -1,11 +1,8 @@ | |||
| import { | |||
| Component, | |||
| OnInit, | |||
| } from '@angular/core'; | |||
| import {MqttClientService} from "./shared/services/mqtt-client.service"; | |||
| import {takeUntil} from "rxjs/operators"; | |||
| import {filter, Subject} from "rxjs"; | |||
| import {TOPIC_GETTING, TOPIC_SETTING} from "./app.constants"; | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { MqttClientService } from './shared/services/mqtt-client.service'; | |||
| import { takeUntil } from 'rxjs/operators'; | |||
| import { filter, Subject } from 'rxjs'; | |||
| import { TOPIC_GETTING, TOPIC_SETTING } from './app.constants'; | |||
| @Component({ | |||
| selector: 'app-root', | |||
| @@ -20,22 +17,33 @@ export class AppComponent implements OnInit { | |||
| ngOnInit() { | |||
| this.mqtt$.createConnection(); | |||
| this.mqtt$.status$.pipe(takeUntil(this.unsubscribeAll)).subscribe( | |||
| (isConnected: boolean) => { | |||
| this.mqtt$.status$ | |||
| .pipe(takeUntil(this.unsubscribeAll)) | |||
| .subscribe((isConnected: boolean) => { | |||
| this.isConnected = isConnected; | |||
| this.mqtt$.getSetting(); | |||
| this.mqtt$.getLog(); | |||
| this.mqtt$.sendPublish({ id: '0', type: 'get', whistletime: ''}, TOPIC_GETTING); | |||
| this.mqtt$.sendPublish({ id: '0', type: 'get', arlamtime: ''}, TOPIC_GETTING); | |||
| this.mqtt$.setting$.pipe(takeUntil(this.unsubscribeAll), filter((item) => item !== null)).subscribe((message: any) => { | |||
| if (message.id == '0' && message.whistletime) { | |||
| localStorage.setItem('whistletime', message.whistletime); | |||
| } | |||
| if (message.id == '0' && message.arlamtime) { | |||
| localStorage.setItem('alarmtime', message.arlamtime); | |||
| } | |||
| }); | |||
| }, | |||
| ); | |||
| this.mqtt$.sendPublish( | |||
| { id: '0', type: 'get', whistletime: '' }, | |||
| TOPIC_GETTING, | |||
| ); | |||
| this.mqtt$.sendPublish( | |||
| { id: '0', type: 'get', arlamtime: '' }, | |||
| TOPIC_GETTING, | |||
| ); | |||
| this.mqtt$.setting$ | |||
| .pipe( | |||
| takeUntil(this.unsubscribeAll), | |||
| filter((item) => item !== null), | |||
| ) | |||
| .subscribe((message: any) => { | |||
| if (message.id == '0' && message.whistletime) { | |||
| localStorage.setItem('whistletime', message.whistletime); | |||
| } | |||
| if (message.id == '0' && message.arlamtime) { | |||
| localStorage.setItem('alarmtime', message.arlamtime); | |||
| } | |||
| }); | |||
| }); | |||
| } | |||
| } | |||
| @@ -1,11 +1,11 @@ | |||
| import { NgModule} from '@angular/core'; | |||
| import { NgModule } from '@angular/core'; | |||
| import { BrowserModule } from '@angular/platform-browser'; | |||
| import { AppRoutingModule } from './app-routing.module'; | |||
| import { AppComponent } from './app.component'; | |||
| import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; | |||
| import {SharedModule} from "./shared/shared.module"; | |||
| import {HttpClientModule} from "@angular/common/http"; | |||
| import { SharedModule } from './shared/shared.module'; | |||
| import { HttpClientModule } from '@angular/common/http'; | |||
| import { ToastrModule } from 'ngx-toastr'; | |||
| import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt'; | |||
| export const connection: IMqttServiceOptions = { | |||
| @@ -18,12 +18,10 @@ export const connection: IMqttServiceOptions = { | |||
| clientId: 'iot-' + Date.parse(new Date().toString()), | |||
| protocol: 'ws', | |||
| connectOnCreate: false, | |||
| } | |||
| }; | |||
| @NgModule({ | |||
| declarations: [ | |||
| AppComponent, | |||
| ], | |||
| declarations: [AppComponent], | |||
| imports: [ | |||
| BrowserModule, | |||
| AppRoutingModule, | |||
| @@ -33,11 +31,11 @@ export const connection: IMqttServiceOptions = { | |||
| ToastrModule.forRoot({ | |||
| maxOpened: 1, | |||
| preventDuplicates: true, | |||
| autoDismiss: true | |||
| autoDismiss: true, | |||
| }), // ToastrModule added | |||
| MqttModule.forRoot(connection) | |||
| MqttModule.forRoot(connection), | |||
| ], | |||
| providers: [], | |||
| bootstrap: [AppComponent] | |||
| bootstrap: [AppComponent], | |||
| }) | |||
| export class AppModule { } | |||
| export class AppModule {} | |||
| @@ -4,4 +4,3 @@ | |||
| <div id="map"></div> | |||
| </div> | |||
| </div> | |||
| @@ -5,7 +5,7 @@ | |||
| width: 100%; | |||
| height: calc(100% - 7rem); | |||
| h2 { | |||
| padding: .5rem 3rem 0 ; | |||
| padding: 0.5rem 3rem 0; | |||
| text-align: center; | |||
| } | |||
| } | |||
| @@ -21,26 +21,27 @@ p { | |||
| margin: 0 0 0 16px !important; | |||
| } | |||
| .box-custom { | |||
| color: #F33152; | |||
| color: #f33152; | |||
| padding: 3px 2px; | |||
| background-color: rbg(243,49,82,0.1); | |||
| background-color: rbg(243, 49, 82, 0.1); | |||
| } | |||
| ::ng-deep.sensor-on { | |||
| width: 30px; | |||
| height: 30px; | |||
| position: relative; | |||
| background: linear-gradient(#ff0000, #C70039); | |||
| background: linear-gradient(#ff0000, #c70039); | |||
| display: flex !important; | |||
| justify-content: center; | |||
| align-items: center; | |||
| border-radius: 50%; | |||
| img{ | |||
| img { | |||
| z-index: 9; | |||
| } | |||
| &:before, &:after { | |||
| &:before, | |||
| &:after { | |||
| position: absolute; | |||
| content: ''; | |||
| content: ""; | |||
| width: 100%; | |||
| height: 100%; | |||
| background: #ff0000; | |||
| @@ -56,7 +57,7 @@ p { | |||
| animation: sensor-on 2s 1s ease-out infinite; | |||
| } | |||
| } | |||
| .sensor-off{ | |||
| .sensor-off { | |||
| display: inline-block; | |||
| } | |||
| @keyframes sensor-on { | |||
| @@ -69,7 +70,7 @@ p { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 5px; | |||
| .dynamic-button{ | |||
| .dynamic-button { | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| @@ -8,7 +8,7 @@ describe('CentralizedSecurityManagementComponent', () => { | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [CentralizedSecurityManagementComponent] | |||
| declarations: [CentralizedSecurityManagementComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(CentralizedSecurityManagementComponent); | |||
| component = fixture.componentInstance; | |||
| @@ -7,21 +7,22 @@ import { | |||
| } from '@angular/core'; | |||
| import * as L from 'leaflet'; | |||
| import 'leaflet.markercluster'; | |||
| import {filter, Subject, Subscription, take} from 'rxjs'; | |||
| import { filter, Subject, Subscription, take } from 'rxjs'; | |||
| import { SocketService } from '../../../shared/services/socket.service'; | |||
| import { MatDialog } from '@angular/material/dialog'; | |||
| import { AlarmSoundService } from '../../../shared/services/alarm-sound.service'; | |||
| import { CameraDialogComponent } from '../../../shared/component/camera-dialog/camera-dialog.component'; | |||
| import { alarmData, alarmDemo } from '../data/fake-data'; | |||
| import {takeUntil} from "rxjs/operators"; | |||
| import {MqttClientService} from "../../../shared/services/mqtt-client.service"; | |||
| import { takeUntil } from 'rxjs/operators'; | |||
| import { MqttClientService } from '../../../shared/services/mqtt-client.service'; | |||
| @Component({ | |||
| selector: 'app-centralized-security-management', | |||
| templateUrl: './centralized-security-management.component.html', | |||
| styleUrls: ['./centralized-security-management.component.scss'], | |||
| }) | |||
| export class CentralizedSecurityManagementComponent implements AfterViewInit, OnDestroy | |||
| export class CentralizedSecurityManagementComponent | |||
| implements AfterViewInit, OnDestroy | |||
| { | |||
| private map!: L.Map; | |||
| private markers!: L.MarkerClusterGroup; | |||
| @@ -39,7 +40,7 @@ export class CentralizedSecurityManagementComponent implements AfterViewInit, On | |||
| private dialog: MatDialog, | |||
| private renderer: Renderer2, | |||
| private alarmSoundService$: AlarmSoundService, | |||
| private mqtt$: MqttClientService | |||
| private mqtt$: MqttClientService, | |||
| ) {} | |||
| openDialog(): void { | |||
| @@ -51,10 +52,15 @@ export class CentralizedSecurityManagementComponent implements AfterViewInit, On | |||
| ngAfterViewInit(): void { | |||
| this.initMap(); | |||
| this.mqtt$.messages$.pipe(takeUntil(this.unsubscribeAll), filter((item) => item !== null)).subscribe(message => { | |||
| console.log('map:',message); | |||
| this.onMessage(message); | |||
| }); | |||
| this.mqtt$.messages$ | |||
| .pipe( | |||
| takeUntil(this.unsubscribeAll), | |||
| filter((item) => item !== null), | |||
| ) | |||
| .subscribe((message) => { | |||
| console.log('map:', message); | |||
| this.onMessage(message); | |||
| }); | |||
| } | |||
| ngOnDestroy(): void { | |||
| @@ -91,10 +97,15 @@ export class CentralizedSecurityManagementComponent implements AfterViewInit, On | |||
| this.state2 = message.state2 == '0'; // 1 OFF // alarm 1h | |||
| this.status2 = message.status2 == '1'; // 0 not, 1 ready, 2 error, 3 bypass | |||
| this.state5 = message.state5 == '1'; // 1 ON, 0 OFF | |||
| this.alarmSoundService$.startAlarm(this.state5, this.state1, this.status1, this.state2, this.status2); | |||
| this.alarmSoundService$.startAlarm( | |||
| this.state5, | |||
| this.state1, | |||
| this.status1, | |||
| this.state2, | |||
| this.status2, | |||
| ); | |||
| this.updateIcons(); | |||
| } | |||
| } | |||
| addIconsToMap(): void { | |||
| @@ -106,7 +117,13 @@ export class CentralizedSecurityManagementComponent implements AfterViewInit, On | |||
| addMarker(item: any, isDemo: boolean = false): void { | |||
| const icon = isDemo | |||
| ? this.getIcon(this.state5, this.state1, this.status1, this.state2, this.status2) | |||
| ? this.getIcon( | |||
| this.state5, | |||
| this.state1, | |||
| this.status1, | |||
| this.state2, | |||
| this.status2, | |||
| ) | |||
| : this.createIcon(item.warning); | |||
| const marker = L.marker( | |||
| [item.detail.coordinates.lat, item.detail.coordinates.lng], | |||
| @@ -152,7 +169,11 @@ export class CentralizedSecurityManagementComponent implements AfterViewInit, On | |||
| </div>`; | |||
| } | |||
| createIcon(active: boolean, className: string = '', text: any = []): L.Icon<L.IconOptions> | L.DivIcon { | |||
| createIcon( | |||
| active: boolean, | |||
| className: string = '', | |||
| text: any = [], | |||
| ): L.Icon<L.IconOptions> | L.DivIcon { | |||
| if (text.length < 1) { | |||
| return L.icon({ | |||
| iconUrl: active | |||
| @@ -182,7 +203,13 @@ export class CentralizedSecurityManagementComponent implements AfterViewInit, On | |||
| } | |||
| } | |||
| getIcon(isTurnOn: boolean, fireArm: boolean, fireStatus: boolean, fenceArm: boolean, fenceStatus: boolean): L.Icon<L.IconOptions> | L.DivIcon { | |||
| getIcon( | |||
| isTurnOn: boolean, | |||
| fireArm: boolean, | |||
| fireStatus: boolean, | |||
| fenceArm: boolean, | |||
| fenceStatus: boolean, | |||
| ): L.Icon<L.IconOptions> | L.DivIcon { | |||
| if (isTurnOn) { | |||
| let text = []; | |||
| if (fireStatus && fireArm) { | |||
| @@ -191,8 +218,7 @@ export class CentralizedSecurityManagementComponent implements AfterViewInit, On | |||
| if (fenceStatus && fenceArm) { | |||
| text.push('FENCE ALARM'); | |||
| } | |||
| return this.createIcon(true, '', text); | |||
| return this.createIcon(true, '', text); | |||
| } | |||
| return this.createIcon(false); | |||
| } | |||
| @@ -219,3 +219,140 @@ export const atmWarningData = [ | |||
| }, | |||
| }, | |||
| ]; | |||
| export const hotelData = [ | |||
| { | |||
| floor: 1, | |||
| rooms: [ | |||
| { | |||
| roomNumber: '101', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T12:00:00', | |||
| }, | |||
| { roomNumber: '102', status: 'available' }, | |||
| { | |||
| roomNumber: '103', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T13:30:00', | |||
| }, | |||
| { roomNumber: '104', status: 'available' }, | |||
| { | |||
| roomNumber: '105', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T14:00:00', | |||
| }, | |||
| { | |||
| roomNumber: '106', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T15:00:00', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| floor: 2, | |||
| rooms: [ | |||
| { | |||
| roomNumber: '201', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T11:00:00', | |||
| }, | |||
| { roomNumber: '202', status: 'available' }, | |||
| { | |||
| roomNumber: '203', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T14:00:00', | |||
| }, | |||
| { roomNumber: '204', status: 'available' }, | |||
| { | |||
| roomNumber: '205', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T15:30:00', | |||
| }, | |||
| { | |||
| roomNumber: '206', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T16:30:00', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| floor: 3, | |||
| rooms: [ | |||
| { roomNumber: '301', status: 'available' }, | |||
| { | |||
| roomNumber: '302', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T12:45:00', | |||
| }, | |||
| { | |||
| roomNumber: '303', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T14:15:00', | |||
| }, | |||
| { roomNumber: '304', status: 'available' }, | |||
| { | |||
| roomNumber: '305', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T17:00:00', | |||
| }, | |||
| { | |||
| roomNumber: '306', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T18:00:00', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| floor: 4, | |||
| rooms: [ | |||
| { roomNumber: '401', status: 'available' }, | |||
| { | |||
| roomNumber: '402', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T10:00:00', | |||
| }, | |||
| { | |||
| roomNumber: '403', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T11:30:00', | |||
| }, | |||
| { roomNumber: '404', status: 'available' }, | |||
| { | |||
| roomNumber: '405', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T15:45:00', | |||
| }, | |||
| { | |||
| roomNumber: '406', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T16:30:00', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| floor: 5, | |||
| rooms: [ | |||
| { | |||
| roomNumber: '501', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T09:00:00', | |||
| }, | |||
| { roomNumber: '502', status: 'available' }, | |||
| { | |||
| roomNumber: '503', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T10:00:00', | |||
| }, | |||
| { | |||
| roomNumber: '504', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-11T13:00:00', | |||
| }, | |||
| { roomNumber: '505', status: 'available' }, | |||
| { | |||
| roomNumber: '506', | |||
| status: 'occupied', | |||
| checkInTime: '2024-09-12T00:30:00', | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| @@ -1,24 +1,25 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import {RouterModule} from "@angular/router"; | |||
| import {SharedMaterialModule} from "../../shared/shared-material.module"; | |||
| import { RouterModule } from '@angular/router'; | |||
| import { SharedMaterialModule } from '../../shared/shared-material.module'; | |||
| import { HomePageComponent } from './homepage/home-page.component'; | |||
| import {homePageRoutes} from "./home-page.routing"; | |||
| import {SharedModule} from "../../shared/shared.module"; | |||
| import { homePageRoutes } from './home-page.routing'; | |||
| import { SharedModule } from '../../shared/shared.module'; | |||
| import { CentralizedSecurityManagementComponent } from './centralized-security-management/centralized-security-management.component'; | |||
| import { SecuritySystemDetailsComponent } from './security-system-details/security-system-details.component'; | |||
| import {FormsModule} from "@angular/forms"; | |||
| import {SecurityAtmDetailsComponent} from "./security-atm-details/security-atm-details.component"; | |||
| import {MatTableModule} from "@angular/material/table"; | |||
| import { FormsModule } from '@angular/forms'; | |||
| import { SecurityAtmDetailsComponent } from './security-atm-details/security-atm-details.component'; | |||
| import { MatTableModule } from '@angular/material/table'; | |||
| import { HotelManagementComponent } from './hotel-management/hotel-management.component'; | |||
| @NgModule({ | |||
| declarations: [ | |||
| HomePageComponent, | |||
| CentralizedSecurityManagementComponent, | |||
| SecuritySystemDetailsComponent, | |||
| SecurityAtmDetailsComponent | |||
| SecurityAtmDetailsComponent, | |||
| HotelManagementComponent, | |||
| ], | |||
| imports: [ | |||
| CommonModule, | |||
| @@ -2,7 +2,8 @@ import { Routes } from '@angular/router'; | |||
| import { HomePageComponent } from './homepage/home-page.component'; | |||
| import { CentralizedSecurityManagementComponent } from './centralized-security-management/centralized-security-management.component'; | |||
| import { SecuritySystemDetailsComponent } from './security-system-details/security-system-details.component'; | |||
| import {SecurityAtmDetailsComponent} from "./security-atm-details/security-atm-details.component"; | |||
| import { SecurityAtmDetailsComponent } from './security-atm-details/security-atm-details.component'; | |||
| import { HotelManagementComponent } from './hotel-management/hotel-management.component'; | |||
| export const homePageRoutes: Routes = [ | |||
| { | |||
| @@ -20,5 +21,9 @@ export const homePageRoutes: Routes = [ | |||
| { | |||
| path: 'atm-monitoring', | |||
| component: SecurityAtmDetailsComponent, | |||
| } | |||
| }, | |||
| { | |||
| path: 'hotel-room-management', | |||
| component: HotelManagementComponent, | |||
| }, | |||
| ]; | |||
| @@ -3,16 +3,19 @@ | |||
| Centralized Security Management | |||
| </button> | |||
| <button mat-stroked-button routerLink="./security-system-details"> | |||
| Security System Details | |||
| Factory Security System | |||
| </button> | |||
| <button mat-stroked-button routerLink="./atm-monitoring"> | |||
| ATM monitoring | |||
| ATM Security Management | |||
| </button> | |||
| <button mat-stroked-button routerLink="./hotel-room-management"> | |||
| Hotel Room Management | |||
| </button> | |||
| </div> | |||
| <mat-card class="sound-group"> | |||
| <mat-card-header> | |||
| <mat-card-title>SIREN TIME OFF: {{whistle.time}}s</mat-card-title> | |||
| <mat-card-title>SIREN TIME OFF: {{ whistle.time }}s</mat-card-title> | |||
| </mat-card-header> | |||
| <mat-card-content> | |||
| <div class="volume-group"> | |||
| @@ -28,14 +31,14 @@ | |||
| <mat-card class="sound-group"> | |||
| <mat-card-header> | |||
| <mat-card-title>24-HOUR ZONE ALARM TIME: {{alarm.time}}s</mat-card-title> | |||
| <mat-card-title>24-HOUR ZONE ALARM TIME: {{ alarm.time }}s</mat-card-title> | |||
| </mat-card-header> | |||
| <mat-card-content> | |||
| <div class="volume-group"> | |||
| <app-slider-range | |||
| [value]="alarm.time" | |||
| [icon]="'access_alarm'" | |||
| (valueChange)="emitSetting('alarm',$event)" | |||
| (valueChange)="emitSetting('alarm', $event)" | |||
| [disable]="!isConnected" | |||
| ></app-slider-range> | |||
| </div> | |||
| @@ -1,6 +1,6 @@ | |||
| .sound-group{ | |||
| .sound-group { | |||
| margin: 2rem 3rem; | |||
| } | |||
| button{ | |||
| button { | |||
| color: #ff7723 !important; | |||
| } | |||
| @@ -8,7 +8,7 @@ describe('HomepageComponent', () => { | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [HomePageComponent] | |||
| declarations: [HomePageComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(HomePageComponent); | |||
| component = fixture.componentInstance; | |||
| @@ -1,8 +1,8 @@ | |||
| import {Component, OnDestroy, OnInit} from '@angular/core'; | |||
| import {Subject, filter} from "rxjs"; | |||
| import {MqttClientService} from "../../../shared/services/mqtt-client.service"; | |||
| import {takeUntil} from "rxjs/operators"; | |||
| import {TOPIC_GETTING, TOPIC_SETTING} from "../../../app.constants"; | |||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | |||
| import { Subject, filter } from 'rxjs'; | |||
| import { MqttClientService } from '../../../shared/services/mqtt-client.service'; | |||
| import { takeUntil } from 'rxjs/operators'; | |||
| import { TOPIC_GETTING, TOPIC_SETTING } from '../../../app.constants'; | |||
| @Component({ | |||
| selector: 'app-homepage', | |||
| @@ -21,35 +21,43 @@ export class HomePageComponent implements OnInit, OnDestroy { | |||
| isConnected = false; | |||
| private unsubscribeAll = new Subject(); | |||
| constructor( | |||
| private mqtt$: MqttClientService | |||
| ) { | |||
| this.mqtt$.status$.pipe(takeUntil(this.unsubscribeAll)).subscribe( | |||
| (isConnected: boolean) => { | |||
| constructor(private mqtt$: MqttClientService) { | |||
| this.mqtt$.status$ | |||
| .pipe(takeUntil(this.unsubscribeAll)) | |||
| .subscribe((isConnected: boolean) => { | |||
| this.isConnected = isConnected; | |||
| }, | |||
| ); | |||
| this.mqtt$.setting$.pipe(takeUntil(this.unsubscribeAll), filter((item) => item !== null)).subscribe((message: any) => { | |||
| if (message.id == '0' && message.whistletime) { | |||
| this.whistle.time = Number(message.whistletime); | |||
| } | |||
| if (message.id == '0' && message.arlamtime) { | |||
| this.alarm.time = Number(message.arlamtime); | |||
| } | |||
| }); | |||
| }); | |||
| this.mqtt$.setting$ | |||
| .pipe( | |||
| takeUntil(this.unsubscribeAll), | |||
| filter((item) => item !== null), | |||
| ) | |||
| .subscribe((message: any) => { | |||
| if (message.id == '0' && message.whistletime) { | |||
| this.whistle.time = Number(message.whistletime); | |||
| } | |||
| if (message.id == '0' && message.arlamtime) { | |||
| this.alarm.time = Number(message.arlamtime); | |||
| } | |||
| }); | |||
| } | |||
| ngOnInit() { | |||
| } | |||
| ngOnInit() {} | |||
| emitSetting(type: string, event: any) { | |||
| if (this.isConnected) { | |||
| if (type === 'whistle') { | |||
| this.whistle.time = event; | |||
| this.mqtt$.sendPublish({id: '0', type: 'set', whistletime: event.toString()}, TOPIC_GETTING); | |||
| this.mqtt$.sendPublish( | |||
| { id: '0', type: 'set', whistletime: event.toString() }, | |||
| TOPIC_GETTING, | |||
| ); | |||
| } else if (type === 'alarm') { | |||
| this.alarm.time = event; | |||
| this.mqtt$.sendPublish({id: '0', type: 'set', arlamtime: event.toString()}, TOPIC_GETTING); | |||
| this.mqtt$.sendPublish( | |||
| { id: '0', type: 'set', arlamtime: event.toString() }, | |||
| TOPIC_GETTING, | |||
| ); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| <div class="container"> | |||
| <h2>Hotel Room Management</h2> | |||
| <div class="description-room"> | |||
| <div> | |||
| <span style="background-color: lightgray"></span> | |||
| Floor | |||
| </div> | |||
| <div> | |||
| <span class="available"></span> | |||
| Available | |||
| </div> | |||
| <div> | |||
| <span class="occupied"></span> | |||
| Booked | |||
| </div> | |||
| </div> | |||
| <div class="management-container"> | |||
| <div *ngFor="let floor of data" class="floor-row"> | |||
| <div class="floor-number">{{ floor.floor }}</div> | |||
| <div class="room-grid"> | |||
| <div | |||
| class="room" | |||
| *ngFor="let room of floor.rooms" | |||
| [ngClass]="room.status" | |||
| > | |||
| <span class="time-checkin" *ngIf="room.checkInTime"> | |||
| {{ room.checkInTime | timeElapsed }} | |||
| </span> | |||
| {{ room?.roomNumber }} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,81 @@ | |||
| .container { | |||
| height: 100%; | |||
| display: flex; | |||
| flex-direction: column; | |||
| h2 { | |||
| text-align: center; | |||
| padding-top: 1rem; | |||
| } | |||
| .description-room { | |||
| display: flex; | |||
| flex-direction: row; | |||
| gap: 2rem; | |||
| padding: 1rem; | |||
| span { | |||
| display: inline-block; | |||
| width: 1rem; | |||
| height: 0.8rem; | |||
| } | |||
| } | |||
| .management-container { | |||
| height: 100%; | |||
| display: grid; | |||
| grid-template-rows: repeat(5, 1fr); | |||
| gap: 1rem; | |||
| } | |||
| } | |||
| .floor-row { | |||
| display: grid; | |||
| grid-template-columns: 5rem auto; | |||
| width: 100%; | |||
| height: 100%; | |||
| gap: 1rem; | |||
| } | |||
| .floor-number { | |||
| text-align: center; | |||
| font-weight: bold; | |||
| background-color: lightgray; | |||
| padding: 10px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| box-sizing: border-box; | |||
| } | |||
| .room-grid { | |||
| display: grid; | |||
| grid-template-columns: repeat(6, 1fr); | |||
| gap: 1rem; | |||
| width: 100%; | |||
| } | |||
| .room { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| align-items: center; | |||
| padding: 20px 0; | |||
| font-size: 2rem; | |||
| border: 1px solid black; | |||
| box-sizing: border-box; | |||
| transition: all 0.3s ease; | |||
| position: relative; | |||
| .time-checkin { | |||
| font-size: 0.75rem; | |||
| font-weight: 500; | |||
| color: green; | |||
| position: absolute; | |||
| top: 5px; | |||
| right: 5px; | |||
| } | |||
| } | |||
| .available { | |||
| background-color: #ffcdd2; | |||
| } | |||
| .occupied { | |||
| background-color: #c8e6c9; | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { HotelManagementComponent } from './hotel-management.component'; | |||
| describe('HotelManagementComponent', () => { | |||
| let component: HotelManagementComponent; | |||
| let fixture: ComponentFixture<HotelManagementComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [HotelManagementComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(HotelManagementComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,15 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { hotelData } from '../data/fake-data'; | |||
| import * as moment from 'moment'; | |||
| @Component({ | |||
| selector: 'app-hotel-management', | |||
| templateUrl: './hotel-management.component.html', | |||
| styleUrls: ['./hotel-management.component.scss'], | |||
| }) | |||
| export class HotelManagementComponent implements OnInit { | |||
| data = hotelData.reverse(); | |||
| constructor() {} | |||
| ngOnInit() {} | |||
| } | |||
| @@ -1,47 +1,88 @@ | |||
| <div class="px-3 py-5" fxLayout="row" fxLayoutAlign="space-around center" fxLayoutGap="20px"> | |||
| <div | |||
| class="px-3 py-5" | |||
| fxLayout="row" | |||
| fxLayoutAlign="space-around center" | |||
| fxLayoutGap="20px" | |||
| > | |||
| <div fxFlex="70" class="map-image"> | |||
| <div class="card-state"> | |||
| <img src="assets/images/map-quan5.jpg"> | |||
| <img src="assets/images/map-quan5.jpg" /> | |||
| <div class="state t2" id="State2"> | |||
| <div fxLayout="row"> | |||
| <div class="sensor-off" [class.sensor-on]="(state1 && state5 && status1)"> | |||
| <img [src]="(state5 && status1) ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'" | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-11')"> | |||
| <div | |||
| class="sensor-off" | |||
| [class.sensor-on]="state1 && state5 && status1" | |||
| > | |||
| <img | |||
| [src]=" | |||
| state5 && status1 | |||
| ? 'assets/images/sensor-on.png' | |||
| : 'assets/images/sensor-off.png' | |||
| " | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-11')" | |||
| /> | |||
| </div> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div *ngIf="(state1 && state5 && status1)" class="alarm-text" | |||
| [ngClass]="{'alarm-text-on': (state1 && state5 && status1) }">FIRE ALARM</div> | |||
| <div | |||
| *ngIf="state1 && state5 && status1" | |||
| class="alarm-text" | |||
| [ngClass]="{ 'alarm-text-on': state1 && state5 && status1 }" | |||
| > | |||
| FIRE ALARM | |||
| </div> | |||
| </div> | |||
| <div class="state t3" id="State3"> | |||
| <div fxLayout="row"> | |||
| <div class="sensor-off" [class.sensor-on]="(state2 && state5 && status2)"> | |||
| <img [src]="(state5 && status2) ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'" | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-9')"> | |||
| <div | |||
| class="sensor-off" | |||
| [class.sensor-on]="state2 && state5 && status2" | |||
| > | |||
| <img | |||
| [src]=" | |||
| state5 && status2 | |||
| ? 'assets/images/sensor-on.png' | |||
| : 'assets/images/sensor-off.png' | |||
| " | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-9')" | |||
| /> | |||
| </div> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div *ngIf="(state2 && state5 && status2)" class="alarm-text" | |||
| [ngClass]="{'alarm-text-on': (state2 && state5 && status2) }">VIBRATION ALARM</div> | |||
| <div | |||
| *ngIf="state2 && state5 && status2" | |||
| class="alarm-text" | |||
| [ngClass]="{ 'alarm-text-on': state2 && state5 && status2 }" | |||
| > | |||
| VIBRATION ALARM | |||
| </div> | |||
| </div> | |||
| <div class="state t4" id="State4" fxLayout="row" > | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-2')"> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| <div class="state t4" id="State4" fxLayout="row"> | |||
| <img | |||
| [src]="getImageSource()" | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-2')" | |||
| /> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div class="state t5 tooltip" id="State5" fxLayout="row"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-1')"> | |||
| <img | |||
| [src]="getImageSource()" | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-1')" | |||
| /> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div class="state t6 tooltip" id="State6" fxLayout="row"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-15')"> | |||
| <img | |||
| [src]="getImageSource()" | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| (click)="openPositionDialog($event, 'ward-15')" | |||
| /> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -49,24 +90,23 @@ | |||
| <table mat-table [dataSource]="dataSource" class="mat-elevation-z8"> | |||
| <!-- Position Column --> | |||
| <ng-container matColumnDef="title"> | |||
| <th mat-header-cell *matHeaderCellDef > | |||
| Title | |||
| </th> | |||
| <td mat-cell *matCellDef="let element"> {{element.title}} </td> | |||
| <th mat-header-cell *matHeaderCellDef>Title</th> | |||
| <td mat-cell *matCellDef="let element">{{ element.title }}</td> | |||
| </ng-container> | |||
| <ng-container matColumnDef="date"> | |||
| <th mat-header-cell *matHeaderCellDef > | |||
| Date | |||
| </th> | |||
| <td mat-cell *matCellDef="let element"> {{element.date}} </td> | |||
| <th mat-header-cell *matHeaderCellDef>Date</th> | |||
| <td mat-cell *matCellDef="let element">{{ element.date }}</td> | |||
| </ng-container> | |||
| <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | |||
| <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> | |||
| <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr> | |||
| </table> | |||
| </div> | |||
| </div> | |||
| <ng-template #camera> | |||
| <div (click)="openDialog()"> | |||
| <img style="width: 30px; height: 30px; cursor: pointer" src="assets/images/camera.png"> | |||
| <img | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| src="assets/images/camera.png" | |||
| /> | |||
| </div> | |||
| </ng-template> | |||
| @@ -8,7 +8,7 @@ describe('SecuritySystemDetailsComponent', () => { | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [SecurityAtmDetailsComponent] | |||
| declarations: [SecurityAtmDetailsComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(SecurityAtmDetailsComponent); | |||
| component = fixture.componentInstance; | |||
| @@ -1,11 +1,13 @@ | |||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | |||
| import { Subject } from 'rxjs'; | |||
| import { filter, Subject } from 'rxjs'; | |||
| import { CameraDialogComponent } from '../../../shared/component/camera-dialog/camera-dialog.component'; | |||
| import { MatDialog } from '@angular/material/dialog'; | |||
| import { AlarmSoundService } from '../../../shared/services/alarm-sound.service'; | |||
| import { MatTableDataSource } from '@angular/material/table'; | |||
| import { DetailPositionDialogComponent } from '../../../shared/component/detail-postion-dialog/detail-position-dialog.component'; | |||
| import { atmWarningData } from '../data/fake-data'; | |||
| import { MqttClientService } from '../../../shared/services/mqtt-client.service'; | |||
| import { takeUntil } from 'rxjs/operators'; | |||
| const ELEMENT_DATA: any[] = [ | |||
| { title: 'Vibration warning Phường 15', date: '26/08/2024 12:14' }, | |||
| @@ -37,31 +39,23 @@ export class SecurityAtmDetailsComponent implements OnInit, OnDestroy { | |||
| constructor( | |||
| private dialog: MatDialog, | |||
| private alarmSoundService$: AlarmSoundService, | |||
| private mqtt$: MqttClientService, | |||
| ) {} | |||
| ngOnInit() { | |||
| const fakeData = { | |||
| state5: '0', | |||
| state6: '1', | |||
| status1: '1', | |||
| state1: '0', | |||
| status2: '1', | |||
| state2: '0', | |||
| }; | |||
| this.onMessage(fakeData); | |||
| setTimeout(() => { | |||
| { | |||
| const fakeData = { | |||
| state5: '1', | |||
| state6: '1', | |||
| status1: '1', | |||
| state1: '0', | |||
| status2: '1', | |||
| state2: '0', | |||
| }; | |||
| this.onMessage(fakeData); | |||
| } | |||
| }, 3000); | |||
| this.mqtt$.status$ | |||
| .pipe(takeUntil(this.unsubscribeAll)) | |||
| .subscribe((isConnected) => { | |||
| this.isConnected = isConnected; | |||
| }); | |||
| this.mqtt$.messages$ | |||
| .pipe( | |||
| takeUntil(this.unsubscribeAll), | |||
| filter((item) => item !== null), | |||
| ) | |||
| .subscribe((message) => { | |||
| this.onMessage(message); | |||
| }); | |||
| } | |||
| ngOnDestroy(): void { | |||
| @@ -1,49 +1,93 @@ | |||
| <div class="px-3 py-5" fxLayout="row" fxLayoutAlign="space-around center" fxLayoutGap="20px"> | |||
| <div | |||
| class="px-3 py-5" | |||
| fxLayout="row" | |||
| fxLayoutAlign="space-around center" | |||
| fxLayoutGap="20px" | |||
| > | |||
| <div fxFlex="50" class="map-image"> | |||
| <div class="card-state"> | |||
| <img src="assets/images/ground.png"> | |||
| <img src="assets/images/ground.png" /> | |||
| <div class="state t2" id="State2"> | |||
| <div fxLayout="row"> | |||
| <div class="sensor-off" [class.sensor-on]="(state1 && state5 && status1)"> | |||
| <img [src]="(state5 && status1) ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'" style="width: 30px; height: 30px"> | |||
| <div | |||
| class="sensor-off" | |||
| [class.sensor-on]="state1 && state5 && status1" | |||
| > | |||
| <img | |||
| [src]=" | |||
| state5 && status1 | |||
| ? 'assets/images/sensor-on.png' | |||
| : 'assets/images/sensor-off.png' | |||
| " | |||
| style="width: 30px; height: 30px" | |||
| /> | |||
| </div> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div *ngIf="(state1 && state5 && status1)" class="alarm-text" | |||
| [ngClass]="{'alarm-text-on': (state1 && state5 && status1) }">FIRE ALARM</div> | |||
| <div | |||
| *ngIf="state1 && state5 && status1" | |||
| class="alarm-text" | |||
| [ngClass]="{ 'alarm-text-on': state1 && state5 && status1 }" | |||
| > | |||
| FIRE ALARM | |||
| </div> | |||
| </div> | |||
| <div class="state t3" id="State3"> | |||
| <div fxLayout="row"> | |||
| <div class="sensor-off" [class.sensor-on]="(state2 && state5 && status2)"> | |||
| <img [src]="(state5 && status2) ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'" style="width: 30px; height: 30px"> | |||
| <div | |||
| class="sensor-off" | |||
| [class.sensor-on]="state2 && state5 && status2" | |||
| > | |||
| <img | |||
| [src]=" | |||
| state5 && status2 | |||
| ? 'assets/images/sensor-on.png' | |||
| : 'assets/images/sensor-off.png' | |||
| " | |||
| style="width: 30px; height: 30px" | |||
| /> | |||
| </div> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div *ngIf="(state2 && state5 && status2)" class="alarm-text" | |||
| [ngClass]="{'alarm-text-on': (state2 && state5 && status2) }">FENCE ALARM</div> | |||
| <div | |||
| *ngIf="state2 && state5 && status2" | |||
| class="alarm-text" | |||
| [ngClass]="{ 'alarm-text-on': state2 && state5 && status2 }" | |||
| > | |||
| FENCE ALARM | |||
| </div> | |||
| </div> | |||
| <div class="state t4" id="State4" fxLayout="row" > | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px"> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| <div class="state t4" id="State4" fxLayout="row"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px" /> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div class="state t5 tooltip" id="State5" fxLayout="row"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px" /> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div class="state t6 tooltip" id="State6" fxLayout="row"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px" /> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div fxFlex="30" fxLayout="column" fxLayoutGap="50px"> | |||
| <button [disabled]="!isConnected" mat-flat-button color="{{switchArm ? 'accent' : 'primary'}}" (click)="toggleState1()">{{ switchArm ? 'DISARM' : 'ARM'}}</button> | |||
| <button | |||
| [disabled]="!isConnected" | |||
| mat-flat-button | |||
| color="{{ switchArm ? 'accent' : 'primary' }}" | |||
| (click)="toggleState1()" | |||
| > | |||
| {{ switchArm ? "DISARM" : "ARM" }} | |||
| </button> | |||
| </div> | |||
| </div> | |||
| <ng-template #camera> | |||
| <div (click)="openDialog()"> | |||
| <img style="width: 30px; height: 30px; cursor: pointer" src="assets/images/camera.png"> | |||
| <img | |||
| style="width: 30px; height: 30px; cursor: pointer" | |||
| src="assets/images/camera.png" | |||
| /> | |||
| </div> | |||
| </ng-template> | |||
| @@ -8,7 +8,7 @@ describe('SecuritySystemDetailsComponent', () => { | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [SecuritySystemDetailsComponent] | |||
| declarations: [SecuritySystemDetailsComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(SecuritySystemDetailsComponent); | |||
| component = fixture.componentInstance; | |||
| @@ -1,17 +1,17 @@ | |||
| import {Component, OnDestroy, OnInit} from '@angular/core'; | |||
| import {debounceTime, filter, Subject, Subscription} from "rxjs"; | |||
| import {SocketService} from "../../../shared/services/socket.service"; | |||
| import {CameraDialogComponent} from "../../../shared/component/camera-dialog/camera-dialog.component"; | |||
| import {MatDialog} from "@angular/material/dialog"; | |||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | |||
| import { debounceTime, filter, Subject, Subscription } from 'rxjs'; | |||
| import { SocketService } from '../../../shared/services/socket.service'; | |||
| import { CameraDialogComponent } from '../../../shared/component/camera-dialog/camera-dialog.component'; | |||
| import { MatDialog } from '@angular/material/dialog'; | |||
| import { ConfirmDialogService } from '../../../shared/services/confirm-dialog.service'; | |||
| import { AlarmSoundService } from "../../../shared/services/alarm-sound.service"; | |||
| import { MqttClientService } from "../../../shared/services/mqtt-client.service"; | |||
| import {takeUntil} from "rxjs/operators"; | |||
| import { AlarmSoundService } from '../../../shared/services/alarm-sound.service'; | |||
| import { MqttClientService } from '../../../shared/services/mqtt-client.service'; | |||
| import { takeUntil } from 'rxjs/operators'; | |||
| @Component({ | |||
| selector: 'app-security-system-details', | |||
| templateUrl: './security-system-details.component.html', | |||
| styleUrls: ['./security-system-details.component.scss'] | |||
| styleUrls: ['./security-system-details.component.scss'], | |||
| }) | |||
| export class SecuritySystemDetailsComponent implements OnInit, OnDestroy { | |||
| isConnected = false; | |||
| @@ -28,21 +28,26 @@ export class SecuritySystemDetailsComponent implements OnInit, OnDestroy { | |||
| private unsubscribeAll = new Subject(); | |||
| constructor( | |||
| private socketService$: SocketService, | |||
| private dialog: MatDialog, | |||
| private confirm$: ConfirmDialogService, | |||
| private alarmSoundService$: AlarmSoundService, | |||
| private mqtt$: MqttClientService | |||
| private mqtt$: MqttClientService, | |||
| ) {} | |||
| ngOnInit() { | |||
| this.mqtt$.status$.pipe(takeUntil(this.unsubscribeAll)).subscribe((isConnected) => { | |||
| this.isConnected = isConnected; | |||
| }); | |||
| this.mqtt$.messages$.pipe(takeUntil(this.unsubscribeAll), filter((item) => item !== null)).subscribe(message => { | |||
| console.log('detail:',message); | |||
| this.onMessage(message); | |||
| }); | |||
| this.mqtt$.status$ | |||
| .pipe(takeUntil(this.unsubscribeAll)) | |||
| .subscribe((isConnected) => { | |||
| this.isConnected = isConnected; | |||
| }); | |||
| this.mqtt$.messages$ | |||
| .pipe( | |||
| takeUntil(this.unsubscribeAll), | |||
| filter((item) => item !== null), | |||
| ) | |||
| .subscribe((message) => { | |||
| this.onMessage(message); | |||
| }); | |||
| } | |||
| ngOnDestroy(): void { | |||
| @@ -53,13 +58,14 @@ export class SecuritySystemDetailsComponent implements OnInit, OnDestroy { | |||
| toggleState1() { | |||
| this.switchArm = !this.switchArm; | |||
| let str = {id: '0', type: 'cmd', state1: this.switchArm.toString()}; | |||
| let str = { id: '0', type: 'cmd', state1: this.switchArm.toString() }; | |||
| this.mqtt$.sendPublish(str); | |||
| } | |||
| getImageSource(): string { | |||
| return (this.state5 && this.state6) || this.state5 ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'; | |||
| return (this.state5 && this.state6) || this.state5 | |||
| ? 'assets/images/sensor-on.png' | |||
| : 'assets/images/sensor-off.png'; | |||
| } | |||
| onMessage(message: any) { | |||
| @@ -73,20 +79,27 @@ export class SecuritySystemDetailsComponent implements OnInit, OnDestroy { | |||
| this.state5 = message.state5 == '1'; // alarm 9h && 6h // 1 ON, 0 OFF | |||
| this.state6 = message.state6 == '1'; // ? 'ON' : 'OFF'; | |||
| this.switchArm = message.state5 == '1';// alarm 9h && 6h // 1 ON, 0 OFF | |||
| this.switchArm = message.state5 == '1'; // alarm 9h && 6h // 1 ON, 0 OFF | |||
| this.isReady = message.ready == '1'; | |||
| if ((message.status1 == '0' || message.status2 == '0') && this.state5) { // not ready and ON arm | |||
| if ((message.status1 == '0' || message.status2 == '0') && this.state5) { | |||
| // not ready and ON arm | |||
| const data = []; | |||
| if (message.status1 == '0'){ | |||
| data.push({key: 'status1', value: false}) | |||
| if (message.status1 == '0') { | |||
| data.push({ key: 'status1', value: false }); | |||
| } | |||
| if (message.status2 == '0'){ | |||
| data.push({key: 'status2', value: false}) | |||
| if (message.status2 == '0') { | |||
| data.push({ key: 'status2', value: false }); | |||
| } | |||
| this.confirm$.openDialog(data); | |||
| } | |||
| this.alarmSoundService$.startAlarm(this.state5, this.state1,this.status1, this.state2, this.status2); | |||
| this.alarmSoundService$.startAlarm( | |||
| this.state5, | |||
| this.state1, | |||
| this.status1, | |||
| this.state2, | |||
| this.status2, | |||
| ); | |||
| } | |||
| } | |||
| openDialog(): void { | |||
| @@ -1,14 +1,20 @@ | |||
| <div style="padding: 10px 20px" fxLayout="row" fxLayoutAlign="center"> | |||
| <div style="text-align: center; font-size: 24px; width: 95%">Camera Stream</div> | |||
| <div style="text-align: center; font-size: 24px; width: 95%"> | |||
| Camera Stream | |||
| </div> | |||
| <button mat-icon-button color="warn" (click)="onClose()"> | |||
| <mat-icon>close</mat-icon> | |||
| </button> | |||
| </div> | |||
| <div mat-dialog-content> | |||
| <div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="10px" | |||
| style="height: 50%;" | |||
| *ngIf="!player"> | |||
| <div | |||
| fxLayout="row" | |||
| fxLayoutAlign="center center" | |||
| fxLayoutGap="10px" | |||
| style="height: 50%" | |||
| *ngIf="!player" | |||
| > | |||
| <div class="circle-loader"></div> | |||
| <div class="loader"></div> | |||
| </div> | |||
| @@ -2,9 +2,9 @@ | |||
| width: 90% !important; | |||
| //height: 90%; | |||
| display: block; | |||
| margin: 0 auto | |||
| margin: 0 auto; | |||
| } | |||
| .mat-mdc-dialog-content{ | |||
| .mat-mdc-dialog-content { | |||
| max-height: unset; | |||
| } | |||
| @@ -17,20 +17,26 @@ | |||
| animation: l4 1s steps(4) infinite; | |||
| } | |||
| .loader:before { | |||
| content:"Loading..." | |||
| content: "Loading..."; | |||
| } | |||
| @keyframes l4 { | |||
| to { | |||
| clip-path: inset(0 -1ch 0 0); | |||
| } | |||
| } | |||
| @keyframes l4 {to{clip-path: inset(0 -1ch 0 0)}} | |||
| .circle-loader { | |||
| width: 50px; | |||
| aspect-ratio: 1; | |||
| border-radius: 50%; | |||
| background: | |||
| radial-gradient(farthest-side,#ffa516 94%,#0000) top/8px 8px no-repeat, | |||
| conic-gradient(#0000 30%,#ffa516); | |||
| -webkit-mask: radial-gradient(farthest-side,#0000 calc(100% - 8px),#000 0); | |||
| radial-gradient(farthest-side, #ffa516 94%, #0000) top/8px 8px no-repeat, | |||
| conic-gradient(#0000 30%, #ffa516); | |||
| -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0); | |||
| animation: l13 1s infinite linear; | |||
| } | |||
| @keyframes l13{ | |||
| 100%{transform: rotate(1turn)} | |||
| @keyframes l13 { | |||
| 100% { | |||
| transform: rotate(1turn); | |||
| } | |||
| } | |||
| @@ -8,7 +8,7 @@ describe('CameraDialogComponent', () => { | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [CameraDialogComponent] | |||
| declarations: [CameraDialogComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(CameraDialogComponent); | |||
| component = fixture.componentInstance; | |||
| @@ -1,27 +1,32 @@ | |||
| import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core'; | |||
| import {MatDialogRef} from "@angular/material/dialog"; | |||
| import {loadPlayer, Player} from "rtsp-relay/browser"; | |||
| import { | |||
| AfterViewInit, | |||
| Component, | |||
| ElementRef, | |||
| OnDestroy, | |||
| ViewChild, | |||
| } from '@angular/core'; | |||
| import { MatDialogRef } from '@angular/material/dialog'; | |||
| import { loadPlayer, Player } from 'rtsp-relay/browser'; | |||
| @Component({ | |||
| selector: 'app-camera-dialog', | |||
| templateUrl: './camera-dialog.component.html', | |||
| styleUrls: ['./camera-dialog.component.scss'] | |||
| styleUrls: ['./camera-dialog.component.scss'], | |||
| }) | |||
| export class CameraDialogComponent implements AfterViewInit, OnDestroy{ | |||
| export class CameraDialogComponent implements AfterViewInit, OnDestroy { | |||
| player?: Player; | |||
| @ViewChild('videoPlayer') videoPlayer?: ElementRef<HTMLCanvasElement>; | |||
| constructor(public dialogRef: MatDialogRef<CameraDialogComponent>) { } | |||
| constructor(public dialogRef: MatDialogRef<CameraDialogComponent>) {} | |||
| async ngAfterViewInit() { | |||
| this.player = await loadPlayer({ | |||
| url: 'wss://wss.aztrace.vn/stream', | |||
| canvas: this.videoPlayer!.nativeElement, | |||
| onDisconnect(): void{ | |||
| console.log('disconnect camera, please wait'); | |||
| } | |||
| }); | |||
| this.player = await loadPlayer({ | |||
| url: 'wss://wss.aztrace.vn/stream', | |||
| canvas: this.videoPlayer!.nativeElement, | |||
| onDisconnect(): void { | |||
| console.log('disconnect camera, please wait'); | |||
| }, | |||
| }); | |||
| } | |||
| onClose(): void { | |||
| @@ -29,7 +34,7 @@ export class CameraDialogComponent implements AfterViewInit, OnDestroy{ | |||
| } | |||
| ngOnDestroy(): void { | |||
| if (this.player){ | |||
| if (this.player) { | |||
| this.player?.destroy(); | |||
| } | |||
| } | |||
| @@ -1,11 +1,13 @@ | |||
| <h5 mat-dialog-title >CONFIRM TO IGNORE THE WARNING</h5> | |||
| <h5 mat-dialog-title>CONFIRM TO IGNORE THE WARNING</h5> | |||
| <mat-dialog-content style="padding-bottom: 4px" *ngIf="data.length"> | |||
| <div *ngFor="let item of data"> | |||
| <mat-checkbox [(ngModel)]="item.value" | |||
| (ngModelChange)="submitChange(item)" | |||
| [disabled]="isConnected && item.value" | |||
| name="sensor"> | |||
| {{ (item.key == 'status1' ? 'Fire Alarm' : 'Fence Alarm') }} | |||
| <mat-checkbox | |||
| [(ngModel)]="item.value" | |||
| (ngModelChange)="submitChange(item)" | |||
| [disabled]="isConnected && item.value" | |||
| name="sensor" | |||
| > | |||
| {{ item.key == "status1" ? "Fire Alarm" : "Fence Alarm" }} | |||
| </mat-checkbox> | |||
| </div> | |||
| </mat-dialog-content> | |||
| @@ -1,10 +1,12 @@ | |||
| .mat-mdc-dialog-title { | |||
| border-bottom: solid 1px #eeeeee; | |||
| font-size: 14px; | |||
| color: #FFFFFF; | |||
| color: #ffffff; | |||
| background: orange; | |||
| } | |||
| ::ng-deep.mdc-checkbox .mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background{ | |||
| ::ng-deep.mdc-checkbox | |||
| .mdc-checkbox__native-control:enabled:checked | |||
| ~ .mdc-checkbox__background { | |||
| background: orange !important; | |||
| border: orange !important; | |||
| } | |||
| @@ -8,7 +8,7 @@ describe('ConfirmDialogComponent', () => { | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [ConfirmDialogComponent] | |||
| declarations: [ConfirmDialogComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(ConfirmDialogComponent); | |||
| component = fixture.componentInstance; | |||
| @@ -1,16 +1,15 @@ | |||
| import {Component, Inject, OnDestroy, OnInit} from '@angular/core'; | |||
| import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; | |||
| import {SocketService} from "../../services/socket.service"; | |||
| import {Subscription} from "rxjs"; | |||
| import {MqttClientService} from "../../services/mqtt-client.service"; | |||
| import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; | |||
| import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | |||
| import { SocketService } from '../../services/socket.service'; | |||
| import { Subscription } from 'rxjs'; | |||
| import { MqttClientService } from '../../services/mqtt-client.service'; | |||
| @Component({ | |||
| selector: 'app-confirm-dialog', | |||
| templateUrl: './confirm-dialog.component.html', | |||
| styleUrls: ['./confirm-dialog.component.scss'], | |||
| }) | |||
| export class ConfirmDialogComponent implements OnDestroy{ | |||
| export class ConfirmDialogComponent implements OnDestroy { | |||
| private statusSubscription?: Subscription; | |||
| isConnected: boolean = false; | |||
| @@ -19,22 +18,20 @@ export class ConfirmDialogComponent implements OnDestroy{ | |||
| private socketService$: SocketService, | |||
| private mqtt$: MqttClientService, | |||
| ) { | |||
| this.statusSubscription = this.mqtt$.status$.subscribe( | |||
| (isConnected) => { | |||
| this.isConnected = isConnected; | |||
| }, | |||
| ); | |||
| this.statusSubscription = this.mqtt$.status$.subscribe((isConnected) => { | |||
| this.isConnected = isConnected; | |||
| }); | |||
| } | |||
| submitChange(item: any): void{ | |||
| if(this.isConnected) { | |||
| submitChange(item: any): void { | |||
| if (this.isConnected) { | |||
| const position = item.key === 'status1' ? '1' : '2'; | |||
| this.mqtt$.sendPublish({ id: '0', type: 'bypass', state: position}) | |||
| this.mqtt$.sendPublish({ id: '0', type: 'bypass', state: position }); | |||
| item.value = true; | |||
| } | |||
| } | |||
| ngOnDestroy() { | |||
| ngOnDestroy() { | |||
| if (this.statusSubscription) { | |||
| this.statusSubscription.unsubscribe(); | |||
| } | |||
| @@ -1,11 +1,14 @@ | |||
| <div class="popup-container"> | |||
| <div class="popup-header"> | |||
| <span class="popup-title">{{item.title}}</span> | |||
| <span class="popup-title">{{ item.title }}</span> | |||
| <mat-icon class="close-button" (click)="onClose()">close</mat-icon> | |||
| </div> | |||
| <div> | |||
| <p><strong>Địa điểm:</strong> {{item.detail.position}}</p> | |||
| <p><strong>Tọa độ:</strong> {{item.detail.coordinates.lat}}, {{item.detail.coordinates.lng}}</p> | |||
| <p><strong>Thời gian:</strong> {{item.detail.time}}</p> | |||
| <p><strong>Địa điểm:</strong> {{ item.detail.position }}</p> | |||
| <p> | |||
| <strong>Tọa độ:</strong> {{ item.detail.coordinates.lat }}, | |||
| {{ item.detail.coordinates.lng }} | |||
| </p> | |||
| <p><strong>Thời gian:</strong> {{ item.detail.time }}</p> | |||
| </div> | |||
| </div> | |||
| @@ -14,16 +14,14 @@ | |||
| } | |||
| .popup-title { | |||
| color: #F33152; | |||
| color: #f33152; | |||
| font-weight: 500; | |||
| } | |||
| .close-button { | |||
| cursor: pointer; | |||
| color: #eeeeee; | |||
| &:hover{ | |||
| &:hover { | |||
| color: #000000; | |||
| } | |||
| } | |||
| @@ -1,13 +1,13 @@ | |||
| <div class="layout-container"> | |||
| <div class="header mat-elevation-z1"> | |||
| <img src="../../../../../../assets/images/logo.png" > | |||
| <img src="../../../../../../assets/images/logo.png" /> | |||
| <div> | |||
| <button mat-button routerLink="/homepage" routerLinkActive="active">Home</button> | |||
| <button mat-button routerLink="/homepage" routerLinkActive="active"> | |||
| Home | |||
| </button> | |||
| </div> | |||
| </div> | |||
| <div> | |||
| <div style="flex: 1"> | |||
| <router-outlet></router-outlet> | |||
| </div> | |||
| </div> | |||
| @@ -1,14 +1,15 @@ | |||
| .layout-container{ | |||
| .layout-container { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| height: 100%; | |||
| .header { | |||
| height: 3rem; | |||
| padding: .25rem 2rem; | |||
| padding: 0.25rem 2rem; | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| img{ | |||
| img { | |||
| width: 7rem; | |||
| height: 3rem; | |||
| } | |||
| @@ -19,5 +20,4 @@ | |||
| color: #ff7723 !important; | |||
| } | |||
| } | |||
| } | |||
| @@ -8,7 +8,7 @@ describe('LayoutComponent', () => { | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [LayoutComponent] | |||
| declarations: [LayoutComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(LayoutComponent); | |||
| component = fixture.componentInstance; | |||
| @@ -3,8 +3,6 @@ import { Component } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-layout', | |||
| templateUrl: './layout.component.html', | |||
| styleUrls: ['./layout.component.scss'] | |||
| styleUrls: ['./layout.component.scss'], | |||
| }) | |||
| export class LayoutComponent { | |||
| } | |||
| export class LayoutComponent {} | |||
| @@ -3,15 +3,34 @@ | |||
| <div class="speaker"> | |||
| <mat-icon>{{ icon }}</mat-icon> | |||
| </div> | |||
| <button class="volume-control" [disabled]="disable" (click)="decreaseVolume()"> | |||
| <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" | |||
| xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <button | |||
| class="volume-control" | |||
| [disabled]="disable" | |||
| (click)="decreaseVolume()" | |||
| > | |||
| <svg | |||
| width="24px" | |||
| height="24px" | |||
| viewBox="0 0 24 24" | |||
| version="1.1" | |||
| xmlns="http://www.w3.org/2000/svg" | |||
| xmlns:xlink="http://www.w3.org/1999/xlink" | |||
| > | |||
| <title>Icons/Minus square fill</title> | |||
| <g id="Icons/Minus-square-fill" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g | |||
| id="Icons/Minus-square-fill" | |||
| stroke="none" | |||
| stroke-width="1" | |||
| fill="none" | |||
| fill-rule="evenodd" | |||
| > | |||
| <g id="MinusSquare"> | |||
| <path | |||
| d="M19.5,3 L4.5,3 C3.67157288,3 3,3.67157288 3,4.5 L3,19.5 C3,20.3284271 3.67157288,21 4.5,21 L19.5,21 C20.3284271,21 21,20.3284271 21,19.5 L21,4.5 C21,3.67157288 20.3284271,3 19.5,3 Z M15.75,12.75 L8.25,12.75 C7.83578644,12.75 7.5,12.4142136 7.5,12 C7.5,11.5857864 7.83578644,11.25 8.25,11.25 L15.75,11.25 C16.1642136,11.25 16.5,11.5857864 16.5,12 C16.5,12.4142136 16.1642136,12.75 15.75,12.75 Z" | |||
| id="Shape" fill="currentColor" fill-rule="nonzero"></path> | |||
| id="Shape" | |||
| fill="currentColor" | |||
| fill-rule="nonzero" | |||
| ></path> | |||
| <rect id="Rectangle" x="0" y="0" width="24" height="24"></rect> | |||
| </g> | |||
| </g> | |||
| @@ -19,21 +38,44 @@ | |||
| </button> | |||
| <div class="volume-control" style="padding: 1rem 0"> | |||
| <div class="volume-slider"> | |||
| <input type="range" [(ngModel)]="value" (input)="onSliderChange($event)" | |||
| [ngStyle]="onSliderChangeBackground()" | |||
| [max]="max" | |||
| [disabled]="disable"> | |||
| <input | |||
| type="range" | |||
| [(ngModel)]="value" | |||
| (input)="onSliderChange($event)" | |||
| [ngStyle]="onSliderChangeBackground()" | |||
| [max]="max" | |||
| [disabled]="disable" | |||
| /> | |||
| </div> | |||
| </div> | |||
| <button class="volume-control" [disabled]="disable" (click)="increaseVolume()"> | |||
| <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" | |||
| xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <button | |||
| class="volume-control" | |||
| [disabled]="disable" | |||
| (click)="increaseVolume()" | |||
| > | |||
| <svg | |||
| width="24px" | |||
| height="24px" | |||
| viewBox="0 0 24 24" | |||
| version="1.1" | |||
| xmlns="http://www.w3.org/2000/svg" | |||
| xmlns:xlink="http://www.w3.org/1999/xlink" | |||
| > | |||
| <title>Icons/Plus square fill</title> | |||
| <g id="Icons/Plus-square-fill" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g | |||
| id="Icons/Plus-square-fill" | |||
| stroke="none" | |||
| stroke-width="1" | |||
| fill="none" | |||
| fill-rule="evenodd" | |||
| > | |||
| <g id="Plus"> | |||
| <path | |||
| d="M19.5,3 L4.5,3 C3.67157288,3 3,3.67157288 3,4.5 L3,19.5 C3,20.3284271 3.67157288,21 4.5,21 L19.5,21 C20.3284271,21 21,20.3284271 21,19.5 L21,4.5 C21,3.67157288 20.3284271,3 19.5,3 Z M17.25,12.75 L12.75,12.75 L12.75,17.25 C12.75,17.6642136 12.4142136,18 12,18 C11.5857864,18 11.25,17.6642136 11.25,17.25 L11.25,12.75 L6.75,12.75 C6.33578644,12.75 6,12.4142136 6,12 C6,11.5857864 6.33578644,11.25 6.75,11.25 L11.25,11.25 L11.25,6.75 C11.25,6.33578644 11.5857864,6 12,6 C12.4142136,6 12.75,6.33578644 12.75,6.75 L12.75,11.25 L17.25,11.25 C17.6642136,11.25 18,11.5857864 18,12 C18,12.4142136 17.6642136,12.75 17.25,12.75 Z" | |||
| id="Shape" fill="currentColor" fill-rule="nonzero"></path> | |||
| id="Shape" | |||
| fill="currentColor" | |||
| fill-rule="nonzero" | |||
| ></path> | |||
| <rect id="Rectangle" x="0" y="0" width="24" height="24"></rect> | |||
| </g> | |||
| </g> | |||
| @@ -21,29 +21,29 @@ | |||
| width: 100%; | |||
| height: 100%; | |||
| clip-path: polygon( | |||
| 50% 0, | |||
| 100% 25%, | |||
| 100% 75%, | |||
| 50% 100%, | |||
| 0 75%, | |||
| 0 25%, | |||
| 50% 0, | |||
| 50% 1px, | |||
| 1px calc(25% + .5px), | |||
| 1px calc(75% - .5px), | |||
| 50% calc(100% - 1px), | |||
| calc(100% - 1px) calc(75% - .5px), | |||
| calc(100% - 1px) calc(25% + .5px), | |||
| 50% 1px | |||
| 50% 0, | |||
| 100% 25%, | |||
| 100% 75%, | |||
| 50% 100%, | |||
| 0 75%, | |||
| 0 25%, | |||
| 50% 0, | |||
| 50% 1px, | |||
| 1px calc(25% + 0.5px), | |||
| 1px calc(75% - 0.5px), | |||
| 50% calc(100% - 1px), | |||
| calc(100% - 1px) calc(75% - 0.5px), | |||
| calc(100% - 1px) calc(25% + 0.5px), | |||
| 50% 1px | |||
| ); | |||
| background-color: #ff7723; | |||
| } | |||
| } | |||
| .volume-control { | |||
| padding: .63rem; | |||
| border-top: solid .06rem #ff7723; | |||
| border-bottom: solid .06rem #ff7723; | |||
| padding: 0.63rem; | |||
| border-top: solid 0.06rem #ff7723; | |||
| border-bottom: solid 0.06rem #ff7723; | |||
| background-color: transparent; | |||
| border-left: none; | |||
| border-right: none; | |||
| @@ -53,12 +53,11 @@ | |||
| width: 1.5rem; | |||
| height: 1.5rem; | |||
| } | |||
| } | |||
| .volume-slider { | |||
| border: .06rem solid #eed3c2; | |||
| height: .75rem; | |||
| border: 0.06rem solid #eed3c2; | |||
| height: 0.75rem; | |||
| width: 39rem; | |||
| position: relative; | |||
| @@ -72,29 +71,28 @@ | |||
| width: 38.5rem; | |||
| appearance: none; | |||
| background-color: transparent; | |||
| margin: .25rem; | |||
| margin: 0.25rem; | |||
| &::-webkit-slider-thumb { | |||
| -webkit-appearance: none; | |||
| appearance: none; | |||
| background: #ff7723 | |||
| ; | |||
| border: .06rem solid #ff7723; | |||
| height: .88rem; | |||
| width: .88rem; | |||
| background: #ff7723; | |||
| border: 0.06rem solid #ff7723; | |||
| height: 0.88rem; | |||
| width: 0.88rem; | |||
| padding: 2px; | |||
| background-clip: content-box; | |||
| transition: transform .2s ease; | |||
| transition: transform 0.2s ease; | |||
| &:focus { | |||
| cursor: pointer; | |||
| border: .12rem solid #ff7723; | |||
| border: 0.12rem solid #ff7723; | |||
| transform: scale(1.2); | |||
| } | |||
| &:hover { | |||
| cursor: pointer; | |||
| border: .12rem solid #ff7723; | |||
| border: 0.12rem solid #ff7723; | |||
| transform: scale(1.2); | |||
| } | |||
| } | |||
| @@ -102,20 +100,20 @@ | |||
| &::-moz-range-thumb { | |||
| border-radius: 0; | |||
| background: #ff7723; | |||
| border: .06rem solid #ff7723; | |||
| height: .5rem; | |||
| width: .5rem ; | |||
| border: 0.06rem solid #ff7723; | |||
| height: 0.5rem; | |||
| width: 0.5rem; | |||
| padding: 3px; | |||
| background-clip: content-box; | |||
| &:focus { | |||
| cursor: pointer; | |||
| border: .14rem solid #ff7723; | |||
| border: 0.14rem solid #ff7723; | |||
| transform: scale(1); | |||
| } | |||
| &:hover { | |||
| cursor: pointer; | |||
| border: .14rem solid #ff7723; | |||
| border: 0.14rem solid #ff7723; | |||
| transform: scale(1); | |||
| } | |||
| } | |||
| @@ -124,26 +122,26 @@ | |||
| -webkit-appearance: none; | |||
| appearance: none; | |||
| background: #ff7723; | |||
| border: .06rem solid #ff7723; | |||
| height: .88rem; | |||
| width: .88rem; | |||
| border: 0.06rem solid #ff7723; | |||
| height: 0.88rem; | |||
| width: 0.88rem; | |||
| padding: 3px; | |||
| background-clip: content-box; | |||
| &:focus { | |||
| cursor: pointer; | |||
| border: .12rem solid #ff7723; | |||
| border: 0.12rem solid #ff7723; | |||
| transform: scale(1.2); | |||
| } | |||
| &:hover { | |||
| cursor: pointer; | |||
| border: .12rem solid #ff7723; | |||
| border: 0.12rem solid #ff7723; | |||
| transform: scale(1.2); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| svg{ | |||
| svg { | |||
| color: #ff7723; | |||
| } | |||
| @@ -8,7 +8,7 @@ describe('SilderRangeComponent', () => { | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [SliderRangeComponent] | |||
| declarations: [SliderRangeComponent], | |||
| }); | |||
| fixture = TestBed.createComponent(SliderRangeComponent); | |||
| component = fixture.componentInstance; | |||
| @@ -6,7 +6,7 @@ import { | |||
| Output, | |||
| } from '@angular/core'; | |||
| import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |||
| import {debounceTime, Subject} from 'rxjs'; | |||
| import { debounceTime, Subject } from 'rxjs'; | |||
| @Component({ | |||
| selector: 'app-slider-range', | |||
| @@ -31,7 +31,7 @@ export class SliderRangeComponent implements ControlValueAccessor { | |||
| private valueChangeSubject = new Subject<number>(); | |||
| constructor() { | |||
| this.valueChangeSubject.pipe(debounceTime(500)).subscribe(value => { | |||
| this.valueChangeSubject.pipe(debounceTime(500)).subscribe((value) => { | |||
| this.valueChange.emit(value); | |||
| }); | |||
| } | |||
| @@ -95,7 +95,6 @@ export class SliderRangeComponent implements ControlValueAccessor { | |||
| this.onChange(this.value); | |||
| this.onTouched(); | |||
| this.valueChangeSubject.next(this.value); | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| import { Pipe, PipeTransform } from '@angular/core'; | |||
| import * as moment from 'moment'; | |||
| @Pipe({ | |||
| name: 'timeElapsed', | |||
| }) | |||
| export class TimeElapsedPipe implements PipeTransform { | |||
| transform(checkInTime: string | null): any { | |||
| if (!checkInTime) return; | |||
| const checkInMoment = moment(checkInTime); | |||
| const now = moment(); | |||
| return moment.duration(now.diff(checkInMoment)).humanize(); | |||
| } | |||
| } | |||
| @@ -1,5 +1,5 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import {max} from "rxjs"; | |||
| import { max } from 'rxjs'; | |||
| @Injectable({ | |||
| providedIn: 'root', | |||
| @@ -13,25 +13,31 @@ export class AlarmSoundService { | |||
| constructor() { | |||
| this.audio.src = 'assets/sound/alarm_5m.mp3'; | |||
| this.audio.load(); | |||
| } | |||
| playSound(): void { | |||
| if(!this.isPlay){ | |||
| this.audio.play().then(() => { | |||
| this.isPlay = true; | |||
| this.alertInterval = setInterval(() => { | |||
| this.stopAlarm(); | |||
| }, this.alertDuration); | |||
| }).catch((error) => { | |||
| console.error('Error playing audio:', error); | |||
| }); | |||
| if (!this.isPlay) { | |||
| this.audio | |||
| .play() | |||
| .then(() => { | |||
| this.isPlay = true; | |||
| this.alertInterval = setInterval(() => { | |||
| this.stopAlarm(); | |||
| }, this.alertDuration); | |||
| }) | |||
| .catch((error) => { | |||
| console.error('Error playing audio:', error); | |||
| }); | |||
| } | |||
| } | |||
| startAlarm(isTurnOn: boolean, fireArm: boolean, fireStatus: boolean, fenceArm: boolean, fenceStatus: boolean): void { | |||
| startAlarm( | |||
| isTurnOn: boolean, | |||
| fireArm: boolean, | |||
| fireStatus: boolean, | |||
| fenceArm: boolean, | |||
| fenceStatus: boolean, | |||
| ): void { | |||
| if (!isTurnOn) { | |||
| this.stopAlarm(); | |||
| return; | |||
| @@ -60,12 +66,12 @@ export class AlarmSoundService { | |||
| this.audio.currentTime = 0; | |||
| this.isPlay = false; | |||
| } | |||
| if (this.alertInterval){ | |||
| if (this.alertInterval) { | |||
| clearInterval(this.alertInterval); | |||
| } | |||
| } | |||
| getTimeDuration(key: string) { | |||
| return parseInt(localStorage.getItem(key) ?? '0') * 100; | |||
| return parseInt(localStorage.getItem(key) ?? '0') * 100; | |||
| } | |||
| } | |||
| @@ -15,7 +15,7 @@ export class ConfirmDialogService { | |||
| if (!this.isDialogOpen) { | |||
| this.isDialogOpen = true; | |||
| const dialogRef = this.dialog.open(ConfirmDialogComponent, { | |||
| data: data | |||
| data: data, | |||
| }); | |||
| dialogRef | |||
| .afterClosed() | |||
| @@ -7,11 +7,16 @@ import { | |||
| IPublishOptions, | |||
| } from 'ngx-mqtt'; | |||
| import { IClientSubscribeOptions } from 'mqtt-browser'; | |||
| import {BehaviorSubject, Observable, Subject, Subscription} from 'rxjs'; | |||
| import {TOPIC_GETTING, TOPIC_INFO, TOPIC_LOG, TOPIC_SETTING} from "../../app.constants"; | |||
| import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; | |||
| import { | |||
| TOPIC_GETTING, | |||
| TOPIC_INFO, | |||
| TOPIC_LOG, | |||
| TOPIC_SETTING, | |||
| } from '../../app.constants'; | |||
| @Injectable({ | |||
| providedIn: 'root' | |||
| providedIn: 'root', | |||
| }) | |||
| export class MqttClientService { | |||
| client: MqttService | undefined; | |||
| @@ -23,11 +28,11 @@ export class MqttClientService { | |||
| connection = { | |||
| hostname: 'broker.hivemq.com', | |||
| clean: true, | |||
| path:'/mqtt', | |||
| path: '/mqtt', | |||
| port: 8884, | |||
| reconnectPeriod: 4000, | |||
| protocol: 'wss', | |||
| } | |||
| }; | |||
| receiveNews = ''; | |||
| isConnection = false; | |||
| private statusSubject = new BehaviorSubject<boolean>(this.isConnection); | |||
| @@ -40,7 +45,7 @@ export class MqttClientService { | |||
| // 创建连接 | |||
| createConnection() { | |||
| try { | |||
| this.client?.connect(this.connection as IMqttServiceOptions) | |||
| this.client?.connect(this.connection as IMqttServiceOptions); | |||
| } catch (error) { | |||
| console.log('mqtt.connect error', error); | |||
| } | |||
| @@ -69,64 +74,75 @@ export class MqttClientService { | |||
| topic: TOPIC_LOG, | |||
| qos: 0, | |||
| }; | |||
| this.curSubscription = this.client?.observe(topic, { qos, dup: false } as IClientSubscribeOptions).subscribe((message: IMqttMessage) => { | |||
| try { | |||
| let plainMessage = ""; | |||
| for (var i = 0; i < message.payload.length; i++) { | |||
| plainMessage += String.fromCharCode(parseInt(String(message.payload[i]))); | |||
| this.curSubscription = this.client | |||
| ?.observe(topic, { qos, dup: false } as IClientSubscribeOptions) | |||
| .subscribe((message: IMqttMessage) => { | |||
| try { | |||
| let plainMessage = ''; | |||
| for (var i = 0; i < message.payload.length; i++) { | |||
| plainMessage += String.fromCharCode( | |||
| parseInt(String(message.payload[i])), | |||
| ); | |||
| } | |||
| console.log(JSON.parse(plainMessage)); | |||
| this.messagesSubject$.next(JSON.parse(plainMessage)); | |||
| } catch (e) { | |||
| console.log(e); | |||
| } | |||
| console.log(JSON.parse(plainMessage)); | |||
| this.messagesSubject$.next(JSON.parse(plainMessage)); | |||
| }catch (e) { | |||
| console.log(e); | |||
| } | |||
| }); | |||
| }); | |||
| } | |||
| // 订阅主题 | |||
| getSetting() { | |||
| const { topic, qos } = { | |||
| topic: TOPIC_SETTING, | |||
| qos: 0, | |||
| }; | |||
| this.settingSubscription = this.client?.observe(topic, { qos, dup: false } as IClientSubscribeOptions).subscribe((message: IMqttMessage) => { | |||
| try { | |||
| let plainMessage = ""; | |||
| for (var i = 0; i < message.payload.length; i++) { | |||
| plainMessage += String.fromCharCode(parseInt(String(message.payload[i]))); | |||
| this.settingSubscription = this.client | |||
| ?.observe(topic, { qos, dup: false } as IClientSubscribeOptions) | |||
| .subscribe((message: IMqttMessage) => { | |||
| try { | |||
| let plainMessage = ''; | |||
| for (var i = 0; i < message.payload.length; i++) { | |||
| plainMessage += String.fromCharCode( | |||
| parseInt(String(message.payload[i])), | |||
| ); | |||
| } | |||
| console.log(JSON.parse(plainMessage)); | |||
| this.settingSubject$.next(JSON.parse(plainMessage)); | |||
| } catch (e) { | |||
| console.log(e); | |||
| } | |||
| console.log(JSON.parse(plainMessage)); | |||
| this.settingSubject$.next(JSON.parse(plainMessage)); | |||
| }catch (e) { | |||
| console.log(e); | |||
| } | |||
| }); | |||
| }); | |||
| } | |||
| // 取消订阅 | |||
| clearLog() { | |||
| this.curSubscription?.unsubscribe() | |||
| this.curSubscription?.unsubscribe(); | |||
| this.settingSubscription?.unsubscribe(); | |||
| } | |||
| // 发送消息 | |||
| doPublish(data: any) { | |||
| this.client?.unsafePublish(TOPIC_INFO, JSON.stringify(data), {qos: 0} as IPublishOptions); | |||
| this.client?.unsafePublish(TOPIC_INFO, JSON.stringify(data), { | |||
| qos: 0, | |||
| } as IPublishOptions); | |||
| } | |||
| // 发送消息 | |||
| sendPublish(data: any, topic = TOPIC_INFO) { | |||
| this.client?.unsafePublish(topic, JSON.stringify(data), {qos: 0} as IPublishOptions); | |||
| this.client?.unsafePublish(topic, JSON.stringify(data), { | |||
| qos: 0, | |||
| } as IPublishOptions); | |||
| // this.client?.publish("isoft/node 4/in4", JSON.stringify(data), {qos: 0} as IPublishOptions).subscribe(); | |||
| } | |||
| // 断开连接 | |||
| destroyConnection() { | |||
| try { | |||
| this.client?.disconnect(true) | |||
| this.isConnection = false | |||
| console.log('Successfully disconnected!') | |||
| this.client?.disconnect(true); | |||
| this.isConnection = false; | |||
| console.log('Successfully disconnected!'); | |||
| } catch (error: any) { | |||
| console.log('Disconnect failed', error.toString()) | |||
| console.log('Disconnect failed', error.toString()); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,34 +1,35 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; | |||
| import { BehaviorSubject,Subject } from "rxjs"; | |||
| import { config } from "../../../assets/config/config"; | |||
| import { BehaviorSubject, Subject } from 'rxjs'; | |||
| import { config } from '../../../assets/config/config'; | |||
| @Injectable({ | |||
| providedIn: 'root' | |||
| providedIn: 'root', | |||
| }) | |||
| export class SocketService { | |||
| gateway = config.gateway; | |||
| websocket$: WebSocketSubject<any> | undefined ; | |||
| websocket$: WebSocketSubject<any> | undefined; | |||
| private isConnected: boolean = false; | |||
| private messagesSubject$ = new Subject(); | |||
| public messages$ = this.messagesSubject$.asObservable(); | |||
| private statusSubject = new BehaviorSubject<boolean>(this.isConnected); | |||
| public status$ = this.statusSubject.asObservable(); | |||
| constructor() { | |||
| } | |||
| constructor() {} | |||
| public connect(cfg: { reconnect: boolean } = { reconnect: false }): void { | |||
| if (cfg.reconnect || !this.websocket$ || this.websocket$.closed) { | |||
| console.log(cfg.reconnect ? 'Reconnecting WebSocket…' : 'Trying to open a WebSocket connection…'); | |||
| console.log( | |||
| cfg.reconnect | |||
| ? 'Reconnecting WebSocket…' | |||
| : 'Trying to open a WebSocket connection…', | |||
| ); | |||
| this.websocket$ = this.getNewWebSocket(); | |||
| this.websocket$.subscribe((messages) => { | |||
| this.messagesSubject$.next(messages); | |||
| }); | |||
| } | |||
| } | |||
| close() { | |||
| @@ -48,16 +49,16 @@ export class SocketService { | |||
| console.log('Connection ok'); | |||
| this.isConnected = true; | |||
| this.statusSubject.next(this.isConnected); | |||
| } | |||
| }, | |||
| }, | |||
| closeObserver: { | |||
| next: () => { | |||
| console.log('Connection closed'); | |||
| this.websocket$ = undefined; | |||
| this.isConnected = false; | |||
| this.connect({reconnect: true}); | |||
| this.connect({ reconnect: true }); | |||
| this.statusSubject.next(this.isConnected); | |||
| } | |||
| }, | |||
| }, | |||
| }); | |||
| } | |||
| @@ -1,17 +1,12 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import {SharedComponentModule} from "./component/shared-component.module"; | |||
| import {FormsModule} from "@angular/forms"; | |||
| import { SharedComponentModule } from './component/shared-component.module'; | |||
| import { FormsModule } from '@angular/forms'; | |||
| import { TimeElapsedPipe } from './pipes/time-elapsed.pipe'; | |||
| @NgModule({ | |||
| declarations: [], | |||
| imports: [ | |||
| CommonModule, | |||
| SharedComponentModule, | |||
| FormsModule | |||
| ], | |||
| exports: [ | |||
| SharedComponentModule, | |||
| ] | |||
| declarations: [TimeElapsedPipe], | |||
| imports: [CommonModule, SharedComponentModule, FormsModule], | |||
| exports: [SharedComponentModule, TimeElapsedPipe], | |||
| }) | |||
| export class SharedModule { } | |||
| export class SharedModule {} | |||
| @@ -1,3 +1,3 @@ | |||
| export const config = { | |||
| gateway: 'wss:/socket.aztrace.vn/ws' | |||
| gateway: 'wss:/socket.aztrace.vn/ws', | |||
| }; | |||
| @@ -66,7 +66,7 @@ | |||
| } | |||
| @mixin mixPosition($i, $px) { | |||
| .top-#{$i} { | |||
| top: $px * 1px !important; | |||
| top: $px * 1px !important; | |||
| } | |||
| .top--#{$i} { | |||
| top: $px * -1px !important; | |||
| @@ -84,14 +84,25 @@ | |||
| left: $px * 1px !important; | |||
| } | |||
| } | |||
| $pxs: 0 0, 1 4, 2 8, 3 16, 4 24, 5 32, 6 48, 7 56, 8 62, 9 70, 10 78 ; | |||
| $pxs: | |||
| 0 0, | |||
| 1 4, | |||
| 2 8, | |||
| 3 16, | |||
| 4 24, | |||
| 5 32, | |||
| 6 48, | |||
| 7 56, | |||
| 8 62, | |||
| 9 70, | |||
| 10 78; | |||
| @each $i, $px in $pxs { | |||
| @include mixPaddingMargin($i, $px); | |||
| @include mixPosition($i, $px); | |||
| } | |||
| @each $weight in 100, 200, 300, 400, 500, 600, 700, 800, 900 { | |||
| [mat-weight='#{$weight}'] { | |||
| [mat-weight="#{$weight}"] { | |||
| font-weight: $weight !important; | |||
| } | |||
| } | |||
| @@ -99,12 +110,22 @@ $pxs: 0 0, 1 4, 2 8, 3 16, 4 24, 5 32, 6 48, 7 56, 8 62, 9 70, 10 78 ; | |||
| @mixin mixFontSize($i, $rem) { | |||
| // reusable code here | |||
| .fz-#{$i} { | |||
| font-size: $rem * 1rem !important; | |||
| font-size: $rem * 1rem !important; | |||
| } | |||
| } | |||
| $rems: 0 0.5, 1 0.75, 2 1, 3 1.25, 4 1.5, 5 1.75, 6 2, 7 2.25, 8 2.5, 9 2.75, 10 3; | |||
| @each $i, $rem in $rems { | |||
| $rems: | |||
| 0 0.5, | |||
| 1 0.75, | |||
| 2 1, | |||
| 3 1.25, | |||
| 4 1.5, | |||
| 5 1.75, | |||
| 6 2, | |||
| 7 2.25, | |||
| 8 2.5, | |||
| 9 2.75, | |||
| 10 3; | |||
| @each $i, $rem in $rems { | |||
| @include mixFontSize($i, $rem); | |||
| } | |||
| @@ -1,16 +1,22 @@ | |||
| <!doctype html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <title>IotWebUi</title> | |||
| <base href="/"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <link rel="icon" type="image/x-icon" href="favicon.ico"> | |||
| <link rel="preconnect" href="https://fonts.gstatic.com"> | |||
| <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> | |||
| <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |||
| </head> | |||
| <body class="mat-typography"> | |||
| <app-root></app-root> | |||
| </body> | |||
| <head> | |||
| <meta charset="utf-8" /> | |||
| <title>IotWebUi</title> | |||
| <base href="/" /> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |||
| <link rel="icon" type="image/x-icon" href="favicon.ico" /> | |||
| <link rel="preconnect" href="https://fonts.gstatic.com" /> | |||
| <link | |||
| href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" | |||
| rel="stylesheet" | |||
| /> | |||
| <link | |||
| href="https://fonts.googleapis.com/icon?family=Material+Icons" | |||
| rel="stylesheet" | |||
| /> | |||
| </head> | |||
| <body class="mat-typography"> | |||
| <app-root></app-root> | |||
| </body> | |||
| </html> | |||
| @@ -2,6 +2,6 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | |||
| import { AppModule } from './app/app.module'; | |||
| platformBrowserDynamic().bootstrapModule(AppModule) | |||
| .catch(err => console.error(err)); | |||
| platformBrowserDynamic() | |||
| .bootstrapModule(AppModule) | |||
| .catch((err) => console.error(err)); | |||
| @@ -1,14 +1,15 @@ | |||
| const express = require('express'); | |||
| const expressWs = require('express-ws'); | |||
| const express = require("express"); | |||
| const expressWs = require("express-ws"); | |||
| const app = express(); | |||
| expressWs(app); | |||
| const { proxy, scriptUrl } = require('rtsp-relay')(app); | |||
| const { proxy, scriptUrl } = require("rtsp-relay")(app); | |||
| const port = 8080; | |||
| const urlCamera = 'rtsp://admin:[email protected]:88/cam/realmonitor?channel=1&subtype=0'; | |||
| const urlCamera = | |||
| "rtsp://admin:[email protected]:88/cam/realmonitor?channel=1&subtype=0"; | |||
| app.ws('/api/stream/:cameraIP', (ws, req) => | |||
| app.ws("/api/stream/:cameraIP", (ws, req) => | |||
| proxy({ | |||
| url: `rtsp://${req.params.cameraIP}:554/feed`, | |||
| })(ws), | |||
| @@ -17,13 +18,11 @@ app.ws('/api/stream/:cameraIP', (ws, req) => | |||
| const handler = proxy({ | |||
| url: urlCamera, | |||
| verbose: false, | |||
| transport: 'tcp' | |||
| transport: "tcp", | |||
| }); | |||
| app.ws('/stream', handler); | |||
| app.ws("/stream", handler); | |||
| const server = app.listen(port, () => { | |||
| console.log(`RTSP Relay Server is listening on port ${port}`); | |||
| }); | |||
| @@ -1,4 +1,10 @@ | |||
| /* You can add global styles to this file, and also import other style files */ | |||
| @import "assets/style/scss/common"; | |||
| html, body { height: 100%; } | |||
| body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } | |||
| html, | |||
| body { | |||
| height: 100%; | |||
| } | |||
| body { | |||
| margin: 0; | |||
| font-family: Roboto, "Helvetica Neue", sans-serif; | |||
| } | |||