├── .editorconfig ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── ngsw-config.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.config.ts │ ├── app.routes.ts │ ├── ble.config.ts │ ├── ble.service.ts │ ├── components │ │ ├── btn-box │ │ │ ├── btn-box.component.html │ │ │ ├── btn-box.component.scss │ │ │ └── btn-box.component.ts │ │ ├── color-picker │ │ │ ├── colorpicker.html │ │ │ ├── colorpicker.scss │ │ │ └── colorpicker.ts │ │ ├── ctrl-box │ │ │ ├── ctrl-box.component.html │ │ │ ├── ctrl-box.component.scss │ │ │ └── ctrl-box.component.ts │ │ ├── data-box │ │ │ ├── data-box.component.html │ │ │ ├── data-box.component.scss │ │ │ └── data-box.component.ts │ │ ├── device-title │ │ │ ├── device-title.component.html │ │ │ ├── device-title.component.scss │ │ │ └── device-title.component.ts │ │ ├── input-box │ │ │ ├── input-box.component.html │ │ │ ├── input-box.component.scss │ │ │ └── input-box.component.ts │ │ ├── line-chart │ │ │ ├── line-chart.component.html │ │ │ ├── line-chart.component.scss │ │ │ └── line-chart.component.ts │ │ ├── setting-btn │ │ │ ├── setting-btn.component.html │ │ │ ├── setting-btn.component.scss │ │ │ └── setting-btn.component.ts │ │ ├── toggle │ │ │ ├── toggle.component.html │ │ │ ├── toggle.component.scss │ │ │ └── toggle.component.ts │ │ ├── widget-joystick │ │ │ ├── widget-joystick.html │ │ │ ├── widget-joystick.scss │ │ │ └── widget-joystick.ts │ │ └── widget-range │ │ │ ├── widget-range.html │ │ │ ├── widget-range.scss │ │ │ └── widget-range.ts │ ├── configs │ │ ├── app.config.ts │ │ └── device.config.ts │ ├── data.service.ts │ ├── device.service.ts │ ├── pages │ │ ├── connect │ │ │ ├── connect.component.html │ │ │ ├── connect.component.scss │ │ │ └── connect.component.ts │ │ ├── device-title.scss │ │ ├── iot-page │ │ │ ├── iot-page.component.html │ │ │ ├── iot-page.component.scss │ │ │ └── iot-page.component.ts │ │ ├── joystick-page │ │ │ ├── joystick-page.component.html │ │ │ ├── joystick-page.component.scss │ │ │ └── joystick-page.component.ts │ │ ├── light-page │ │ │ ├── light-page.component.html │ │ │ ├── light-page.component.scss │ │ │ └── light-page.component.ts │ │ ├── page.scss │ │ ├── ppt-page │ │ │ ├── ppt-page.component.html │ │ │ ├── ppt-page.component.scss │ │ │ └── ppt-page.component.ts │ │ └── serial │ │ │ ├── serial.component.html │ │ │ ├── serial.component.scss │ │ │ └── serial.component.ts │ ├── pipes │ │ └── html.pipe.ts │ ├── serial.service.ts │ └── wifi.service.ts ├── assets │ ├── .gitkeep │ ├── diandeng-logo.png │ ├── iconfont │ │ ├── demo.css │ │ ├── demo_index.html │ │ ├── iconfont.css │ │ ├── iconfont.js │ │ ├── iconfont.json │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png │ ├── img │ │ ├── colorpicker.webp │ │ ├── environment-bg.webp │ │ ├── health-bg.webp │ │ ├── industry-bg.webp │ │ ├── joyout.webp │ │ ├── joystick.webp │ │ ├── js-btn.webp │ │ ├── js-btn2.webp │ │ ├── smartfarming-bg.webp │ │ ├── smarthome-bg.webp │ │ └── weatherstation-bg.webp │ ├── iphone.webp │ └── oj-logo.png ├── favicon.ico ├── index.html ├── main.ts ├── manifest.webmanifest └── styles.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: CI&&CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - deploy 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | - name: Use Node.js 18.x 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '18.x' 19 | 20 | - name: Dependent environment 21 | run: | 22 | npm i -g @angular/cli 23 | npm i 24 | 25 | - name: Compile 26 | run: | 27 | npm run build 28 | 29 | - name: Deploy 30 | uses: garygrossgarten/github-action-scp@v0.5.3 31 | with: 32 | local: dist/web-ble 33 | remote: ${{ secrets.DEPLOY_DIR }} 34 | host: ${{ secrets.DEPLOY_HOST }} 35 | username: ${{ secrets.DEPLOY_USER }} 36 | privateKey: ${{ secrets.DEPLOY_SECRET }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | gulpfile.js 44 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 蓝牙BLE遥控Arduino设备 2 | 注意:本软件需要使用Edge或Chrome等现代浏览器方可正常使用 3 | 4 | ## PC端 5 | 访问 [https://ble.openjumper.com](https://ble.openjumper.com) 即可使用 6 | 需要使用Edge或Chrome访问 7 | 需要PC具备蓝牙功能,如果不具备蓝牙功能,可以通过usb蓝牙适配器扩展 8 | 9 | ## 移动端 10 | 访问 [https://ble.openjumper.com](https://ble.openjumper.com) 即可使用 11 | 仅支持android手机使用,如自带浏览器无法正常使用,请安装Edge或Chrome浏览器 12 | 13 | ## 设备支持 14 | 15 | 市面上的大部分BLE串口透传模块均可使用。 16 | 已测试可用模块:HC-42、JDY-18 17 | 已知不可用的模块型号:JDY-10 18 | 19 | ## 使用说明 20 | 21 | 点击连接设备后会搜索附近的设备,选择你要连接的设备,然后选择对应的控制界面。下面对每个界面的通信指令进行说明: 22 | 本软件中通信指令统一格式为: 23 | 24 | ``` 25 | key:value\n 26 | ``` 27 | 28 | 每条指令都以换行符\\n结束 29 | 30 | ### led控制界面 31 | 32 | | 组件 | key | value | 指令示例 | 33 | | --- | --- | ----- | ---- | 34 | | 取色器 | rgb | 255,255,255 | rgb:0,55,200 | 35 | | 亮度调节 | brightness | 0 \~ 255 | brightness:100 | 36 | | 开关 | turn | on \| off | turn:on | 37 | 38 | 示例程序: 39 | 40 | ### 小车遥控 41 | 42 | | 组件 | key | value | 指令示例 | 43 | | --- | --- | ----- | ---- | 44 | | 摇杆 | joy | 255,255 | joy:125,125 | 45 | | 按键1 | B1 | press \| pressup | B1:pressup | 46 | | 按键2 | B2 | press \| pressup | B2:press | 47 | | 按键3 | B3 | press \| pressup | B3:pressup | 48 | | 按键4 | B4 | press \| pressup | B4:press | 49 | 50 | 示例程序: 51 | 52 | ### 智能家居 53 | 54 | | 组件 | key | value | 指令示例 | 55 | | --- | --- | ----- | ---- | 56 | | 温度 | temperature | 数值 | temperature:123 | 57 | | 湿度 | humidity | 数值 | humidity:123 | 58 | | 光照 | illuminance | 数值 | illuminance:press:123 | 59 | | PM2.5 | pressure | 数值 | pressure:123 | 60 | | 二氧化碳 | co2 | 数值 | co2:123 | 61 | | 窗户 | window | on \| off | window:on | 62 | | 通风 | fan | on \| off | fan:on | 63 | | 补光灯 | light | on \| off | light:on | 64 | | 开门 | door | on \| off | door:on | 65 | | 音乐 | music | on \| off | music:on | 66 | | 其他 | custom | on \| off | custom:on | 67 | | 文本编辑 | text | 文本 | text:欢迎光临 | 68 | 69 | 示例程序: 70 | 71 | ### 智慧农场 72 | 73 | | 组件 | key | value | 指令示例 | 74 | | --- | --- | ----- | ---- | 75 | | 温度 | temperature | 数值 | temperature:100 | 76 | | 湿度 | humidity | 数值 | humidity:100 | 77 | | 光照 | illuminance | 数值 | light:100 | 78 | | 气压 | pressure | 数值 | pressure:100 | 79 | | 风向 | windDirection | 数值 | windDirection:100 | 80 | | 风速 | windSpeed | 数值 | windSpeed:100 | 81 | | 水质 | waterQuality | 数值 | waterQuality:100 | 82 | | 降雨量 | rainfall | 数值 | rainfall:100 | 83 | | 灌溉 | irrigation | on \| off | irrigation:on | 84 | | 通风 | fan | on \| off | fan:on | 85 | | 补光灯 | light | on \| off | light:on | 86 | | 风车 | windmill | on \| off | windmill:on | 87 | | 其他功能 | custom | on \| off | custom:on | 88 | | 文本编辑 | text | 文本 | text:欢迎光临 | 89 | 90 | 示例程序: 91 | 92 | ## 源代码 93 | 94 | 见[GITHUB](https://github.com/coloz/web-ble) 95 | 96 | ## 赞助商 97 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "web-ble": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss", 11 | "skipTests": true 12 | }, 13 | "@schematics/angular:service": { 14 | "skipTests": true 15 | } 16 | }, 17 | "root": "", 18 | "sourceRoot": "src", 19 | "prefix": "app", 20 | "architect": { 21 | "build": { 22 | "builder": "@angular-devkit/build-angular:application", 23 | "options": { 24 | "outputPath": "dist/web-ble", 25 | "index": "src/index.html", 26 | "browser": "src/main.ts", 27 | "polyfills": [ 28 | "zone.js" 29 | ], 30 | "tsConfig": "tsconfig.app.json", 31 | "inlineStyleLanguage": "scss", 32 | "assets": [ 33 | "src/favicon.ico", 34 | "src/assets", 35 | "src/manifest.webmanifest" 36 | ], 37 | "styles": [ 38 | "src/styles.scss" 39 | ], 40 | "scripts": [ 41 | "node_modules/hammerjs/hammer.min.js" 42 | ] 43 | }, 44 | "configurations": { 45 | "production": { 46 | "budgets": [ 47 | { 48 | "type": "initial", 49 | "maximumWarning": "500kb", 50 | "maximumError": "1mb" 51 | }, 52 | { 53 | "type": "anyComponentStyle", 54 | "maximumWarning": "2kb", 55 | "maximumError": "4kb" 56 | } 57 | ], 58 | "outputHashing": "all", 59 | "serviceWorker": "ngsw-config.json" 60 | }, 61 | "development": { 62 | "optimization": false, 63 | "extractLicenses": false, 64 | "sourceMap": true 65 | } 66 | }, 67 | "defaultConfiguration": "production" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "configurations": { 72 | "production": { 73 | "buildTarget": "web-ble:build:production" 74 | }, 75 | "development": { 76 | "buildTarget": "web-ble:build:development" 77 | } 78 | }, 79 | "defaultConfiguration": "development" 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "buildTarget": "web-ble:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "polyfills": [ 91 | "zone.js", 92 | "zone.js/testing" 93 | ], 94 | "tsConfig": "tsconfig.spec.json", 95 | "inlineStyleLanguage": "scss", 96 | "assets": [ 97 | "src/favicon.ico", 98 | "src/assets", 99 | "src/manifest.webmanifest" 100 | ], 101 | "styles": [ 102 | "src/styles.scss" 103 | ], 104 | "scripts": [] 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | "cli": { 111 | "analytics": false 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/media/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-ble", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "bd":"ng build && gulp deploy", 11 | "d":"gulp deploy" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^17.0.0", 16 | "@angular/common": "^17.0.0", 17 | "@angular/compiler": "^17.0.0", 18 | "@angular/core": "^17.0.0", 19 | "@angular/forms": "^17.0.0", 20 | "@angular/platform-browser": "^17.0.0", 21 | "@angular/platform-browser-dynamic": "^17.0.0", 22 | "@angular/router": "^17.0.0", 23 | "@angular/service-worker": "^17.0.0", 24 | "@types/web-bluetooth": "^0.0.20", 25 | "hammerjs": "^2.0.8", 26 | "lightweight-charts": "^4.1.1", 27 | "rxjs": "~7.8.0", 28 | "tslib": "^2.3.0", 29 | "zone.js": "~0.14.2" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "^17.0.3", 33 | "@angular/cli": "^17.0.3", 34 | "@angular/compiler-cli": "^17.0.0", 35 | "@types/jasmine": "~5.1.0", 36 | "gulp": "^4.0.2", 37 | "jasmine-core": "~5.1.0", 38 | "karma": "~6.4.0", 39 | "karma-chrome-launcher": "~3.2.0", 40 | "karma-coverage": "~2.2.0", 41 | "karma-jasmine": "~5.1.0", 42 | "karma-jasmine-html-reporter": "~2.1.0", 43 | "typescript": "~5.2.2", 44 | "vinyl-ftp": "^0.6.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | 8 |
9 |
10 | 当前浏览器不支持蓝牙功能,请使用最新的Edge或Chrome浏览器访问 11 |
12 |
13 |
14 |
15 | 点击阅读软件使用说明 16 |
17 | 18 | arduino UNO R4 19 | 20 | 21 | arduino book 22 | 23 | 24 | oj 25 | 26 |
-------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .phone-box { 2 | display: none; 3 | } 4 | 5 | .content { 6 | height: 100%; 7 | } 8 | 9 | .mask { 10 | height: 100vh; 11 | width: 100vw; 12 | display:flex; 13 | justify-content: center; 14 | align-items: center; 15 | 16 | .warn-bg { 17 | background: #FFF; 18 | } 19 | } 20 | 21 | .add{ 22 | display: none; 23 | } 24 | 25 | 26 | @media screen and (min-width: 500px) { 27 | .phone-box { 28 | display: block; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | width: 450px; 33 | pointer-events: none; 34 | z-index: 9; 35 | 36 | img { 37 | width: 100%; 38 | } 39 | } 40 | 41 | .he { 42 | transform-origin: left center; 43 | transform: rotate(-90deg) translateY(50%); 44 | 45 | &.content { 46 | margin-left: 32px; 47 | margin-top: 41px; 48 | } 49 | } 50 | 51 | .content { 52 | margin-left: 34px; 53 | margin-top: 31px; 54 | background-color: #fafafa; 55 | border: 1px solid red; 56 | width: 307px; 57 | height: 663px; 58 | } 59 | 60 | .add{ 61 | position: fixed; 62 | top: 15px; 63 | left: 400px; 64 | display: flex; 65 | flex-direction: column; 66 | img{ 67 | width: 260px; 68 | } 69 | 70 | .text{ 71 | text-align: center; 72 | width: 100%; 73 | padding: 15px 0; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Injectable } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NavigationEnd, Router, RouterOutlet } from '@angular/router'; 4 | import { HammerModule } from '@angular/platform-browser'; 5 | import { BleService } from './ble.service'; 6 | import { DeviceService } from './device.service'; 7 | // import 'hammerjs'; 8 | // import { HAMMER_GESTURE_CONFIG, HammerGestureConfig, HammerModule } from '@angular/platform-browser'; 9 | 10 | // declare var Hammer: any; 11 | // @Injectable() 12 | // export class MyHammerConfig extends HammerGestureConfig { 13 | // override overrides = { 14 | // 'pan': { direction: Hammer.DIRECTION_ALL, threshold: 5 }, 15 | // 'press': { time: 300, threshold: 99 } 16 | // } 17 | // } 18 | 19 | const orientation = (screen.orientation as any); 20 | 21 | @Component({ 22 | selector: 'app-root', 23 | standalone: true, 24 | imports: [CommonModule, RouterOutlet, HammerModule], 25 | // providers: [ 26 | // { provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig }, 27 | // ], 28 | templateUrl: './app.component.html', 29 | styleUrl: './app.component.scss' 30 | }) 31 | export class AppComponent { 32 | title = 'web-ble'; 33 | 34 | isH = false; 35 | 36 | get browserVersionError() { 37 | return this.bleService.browserVersionError 38 | } 39 | 40 | constructor( 41 | private router: Router, 42 | private bleService: BleService, 43 | private deviceService: DeviceService 44 | ) { 45 | } 46 | 47 | ngOnInit(): void { 48 | this.bleService.init() 49 | this.deviceService.init() 50 | this.router.events.subscribe((event) => { 51 | if (event instanceof NavigationEnd) { 52 | // console.log('NavigationEnd:', event); 53 | if (event.url == '/joystick') { 54 | this.isH = true; 55 | } else { 56 | this.isH = false; 57 | } 58 | } 59 | }) 60 | 61 | window.onload = function () { 62 | document.addEventListener('contextmenu', function (e) { 63 | if (e.target['tagName'] === 'IMG') { 64 | e.preventDefault(); 65 | } 66 | }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom, isDevMode } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { provideServiceWorker } from '@angular/service-worker'; 6 | import { HammerModule } from '@angular/platform-browser'; 7 | 8 | export const appConfig: ApplicationConfig = { 9 | providers: [ 10 | provideRouter(routes), provideServiceWorker('ngsw-worker.js', { 11 | enabled: !isDevMode(), 12 | registrationStrategy: 'registerWhenStable:30000' 13 | }), 14 | importProvidersFrom(HammerModule) 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { path: '', redirectTo: 'connect', pathMatch: 'full' }, 5 | { path: 'connect', loadComponent: () => import('./pages/connect/connect.component').then(m => m.ConnectComponent) }, 6 | { path: 'light', loadComponent: () => import('./pages/light-page/light-page.component').then(m => m.LightPageComponent) }, 7 | { path: 'ppt', loadComponent: () => import('./pages/ppt-page/ppt-page.component').then(m => m.PptPageComponent) }, 8 | { path: 'joystick', loadComponent: () => import('./pages/joystick-page/joystick-page.component').then(m => m.JoystickPageComponent) }, 9 | { path: 'iot/:devicename', loadComponent: () => import('./pages/iot-page/iot-page.component').then(m => m.IotPageComponent) }, 10 | { path: 'serial', loadComponent: () => import('./pages/serial/serial.component').then(m => m.SerialComponent) }, 11 | // { path: 'smarthome', loadComponent: () => import('./pages/smarthome-page/smarthome-page.component').then(m => m.SmarthomePageComponent) }, 12 | // { path: 'farming', loadComponent: () => import('./pages/farming-page/farming-page.component').then(m => m.FarmingPageComponent) }, 13 | // { path: 'weather', loadComponent: () => import('./pages/weather-page/weather-page.component').then(m => m.WeatherPageComponent) }, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/app/ble.config.ts: -------------------------------------------------------------------------------- 1 | const blemList = [ 2 | { 3 | name: 'HM-10', 4 | service: '0000ffe0-0000-1000-8000-00805f9b34fb', 5 | read: '0000ffe1-0000-1000-8000-00805f9b34fb', 6 | write: '0000ffe1-0000-1000-8000-00805f9b34fb' 7 | }, 8 | { 9 | name: 'HM-11', 10 | service: '0000ffe0-0000-1000-8000-00805f9b34fb', 11 | read: '0000ffe1-0000-1000-8000-00805f9b34fb', 12 | write: '0000ffe1-0000-1000-8000-00805f9b34fb' 13 | }, 14 | { 15 | name: 'JDY-08', 16 | service: '0000ffe0-0000-1000-8000-00805f9b34fb', 17 | read: '0000ffe1-0000-1000-8000-00805f9b34fb', 18 | write: '0000ffe1-0000-1000-8000-00805f9b34fb' 19 | }, 20 | { 21 | name: 'JDY-10', 22 | service: '0000ffe0-0000-1000-8000-00805f9b34fb', 23 | read: '0000ffe1-0000-1000-8000-00805f9b34fb', 24 | write: '0000ffe1-0000-1000-8000-00805f9b34fb' 25 | }, 26 | { 27 | name: 'JDY-18', 28 | service: '0000ffe0-0000-1000-8000-00805f9b34fb', 29 | read: '0000ffe1-0000-1000-8000-00805f9b34fb', 30 | write: '0000ffe1-0000-1000-8000-00805f9b34fb' 31 | }, 32 | { 33 | name: 'JDY-09', 34 | service: '0000ffe0-0000-1000-8000-00805f9b34fb', 35 | read: '0000ffe1-0000-1000-8000-00805f9b34fb', 36 | write: '0000ffe1-0000-1000-8000-00805f9b34fb' 37 | }, 38 | { 39 | name: 'ai-thinker-PB-03', 40 | service: '55535343-fe7d-4ae5-8fa9-9fafd205e455', 41 | read: '49535343-1e4d-4bd9-ba61-23c647249616', 42 | write: '49535343-8841-43f4-a8d4-ecbe34729bb3' 43 | } 44 | ] -------------------------------------------------------------------------------- /src/app/ble.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { DataService } from './data.service'; 4 | 5 | const bluetooth = (navigator as any).bluetooth; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class BleService { 11 | 12 | device = null; 13 | server = null; 14 | characteristicInstance = null; 15 | 16 | serviceUUID = '0000ffe0-0000-1000-8000-00805f9b34fb' 17 | characteristicUUID = '0000ffe1-0000-1000-8000-00805f9b34fb' 18 | 19 | // characteristicUUID_read = '0000ffe1-0000-1000-8000-00805f9b34fb' 20 | // characteristicUUID_write = '0000ffe1-0000-1000-8000-00805f9b34fb' 21 | 22 | dataChanged = new Subject() 23 | 24 | constructor( 25 | private dataService: DataService 26 | ) { } 27 | 28 | browserVersionError = false 29 | 30 | init() { 31 | if (!bluetooth) { 32 | console.log('不支持的浏览器'); 33 | } 34 | } 35 | 36 | searchDevice() { 37 | bluetooth.requestDevice({ 38 | // acceptAllDevices: true, 39 | optionalServices: [this.serviceUUID], 40 | filters: [{ services: [this.serviceUUID] }] 41 | }) 42 | .then(device => { 43 | this.device = device; 44 | // console.log(this.device); 45 | // console.log(this.device.name); 46 | return device.gatt.connect(); 47 | }) 48 | .then(server => { 49 | this.server = server; 50 | this.device.addEventListener('gattserverdisconnected', (event) => this.onDisconnected(event)); 51 | return server.getPrimaryService(this.serviceUUID); 52 | }) 53 | .then(service => { 54 | console.log(service); 55 | 56 | return service.getCharacteristic(this.characteristicUUID); 57 | }) 58 | .then(characteristic => { 59 | // console.log(characteristic); 60 | this.characteristicInstance = characteristic; 61 | // console.log(this.characteristicInstance); 62 | this.characteristicInstance.addEventListener('characteristicvaluechanged', (event) => this.handleCharacteristicValueChanged(event)); 63 | this.characteristicInstance.startNotifications(); 64 | }) 65 | .catch(error => { 66 | console.log(error); 67 | // this.device = null 68 | }); 69 | } 70 | 71 | sendData(data) { 72 | if (!this.characteristicInstance) { 73 | console.log('No characteristic to write to!'); 74 | return; 75 | } 76 | let encoder = new TextEncoder(); 77 | this.characteristicInstance.writeValue(encoder.encode(data)) 78 | .then(() => { 79 | // console.log(`Data sent: ${data}`); 80 | }) 81 | .catch(error => { 82 | // console.log(`Send error: ${error}`); 83 | }); 84 | } 85 | 86 | tempData = ''; 87 | handleCharacteristicValueChanged(event) { 88 | let value = event.target.value; 89 | let decoder = new TextDecoder(); 90 | let decodedValue = decoder.decode(value); 91 | console.log(`Received data: ${decodedValue}`); 92 | 93 | this.tempData += decodedValue; 94 | 95 | try { 96 | // 检查是否存在'\n' 97 | let lastIndex = this.tempData.lastIndexOf('\n'); 98 | if (lastIndex !== -1) { 99 | // 获取最后一个'\n'前的数据并处理 100 | let processData = this.tempData.substring(0, lastIndex); 101 | this.dataService.processData(processData); 102 | 103 | // 保留'\n'后的数据 104 | this.tempData = this.tempData.substring(lastIndex + 1); 105 | } 106 | } catch (error) { 107 | 108 | } 109 | this.dataChanged.next(decodedValue); 110 | } 111 | 112 | onDisconnected(event) { 113 | console.log(`Device ${this.device.name} is disconnected.`); 114 | this.device = null; 115 | } 116 | 117 | disconnect() { 118 | if (this.device) { 119 | console.log(`Disconnecting from ${this.device.name}...`); 120 | if (this.device.gatt.connected) { 121 | this.device.gatt.disconnect(); 122 | console.log(`Device ${this.device.name} is disconnected.`); 123 | } else { 124 | console.log(`Device ${this.device.name} is already disconnected.`); 125 | } 126 | this.device = null; 127 | } else { 128 | console.log('No device is connected.'); 129 | } 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /src/app/components/btn-box/btn-box.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
{{config.name}}
9 |
10 | 点击执行 11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
-------------------------------------------------------------------------------- /src/app/components/btn-box/btn-box.component.scss: -------------------------------------------------------------------------------- 1 | @import "../ctrl-box/ctrl-box.component.scss"; 2 | 3 | .ctrl-box { 4 | &:active { 5 | .on { 6 | transform: scale(0.9); 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/app/components/btn-box/btn-box.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ToggleComponent } from '../toggle/toggle.component'; 4 | 5 | @Component({ 6 | selector: 'btn-box', 7 | standalone: true, 8 | imports: [CommonModule, ToggleComponent], 9 | templateUrl: './btn-box.component.html', 10 | styleUrl: './btn-box.component.scss' 11 | }) 12 | export class BtnBoxComponent { 13 | @Input() config; 14 | // @Output() stateChange = new EventEmitter(); 15 | 16 | get state() { 17 | return this.config.state; 18 | } 19 | 20 | // trunCtrlItem() { 21 | // this.config.state = !this.config.state; 22 | // // this.stateChange.emit(this.config.state); 23 | // } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/components/color-picker/colorpicker.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
6 |
7 |
-------------------------------------------------------------------------------- /src/app/components/color-picker/colorpicker.scss: -------------------------------------------------------------------------------- 1 | .colorpicker { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | box-shadow: 0px 3px 11px 0px rgba(0,0,0,0.11); 6 | border-radius: 50%; 7 | 8 | canvas { 9 | height: 100%; 10 | width: 100%; 11 | } 12 | 13 | .knob { 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | width: 40px; 18 | height: 40px; 19 | border-radius: 50%; 20 | background: transparent; 21 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 22 | border: 2px solid #FFF; 23 | transition: opacity 1s; 24 | opacity: 0; 25 | } 26 | 27 | .pickerbox { 28 | position: absolute; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | border-radius: 50%; 33 | top: 0; 34 | width: 100%; 35 | height: 100%; 36 | 37 | .picker-inner { 38 | width: 30%; 39 | height: 30%; 40 | border-radius: 50%; 41 | } 42 | } 43 | } 44 | .picker-inner{ 45 | height: 15px; 46 | width: 40px; 47 | } -------------------------------------------------------------------------------- /src/app/components/color-picker/colorpicker.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | Component, 4 | ViewChild, 5 | ElementRef, 6 | Renderer2, 7 | ChangeDetectorRef, 8 | Input, 9 | Output, 10 | EventEmitter, 11 | Injectable 12 | } from '@angular/core'; 13 | import { FormsModule } from '@angular/forms'; 14 | import { HAMMER_GESTURE_CONFIG, HammerGestureConfig, HammerModule } from '@angular/platform-browser'; 15 | 16 | declare var Hammer: any; 17 | 18 | @Injectable() 19 | export class MyHammerConfig extends HammerGestureConfig { 20 | override overrides = { 21 | 'pan': { direction: Hammer.DIRECTION_ALL, threshold: 5 }, 22 | 'press': { time: 300, threshold: 99 } 23 | } 24 | } 25 | 26 | @Component({ 27 | selector: 'color-picker', 28 | templateUrl: 'colorpicker.html', 29 | styleUrls: ['colorpicker.scss'], 30 | imports: [CommonModule, HammerModule, FormsModule], 31 | providers: [ 32 | { provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig }, 33 | 34 | ], 35 | standalone: true 36 | }) 37 | export class ColorPickerComponent { 38 | 39 | @Output() colorChange = new EventEmitter(); 40 | @Output() sendData = new EventEmitter(); 41 | 42 | _lstyle = 0; 43 | @Input() 44 | set lstyle(lstyle) { 45 | if (this.loaded) 46 | this.loadColorImg(); 47 | this._lstyle = lstyle; 48 | } 49 | get lstyle() { 50 | return this._lstyle 51 | } 52 | 53 | _color; 54 | @Input() 55 | set color(color) { 56 | // this.renderer.setStyle(this.picker.nativeElement, 'background-color', color); 57 | this.rgbStr = color; 58 | this._color = color; 59 | } 60 | get color() { 61 | return this._color; 62 | } 63 | brightness; 64 | 65 | gesture; 66 | loaded = false; 67 | 68 | // @ViewChild('picker', { read: ElementRef, static: true }) picker: ElementRef; 69 | @ViewChild('pickerbox', { read: ElementRef, static: true }) pickerbox: ElementRef; 70 | @ViewChild('knob', { read: ElementRef, static: true }) knob: ElementRef; 71 | 72 | value = 0; 73 | 74 | imgsize = 2; 75 | 76 | constructor( 77 | private renderer: Renderer2, 78 | public changeDetectorRef: ChangeDetectorRef, 79 | ) { 80 | } 81 | 82 | ngAfterViewInit() { 83 | this.loadColorImg(); 84 | this.loaded = true; 85 | } 86 | 87 | processData(data) { 88 | if (typeof data != "undefined") { 89 | if (data instanceof Array) { 90 | if (data.length < 3) return; 91 | let col = data.length == 3 ? 'rgb(' : 'rgba('; 92 | for (var i = 0; i < 3; i++) { 93 | col = col + `${data[i].toString()},` 94 | } 95 | if (data.length == 3) { 96 | col = col.substr(0, col.length) + ")"; 97 | } else { 98 | col = col + `${(data[3] / 255).toString()})`; 99 | this.brightness = data[3]; 100 | this.value = data[3]; 101 | } 102 | // console.log("获得颜色:" + col); 103 | this.color = col; 104 | } 105 | } 106 | } 107 | 108 | getKnob(e) { 109 | let rect = this.pickerbox.nativeElement.getBoundingClientRect(); 110 | let r = (rect.right - rect.left) / 2; 111 | let centerX = rect.left + r; 112 | let centerY = rect.top + r; 113 | 114 | let x = e.center.x; 115 | let y = e.center.y; 116 | 117 | let x1 = x - rect.left; 118 | let y1 = y - rect.top; 119 | 120 | let currentR = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)) 121 | let z = r / currentR * 0.90 122 | 123 | if (currentR > r) { 124 | x1 = (x - centerX) * z + centerX - rect.left; 125 | y1 = (y - centerY) * z + centerY - rect.top; 126 | } 127 | this.renderer.setStyle(this.knob.nativeElement, 'left', `${(x1 - 20).toString()}px`); 128 | this.renderer.setStyle(this.knob.nativeElement, 'top', `${(y1 - 20).toString()}px`); 129 | this.pick(x1, y1); 130 | } 131 | 132 | tapEvent(e) { 133 | this.getKnob(e); 134 | this.sendDataAtEnd(); 135 | } 136 | 137 | panstartEvent(e) { 138 | this.renderer.setStyle(this.knob.nativeElement, 'opacity', '1') 139 | this.getKnob(e); 140 | } 141 | 142 | panmoveEvent(e) { 143 | this.getKnob(e); 144 | } 145 | 146 | panendEvent(e) { 147 | this.renderer.setStyle(this.knob.nativeElement, 'opacity', '0') 148 | this.getKnob(e); 149 | this.sendDataAtEnd(); 150 | } 151 | 152 | context; 153 | image; 154 | @ViewChild("myCanvas", { read: ElementRef, static: true }) myCanvas; 155 | length; 156 | loadColorImg() { 157 | this.context = this.myCanvas.nativeElement.getContext("2d"); 158 | this.image = new Image(); 159 | this.image.src = `assets/img/colorpicker.webp`; 160 | this.image.onload = () => { 161 | window.setTimeout(() => { 162 | this.length = this.pickerbox.nativeElement.clientHeight; 163 | this.renderer.setAttribute(this.myCanvas.nativeElement, 'width', `${this.length * this.imgsize}`); 164 | this.renderer.setAttribute(this.myCanvas.nativeElement, 'height', `${this.length * this.imgsize}`); 165 | this.renderer.setStyle(this.knob.nativeElement, 'top', `${this.length / 2 - 20}px`) 166 | this.renderer.setStyle(this.knob.nativeElement, 'left', `${this.length / 2 - 20}px`) 167 | this.context.drawImage(this.image, 0, 0, this.length * this.imgsize, this.length * this.imgsize); 168 | }, 50); 169 | 170 | } 171 | } 172 | 173 | lastSendColor = ''; 174 | rgb = [255, 255, 255]; 175 | rgbStr = '#e6e6e6'; 176 | pick(x, y) { 177 | let temp = this.context.getImageData(x, y, 1, 1).data; 178 | this.rgb = [temp[0], temp[1], temp[2]]; 179 | let rgbString = this.rgb.toString(); 180 | if (rgbString != this.lastSendColor) { 181 | this.lastSendColor = rgbString; 182 | this.rgbStr = `rgb(${this.rgb[0]},${this.rgb[1]},${this.rgb[2]})` 183 | this.changeDetectorRef.detectChanges(); 184 | this.pickend(); 185 | } 186 | } 187 | 188 | canSend = false; 189 | pickend() { 190 | this.colorChange.emit(this.rgb); 191 | } 192 | 193 | sendDataAtEnd() { 194 | this.colorChange.emit(this.rgb); 195 | this.sendData.emit(this.rgb); 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /src/app/components/ctrl-box/ctrl-box.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
{{config.name}}
9 |
10 | 已开启 11 | 已关闭 12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 |
-------------------------------------------------------------------------------- /src/app/components/ctrl-box/ctrl-box.component.scss: -------------------------------------------------------------------------------- 1 | .ctrl-box { 2 | padding: 10px; 3 | } 4 | 5 | .icon-box { 6 | width: 44px; 7 | height: 44px; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | background: #D1D6E2; 12 | border-radius: 50%; 13 | transition: all 0.3s; 14 | 15 | i { 16 | font-size: 20px; 17 | color: #FFF; 18 | } 19 | 20 | &.on { 21 | background: #306CFF; 22 | } 23 | } 24 | 25 | .toggle { 26 | position: absolute; 27 | bottom: 10px; 28 | right: 10px; 29 | transform: scale(0.8); 30 | } 31 | 32 | .text-box { 33 | margin-top: 3px; 34 | width: 44px; 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | position: relative; 39 | 40 | .name { 41 | font-size: 13px; 42 | white-space: nowrap; 43 | } 44 | 45 | .state { 46 | margin-top: 2px; 47 | font-size: 10px; 48 | color: #999999; 49 | } 50 | } 51 | 52 | .toggle { 53 | i { 54 | color: #389bee; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/components/ctrl-box/ctrl-box.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ToggleComponent } from '../toggle/toggle.component'; 4 | 5 | @Component({ 6 | selector: 'ctrl-box', 7 | standalone: true, 8 | imports: [CommonModule, ToggleComponent], 9 | templateUrl: './ctrl-box.component.html', 10 | styleUrl: './ctrl-box.component.scss' 11 | }) 12 | export class CtrlBoxComponent { 13 | @Input() config; 14 | @Output() stateChange = new EventEmitter(); 15 | 16 | get state() { 17 | return this.config.state; 18 | } 19 | 20 | trunCtrlItem() { 21 | this.config.state = !this.config.state; 22 | this.stateChange.emit(this.config.state); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/components/data-box/data-box.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
{{value}} {{config.unit}}
7 |
{{config.name}}
8 |
9 |
-------------------------------------------------------------------------------- /src/app/components/data-box/data-box.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | display: block; 6 | } 7 | 8 | .data-box { 9 | position: relative; 10 | width: calc(100% - 20px); 11 | height: calc(100% - 20px); 12 | border-radius: 10px; 13 | padding: 10px; 14 | display: flex; 15 | align-items: center; 16 | // justify-content: space-between; 17 | flex-wrap: nowrap; 18 | 19 | .icon-box { 20 | color: #306CFF; 21 | margin-right: 1vw; 22 | 23 | i { 24 | font-size: 28px; 25 | } 26 | } 27 | 28 | .text-box { 29 | display: flex; 30 | flex-direction: column; 31 | align-items: flex-start; 32 | color: #b9b9b9; 33 | font-size: 12px; 34 | white-space: nowrap; 35 | 36 | .value { 37 | color: #306CFF; 38 | font-size: 20px; 39 | font-weight: 700; 40 | 41 | span { 42 | font-weight: 500; 43 | font-size: 12px; 44 | color: #b9b9b9; 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/app/components/data-box/data-box.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { DataService } from '../../data.service'; 4 | 5 | @Component({ 6 | selector: 'data-box', 7 | standalone: true, 8 | imports: [CommonModule], 9 | templateUrl: './data-box.component.html', 10 | styleUrl: './data-box.component.scss' 11 | }) 12 | export class DataBoxComponent { 13 | @Input() config; 14 | 15 | get value() { 16 | if (this.config.key in this.dataService.manager) { 17 | return this.dataService.manager[this.config.key] 18 | } 19 | return '-' 20 | } 21 | 22 | constructor( 23 | public dataService: DataService 24 | ) {} 25 | } 26 | -------------------------------------------------------------------------------- /src/app/components/device-title/device-title.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 已连接 7 | 点击连接设备 8 |
9 |
-------------------------------------------------------------------------------- /src/app/components/device-title/device-title.component.scss: -------------------------------------------------------------------------------- 1 | .device-title { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | 7 | .name { 8 | font-size: 16px; 9 | font-weight: 700; 10 | color:#595959; 11 | position: relative; 12 | text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.5); 13 | } 14 | 15 | .state { 16 | font-size: 12px; 17 | font-weight: 400; 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | justify-content: center; 22 | transition: height 0.5s, opacity 1s; 23 | opacity: 0.6; 24 | text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.5); 25 | } 26 | } -------------------------------------------------------------------------------- /src/app/components/device-title/device-title.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BleService } from '../../ble.service'; 4 | 5 | @Component({ 6 | selector: 'device-title', 7 | standalone: true, 8 | imports: [CommonModule], 9 | templateUrl: './device-title.component.html', 10 | styleUrl: './device-title.component.scss' 11 | }) 12 | export class DeviceTitleComponent { 13 | 14 | get device() { 15 | return this.bleService.device 16 | } 17 | 18 | constructor( 19 | private bleService: BleService 20 | ) { 21 | 22 | } 23 | 24 | connect() { 25 | if (!this.device) 26 | this.bleService.searchDevice() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/input-box/input-box.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
{{config.name}}
9 |
10 | 点击编辑 11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
-------------------------------------------------------------------------------- /src/app/components/input-box/input-box.component.scss: -------------------------------------------------------------------------------- 1 | @import "../ctrl-box/ctrl-box.component.scss"; -------------------------------------------------------------------------------- /src/app/components/input-box/input-box.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @Component({ 5 | selector: 'input-box', 6 | standalone: true, 7 | imports: [CommonModule], 8 | templateUrl: './input-box.component.html', 9 | styleUrl: './input-box.component.scss' 10 | }) 11 | export class InputBoxComponent { 12 | @Input() config; 13 | @Output() textChange = new EventEmitter() 14 | 15 | text = "Hello" 16 | 17 | editText() { 18 | const userInput = window.prompt('请输入要显示的字符',this.text); 19 | if (userInput !== null) { 20 | this.text = userInput; 21 | this.textChange.emit(this.text); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/components/line-chart/line-chart.component.html: -------------------------------------------------------------------------------- 1 |
暂无数据
2 |
-------------------------------------------------------------------------------- /src/app/components/line-chart/line-chart.component.scss: -------------------------------------------------------------------------------- 1 | .chart-box{ 2 | height: 150px; 3 | width: calc(100% - 15px); 4 | border-radius: 10px; 5 | overflow: hidden; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | color: #ccc; 10 | font-size: 12px; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/components/line-chart/line-chart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, SimpleChanges, ViewChild } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { createChart, IChartApi, ISeriesApi, LineSeriesPartialOptions, UTCTimestamp } from 'lightweight-charts'; 4 | import { DataService } from '../../data.service'; 5 | 6 | @Component({ 7 | selector: 'line-chart', 8 | standalone: true, 9 | imports: [CommonModule], 10 | templateUrl: './line-chart.component.html', 11 | styleUrl: './line-chart.component.scss' 12 | }) 13 | export class LineChartComponent { 14 | @ViewChild('chart') chartContainer: ElementRef; 15 | private chart: IChartApi; 16 | private lineSeries: ISeriesApi<"Line">; 17 | // private data: { time: UTCTimestamp, value: number }[] = []; 18 | private intervalTimer: any; 19 | 20 | 21 | @Input() config; 22 | 23 | get value() { 24 | if (!this.config) return null; 25 | if (this.config.key in this.dataService.manager) { 26 | return this.dataService.manager[this.config.key] 27 | } 28 | return null 29 | } 30 | 31 | constructor( 32 | public dataService: DataService 33 | ) { } 34 | 35 | ngAfterViewInit(): void { 36 | // this.darwChart() 37 | } 38 | 39 | ngOnDestroy(): void { 40 | if (this.intervalTimer) { 41 | clearInterval(this.intervalTimer); 42 | } 43 | if (this.chart) { 44 | this.chart.remove(); 45 | } 46 | } 47 | 48 | ngOnChanges(changes: SimpleChanges): void { 49 | if (changes['config']) { 50 | // this.darwChart() 51 | // 清空数据,重新记录 52 | } 53 | } 54 | 55 | initd = false; 56 | ngDoCheck() { 57 | if (this.value !== null && !this.initd) { 58 | this.initd = true; 59 | setTimeout(() => { 60 | this.darwChart(); 61 | }, 500) 62 | } 63 | } 64 | 65 | darwChart() { 66 | console.log('darwChart'); 67 | if (this.chart) { 68 | this.chart.remove(); 69 | } 70 | this.chart = createChart(this.chartContainer.nativeElement, { 71 | width: this.chartContainer.nativeElement.offsetWidth, 72 | layout: { 73 | textColor: 'rgba(33, 56, 77, 1)', 74 | }, 75 | grid: { 76 | vertLines: { 77 | color: 'rgba(197, 203, 206, 0.7)', 78 | }, 79 | horzLines: { 80 | color: 'rgba(197, 203, 206, 0.7)', 81 | }, 82 | }, 83 | timeScale: { 84 | timeVisible: true, 85 | secondsVisible: false, 86 | }, 87 | }); 88 | const lineSeriesOptions: LineSeriesPartialOptions = {}; 89 | this.lineSeries = this.chart.addLineSeries(lineSeriesOptions); 90 | 91 | this.intervalTimer = setInterval(() => this.updateData(), 1000); 92 | console.log(this.intervalTimer); 93 | } 94 | 95 | // oldValue; 96 | updateData() { 97 | let data = this.dataService.managerSeries[this.config.key] 98 | if (data.length == 0) 99 | this.lineSeries.setData([]); 100 | else { 101 | const localTime = new Date(); 102 | const time: UTCTimestamp = Math.floor(localTime.getTime() / 1000) - localTime.getTimezoneOffset() * 60 as UTCTimestamp; 103 | if (time - data[data.length - 1].time > 1) { 104 | data.push({ time, value: data[data.length - 1].value }); 105 | } 106 | try { 107 | this.lineSeries.setData(data); 108 | } catch (e) { 109 | console.log(e); 110 | console.log(data); 111 | } 112 | } 113 | 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/app/components/setting-btn/setting-btn.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/components/setting-btn/setting-btn.component.scss: -------------------------------------------------------------------------------- 1 | .setting-btn { 2 | position: absolute; 3 | right: 0; 4 | top: 5px; 5 | margin: 10px; 6 | width: 50px; 7 | height: 50px; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | font-size: 26px; 12 | color: #969696; 13 | cursor: pointer; 14 | z-index: 9999; 15 | } 16 | 17 | .menu { 18 | width: 50px; 19 | position: absolute; 20 | right: 0; 21 | top: 50px; 22 | margin: 10px; 23 | box-shadow: 0 0 10px #CCC; 24 | border-radius: 10px; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-direction: column; 29 | z-index: 9999; 30 | background: #FFF; 31 | 32 | >div { 33 | width: 42px; 34 | height: 50px; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | font-size: 26px; 39 | border-bottom: 1px solid #CCC; 40 | cursor: pointer; 41 | } 42 | 43 | .active { 44 | i { 45 | color: #389bee; 46 | } 47 | 48 | } 49 | } 50 | 51 | .he { 52 | i { 53 | transform: rotate(90deg); 54 | } 55 | } 56 | 57 | 58 | i { 59 | color: #404040; 60 | font-size: 30px; 61 | } -------------------------------------------------------------------------------- /src/app/components/setting-btn/setting-btn.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Router, RouterModule } from '@angular/router'; 4 | import { MENU } from '../../configs/app.config'; 5 | 6 | @Component({ 7 | selector: 'setting-btn', 8 | standalone: true, 9 | imports: [CommonModule, RouterModule], 10 | templateUrl: './setting-btn.component.html', 11 | styleUrl: './setting-btn.component.scss' 12 | }) 13 | export class SettingBtnComponent { 14 | 15 | hideMenu = true; 16 | isH = false; 17 | 18 | MENU = MENU 19 | 20 | constructor( 21 | private router: Router 22 | ) { 23 | } 24 | 25 | ngOnInit(): void { 26 | window.addEventListener("orientationchange", () => { 27 | if (window.orientation === 0) { // Portrait 28 | console.log("Portrait mode"); 29 | this.isH = false; 30 | } else if (window.orientation === 90 || window.orientation === -90) { // Landscape 31 | console.log("Landscape mode"); 32 | this.isH = true; 33 | } 34 | }, false); 35 | if (this.router.url === '/joystick') { 36 | this.isH = true; 37 | } 38 | } 39 | 40 | turnMenu() { 41 | this.hideMenu = !this.hideMenu; 42 | } 43 | 44 | onClick() { 45 | this.hideMenu = false 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/components/toggle/toggle.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
-------------------------------------------------------------------------------- /src/app/components/toggle/toggle.component.scss: -------------------------------------------------------------------------------- 1 | .toggle-box { 2 | position: relative; 3 | // border: 1px solid #959595; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | background-color: #E0E0E0; 8 | border-radius: 13.5px; 9 | width: 45px; 10 | height: 25px; 11 | transition: all 0.3s; 12 | 13 | .toggle-inner { 14 | border-radius: 50%; 15 | background: #fff; 16 | height: 21px; 17 | width: 21px; 18 | position: absolute; 19 | transition: all 0.3s; 20 | } 21 | 22 | .on { 23 | right: 2px; 24 | } 25 | 26 | .off { 27 | right: calc(100% - 23px); 28 | } 29 | } -------------------------------------------------------------------------------- /src/app/components/toggle/toggle.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, OnInit, Input, Output, EventEmitter, HostListener } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'toggle', 6 | templateUrl: './toggle.component.html', 7 | styleUrls: ['./toggle.component.scss'], 8 | imports: [CommonModule], 9 | standalone: true 10 | }) 11 | export class ToggleComponent implements OnInit { 12 | 13 | @Input() color = "#389bee"; 14 | @Input() state: any = true 15 | @Output() stateChange = new EventEmitter() 16 | 17 | constructor() { } 18 | 19 | ngOnInit() { } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/widget-joystick/widget-joystick.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
-------------------------------------------------------------------------------- /src/app/components/widget-joystick/widget-joystick.scss: -------------------------------------------------------------------------------- 1 | .joyout { 2 | position: absolute; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | height: 100%; 7 | width: 100%; 8 | img{ 9 | height: 100%; 10 | width: 100%; 11 | } 12 | } 13 | 14 | .stick { 15 | position: absolute; 16 | height: 70px; 17 | width: 70px; 18 | background: rgba(56, 156, 238, 0.8); 19 | border-radius: 50%; 20 | top: calc(50% - 35px); 21 | left: calc(50% - 35px); 22 | } 23 | 24 | .touch { 25 | height: 100%; 26 | width: 100%; 27 | border-radius: 50%; 28 | z-index: 1; 29 | } 30 | 31 | .joystick-box { 32 | height: 100%; 33 | width: 100%; 34 | position: relative; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/components/widget-joystick/widget-joystick.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Renderer2, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core'; 3 | 4 | 5 | @Component({ 6 | selector: 'widget-joystick', 7 | templateUrl: 'widget-joystick.html', 8 | styleUrls: ['widget-joystick.scss'], 9 | imports: [CommonModule], 10 | standalone: true 11 | }) 12 | export class WidgetJoystickComponent { 13 | @ViewChild("touchZone", { read: ElementRef, static: true }) touchZone: ElementRef; 14 | @ViewChild("stick", { read: ElementRef, static: true }) stick: ElementRef; 15 | 16 | @Output() valueChange = new EventEmitter(); 17 | 18 | constructor( 19 | private renderer: Renderer2 20 | ) { 21 | } 22 | 23 | oldX; 24 | oldY; 25 | lastSendTime; 26 | panmove(event) { 27 | let _touchZone = this.touchZone.nativeElement.getBoundingClientRect(); 28 | let _stick = this.stick.nativeElement.getBoundingClientRect(); 29 | let r = (_touchZone.width - _stick.width) / 2; 30 | let x = event.deltaX; 31 | let y = event.deltaY; 32 | let resize = r / Math.sqrt((x * x + y * y)); 33 | if (resize > 1) resize = 1; 34 | let l = r + resize * x; 35 | let t = r + resize * y; 36 | x = Math.round((l / r) * 127.5); 37 | y = Math.round((t / r) * 127.5); 38 | if (this.oldX != x || this.oldY != y) { 39 | this.renderer.setStyle(this.stick.nativeElement, 'left', `${(l).toString()}px`); 40 | this.renderer.setStyle(this.stick.nativeElement, 'top', `${(t).toString()}px`); 41 | this.senddata = `${x},${y}`; 42 | // this.sendData(senddata); 43 | this.oldX = x; 44 | this.oldY = y; 45 | } 46 | } 47 | 48 | panend(event) { 49 | this.renderer.setStyle(this.stick.nativeElement, 'left', `calc(50% - 35px)`); 50 | this.renderer.setStyle(this.stick.nativeElement, 'top', `calc(50% - 35px)`); 51 | this.senddata = `128,128`; 52 | this.stopSend() 53 | } 54 | 55 | timer; 56 | canSend = true; 57 | senddata = `128,128` 58 | 59 | panstart(event) { 60 | this.timer = setInterval(() => { 61 | this.valueChange.emit(this.senddata) 62 | }, 100) 63 | } 64 | 65 | stopSend() { 66 | this.valueChange.emit(this.senddata) 67 | clearInterval(this.timer) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/app/components/widget-range/widget-range.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
{{value}}
6 | 7 |
8 |
9 |
10 |
{{value}}
11 | 12 |
13 |
14 |
15 |
17 |
18 |
-------------------------------------------------------------------------------- /src/app/components/widget-range/widget-range.scss: -------------------------------------------------------------------------------- 1 | .range-box { 2 | position: relative; 3 | height: 7vh; 4 | max-height: 60px; 5 | width: 100%; 6 | box-shadow: 0px 3px 11px 0px rgba(0,0,0,0.11); 7 | border-radius: 10px; 8 | } 9 | 10 | 11 | .text-box { 12 | position: absolute; 13 | top: 0; 14 | color: #333; 15 | display: flex; 16 | height: 100%; 17 | width: 100%; 18 | padding: 0 20px; 19 | align-items: center; 20 | pointer-events: none; 21 | font-size: 16px; 22 | 23 | .text { 24 | display: flex; 25 | align-items: center; 26 | transition: all 0.5s; 27 | i { 28 | margin-right: 6px; 29 | font-size: 22px; 30 | transition: all 0.5s; 31 | } 32 | } 33 | 34 | .value { 35 | position: absolute; 36 | right: 100px; 37 | } 38 | } 39 | 40 | 41 | .slider { 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | position: relative; 46 | width: 100%; 47 | height: 100%; 48 | 49 | .bar { 50 | width: 100%; 51 | height: 100%; 52 | border-radius: 10px; 53 | overflow: hidden; 54 | background: rgba(238, 238, 238, 0.5); 55 | transition: background-color 0.5s; 56 | position: relative; 57 | 58 | .activebar { 59 | border-radius: 5px; 60 | width: 0%; 61 | height: 100%; 62 | background: #333; 63 | overflow: hidden; 64 | color: #FFF; 65 | transition: background-color 0.5s; 66 | white-space: nowrap; 67 | position: relative; 68 | 69 | .text-box { 70 | color: #fff; 71 | } 72 | } 73 | } 74 | 75 | .layer { 76 | width: 100%; 77 | height: 100%; 78 | position: absolute; 79 | top: 0; 80 | left: 0; 81 | } 82 | 83 | .knob { 84 | position: absolute; 85 | left: 0; 86 | top: calc(50% - 10px); 87 | width: 20px; 88 | height: 20px; 89 | border-radius: 50%; 90 | background-color: #FFF; 91 | box-shadow: 0px 2px 4px 1px rgba(0, 0, 0, 0.3); 92 | } 93 | } 94 | 95 | .slider-y { 96 | position: relative; 97 | display: flex; 98 | display: -webkit-flex; 99 | align-items: center; 100 | justify-content: center; 101 | flex-direction: row; 102 | height: 100%; 103 | min-width: 20px; 104 | min-height: 50px; 105 | padding: 10px 0; 106 | 107 | .bar { 108 | width: 5px; 109 | height: 100%; 110 | border-radius: 2.5px; 111 | } 112 | 113 | .bar-active { 114 | position: absolute; 115 | bottom: 0; 116 | height: 0%; 117 | } 118 | 119 | .knob-box { 120 | position: absolute; 121 | height: 100%; 122 | width: 20px; 123 | left: 0; 124 | 125 | .knob { 126 | bottom: 0; 127 | } 128 | } 129 | 130 | .touch { 131 | width: 150%; 132 | height: 100%; 133 | position: absolute; 134 | left: 0; 135 | } 136 | // color:rgba(64, 128, 255, 0.552); 137 | } 138 | 139 | -------------------------------------------------------------------------------- /src/app/components/widget-range/widget-range.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, EventEmitter, Injectable, Input, Output, Renderer2, ViewChild } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HAMMER_GESTURE_CONFIG, HammerGestureConfig, HammerModule } from '@angular/platform-browser'; 4 | 5 | declare var Hammer: any; 6 | 7 | @Injectable() 8 | export class MyHammerConfig extends HammerGestureConfig { 9 | override overrides = { 10 | 'pan': { direction: Hammer.DIRECTION_ALL, threshold: 5 }, 11 | 'press': { time: 300, threshold: 99 } 12 | } 13 | } 14 | 15 | @Component({ 16 | selector: 'widget-range', 17 | templateUrl: 'widget-range.html', 18 | styleUrls: ['widget-range.scss'], 19 | standalone: true, 20 | imports: [ 21 | CommonModule, HammerModule 22 | ], 23 | providers: [ 24 | { provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig }, 25 | ], 26 | }) 27 | export class WidgetRangeComponent { 28 | 29 | 30 | @Output() valueChange = new EventEmitter(); 31 | 32 | get tex() { 33 | return '亮度' 34 | } 35 | 36 | // @ViewChild('rangebox', { read: ElementRef, static: true }) rangebox: ElementRef; 37 | 38 | showValue = 0; 39 | // set value(value) { 40 | // if (typeof this.device.data[this.key] == 'undefined') 41 | // this.device.data[this.key] = {}; 42 | // this.device.data[this.key]['val'] = value; 43 | // } 44 | 45 | // get value() { 46 | // return this.getValue('val') 47 | // } 48 | 49 | value = 0; 50 | 51 | get max() { 52 | return 255 53 | } 54 | 55 | get min() { 56 | return 0 57 | } 58 | 59 | @Input() color; 60 | 61 | get color2() { 62 | return convertToRgba(this.color, 0.3) 63 | } 64 | 65 | textBoxWidth = '300px' 66 | 67 | constructor( 68 | private renderer: Renderer2 69 | ) { 70 | } 71 | 72 | timer; 73 | ngAfterViewInit() { 74 | this.timer = setInterval(() => { 75 | if (!this.isMoving) { 76 | this.processValue(); 77 | } 78 | }, 1000) 79 | this.refresh() 80 | } 81 | 82 | ngOnDestroy() { 83 | clearInterval(this.timer) 84 | } 85 | 86 | canSend = true; 87 | sendData(senddata) { 88 | // this.layouterService.send(`{"${this.key}":${senddata}}\n`); 89 | this.valueChange.emit(this.value) 90 | } 91 | 92 | refresh() { 93 | setTimeout(() => { 94 | // this.boxPadding = `0 ${this.gridSize - 32}px` 95 | // setTimeout(() => { 96 | this.textBoxWidth = `${this.bar.nativeElement.clientWidth}px` 97 | // }, 100) 98 | }) 99 | } 100 | 101 | @ViewChild("bar", { read: ElementRef, static: true }) bar: ElementRef; 102 | @ViewChild("barActive", { read: ElementRef, static: true }) activeBar: ElementRef; 103 | 104 | @Input() direction = 'x'; 105 | 106 | @Input() 107 | get length() { 108 | if (this.direction == 'x') { 109 | return this.bar.nativeElement.clientWidth; 110 | } 111 | } 112 | 113 | isMoving = false 114 | 115 | oldInputValue; 116 | // _value; 117 | // @Input() 118 | // set value(value) { 119 | // if (value > this.max) value = this.max; 120 | // if (value < this.min) value = this.min; 121 | // this._value = value; 122 | // }; 123 | 124 | // get value() { 125 | // return this._value; 126 | // } 127 | 128 | // @Output() valueChange = new EventEmitter(); 129 | 130 | _disabled = false; 131 | @Input() 132 | set disabled(disabled) { 133 | this._disabled = disabled; 134 | } 135 | get disabled() { 136 | return this._disabled; 137 | } 138 | 139 | _barColor = '#389BEE' 140 | @Input() 141 | set barcolor(color) { 142 | if (color[0] != '#' && color[0] != 'r' && color[0] != 'l') color = '#' + color; 143 | this.renderer.setStyle(this.bar.nativeElement, 'background', color); 144 | this._barColor = color; 145 | } 146 | get barcolor() { 147 | return this._barColor; 148 | } 149 | 150 | _activebarColor = '#389BEE' 151 | @Input() 152 | set activecolor(color) { 153 | if (color[0] != '#' && color[0] != 'r') color = '#' + color; 154 | this.renderer.setStyle(this.activeBar.nativeElement, 'background', color); 155 | this._activebarColor = color; 156 | } 157 | get activecolor() { 158 | return this._activebarColor; 159 | } 160 | 161 | processValue() { 162 | if (this.oldInputValue != this.value) { 163 | let x = (this.value - this.min) / (this.max - this.min) * this.length; 164 | this.renderer.setStyle(this.activeBar.nativeElement, 'transition', `width 0.5s`); 165 | this.moveSlider(x); 166 | setTimeout(() => { 167 | this.renderer.removeStyle(this.activeBar.nativeElement, 'transition'); 168 | }, 500); 169 | this.oldInputValue = this.value; 170 | } 171 | } 172 | 173 | moveSlider(move) { 174 | let barActiveScale = (move / this.length * 100); 175 | if (this.direction == 'x') { 176 | this.renderer.setStyle(this.activeBar.nativeElement, 'width', `${barActiveScale.toString()}%`); 177 | } 178 | this.value = Math.round(move / this.length * (this.max - this.min) + this.min); 179 | } 180 | 181 | tapEvent(e) { 182 | this.renderer.removeStyle(this.activeBar.nativeElement, 'transition'); 183 | let move; 184 | if (this.direction == 'x') { 185 | let p = e.target.getBoundingClientRect().left; 186 | move = e.center.x - p - 10; 187 | } 188 | move = this.checkLimit(move); 189 | this.moveSlider(move); 190 | this.pick('tap'); 191 | } 192 | 193 | panstartEvent(e) { 194 | this.renderer.removeStyle(this.activeBar.nativeElement, 'transition'); 195 | this.isMoving = true; 196 | let move; 197 | if (this.direction == 'x') { 198 | let p = e.target.getBoundingClientRect().left; 199 | move = e.center.x - p - 10; 200 | } 201 | move = this.checkLimit(move) 202 | this.moveSlider(move); 203 | } 204 | 205 | lastSendTime = 0; 206 | panmoveEvent(e) { 207 | let move; 208 | if (this.direction == 'x') { 209 | let p = e.target.getBoundingClientRect().left; 210 | move = e.center.x - p - 10; 211 | } 212 | move = this.checkLimit(move) 213 | this.moveSlider(move); 214 | this.pick('end'); 215 | } 216 | 217 | panendEvent(e) { 218 | this.isMoving = false; 219 | // this.pick('end'); 220 | } 221 | 222 | oldValue: number; 223 | pick(state = 'move') { 224 | if (this.value != this.oldValue) { 225 | this.oldValue = this.value; 226 | // this.valueChange.emit(this.value); 227 | } 228 | if (state == 'end' || state == 'tap') { 229 | this.sendData(this.value) 230 | } 231 | } 232 | 233 | checkLimit(move) { 234 | if (move > this.length) { 235 | move = this.length; 236 | } else if (move < 0) { 237 | move = 0; 238 | } 239 | return move; 240 | } 241 | } 242 | 243 | export function convertToRgba(cssColorString, opacity) { 244 | var div = document.createElement('div'); 245 | div.style.color = cssColorString; 246 | document.body.appendChild(div); 247 | 248 | var colors = window.getComputedStyle(div).color.match(/\d+/g).map(function (a) { return parseInt(a, 10); }); 249 | 250 | document.body.removeChild(div); 251 | 252 | return 'rgba(' + colors[0] + ', ' + colors[1] + ', ' + colors[2] + ', ' + opacity + ')'; 253 | } -------------------------------------------------------------------------------- /src/app/configs/app.config.ts: -------------------------------------------------------------------------------- 1 | export const MENU = [ 2 | { 3 | name: '灯光控制', 4 | icon: 'light', 5 | link: '/light' 6 | }, 7 | { 8 | name: '遥控器', 9 | icon: 'remote-control', 10 | link: '/ppt' 11 | }, 12 | { 13 | name: '小车遥控器', 14 | icon: 'joystick', 15 | link: '/joystick' 16 | }, 17 | { 18 | name: '智能家居', 19 | icon: 'smarthome', 20 | link: '/iot/smarthome' 21 | }, 22 | { 23 | name: '智慧农场', 24 | icon: 'farming', 25 | link: '/iot/farming' 26 | }, 27 | { 28 | name: '气象站', 29 | icon: 'weather-station', 30 | link: '/iot/weather' 31 | }, 32 | { 33 | name: '健康监测', 34 | icon: 'health', 35 | link: '/iot/health' 36 | }, 37 | { 38 | name: '环境监测', 39 | icon: 'environment', 40 | link: '/iot/environment' 41 | }, 42 | // { 43 | // name: '工业自动化', 44 | // icon: 'industry', 45 | // link: '/iot/industry' 46 | // }, 47 | { 48 | name: '蓝牙串口', 49 | icon: 'ble', 50 | link: '/serial' 51 | }, 52 | { 53 | name: '首页', 54 | icon: 'home_light', 55 | link: '/connect', 56 | root: true 57 | }, 58 | ] -------------------------------------------------------------------------------- /src/app/configs/device.config.ts: -------------------------------------------------------------------------------- 1 | export const SMARTHOME_CONFIG = { 2 | name: '智能家居', 3 | background: 'assets/img/smarthome-bg.webp', 4 | widgets: [{ 5 | name: '温度', 6 | icon: 'iconfont icon-temperature', 7 | unit: '℃', 8 | key: 'temperature', 9 | type: 'number' 10 | }, { 11 | name: '湿度', 12 | icon: 'iconfont icon-humidity', 13 | unit: '%', 14 | key: 'humidity', 15 | type: 'number' 16 | }, { 17 | name: '烟雾浓度', 18 | icon: 'iconfont icon-smoke', 19 | unit: 'mg/m³', 20 | key: 'smoke', 21 | type: 'number' 22 | }, { 23 | name: '光照强度', 24 | icon: 'iconfont icon-sun', 25 | unit: 'LX', 26 | key: 'illuminance', 27 | type: 'number' 28 | }, { 29 | name: 'TVOC', 30 | icon: 'iconfont icon-pm', 31 | unit: 'ppb', 32 | key: 'tvoc', 33 | type: 'number' 34 | }, 35 | { 36 | name: '照明', 37 | icon: 'iconfont icon-light3', 38 | key: 'light', 39 | state: false, 40 | type: 'switch' 41 | }, 42 | { 43 | name: '氛围灯', 44 | icon: 'iconfont icon-illumination', 45 | key: 'light2', 46 | state: false, 47 | type: 'switch' 48 | }, 49 | { 50 | name: '门', 51 | icon: 'iconfont icon-door', 52 | key: 'door', 53 | state: false, 54 | type: 'switch' 55 | }, 56 | { 57 | name: '窗帘', 58 | icon: 'iconfont icon-curtain', 59 | key: 'curtain', 60 | state: false, 61 | type: 'switch' 62 | }, 63 | { 64 | name: '风扇', 65 | icon: 'iconfont icon-fan', 66 | key: 'fan', 67 | state: false, 68 | type: 'switch' 69 | }, { 70 | name: '音乐', 71 | icon: 'iconfont icon-music', 72 | key: 'music', 73 | state: false, 74 | type: 'switch' 75 | }, { 76 | name: '语音播报', 77 | icon: 'iconfont icon-voiceplay', 78 | key: 'voice', 79 | state: false, 80 | type: 'button' 81 | }, { 82 | name: '显示屏', 83 | icon: 'iconfont icon-display', 84 | key: 'display', 85 | state: false, 86 | type: 'input' 87 | }] 88 | } 89 | 90 | export const SMARTFARMING_CONFIG = { 91 | name: '智慧农场', 92 | background: 'assets/img/smartfarming-bg.webp', 93 | widgets: [{ 94 | name: '环境温度', 95 | icon: 'iconfont icon-temperature', 96 | unit: '℃', 97 | key: 'temperature', 98 | type: 'number' 99 | }, { 100 | name: '空气湿度', 101 | icon: 'iconfont icon-humidity', 102 | unit: '%', 103 | key: 'humidity', 104 | type: 'number' 105 | }, { 106 | name: '土壤湿度', 107 | icon: 'iconfont icon-soil_moisture', 108 | unit: '%', 109 | key: 'soil_moisture', 110 | type: 'number' 111 | }, { 112 | name: '光照强度', 113 | icon: 'iconfont icon-sun', 114 | unit: 'LX', 115 | key: 'illuminance', 116 | type: 'number' 117 | }, { 118 | name: 'CO2', 119 | icon: 'iconfont icon-co2', 120 | unit: 'ppm', 121 | key: 'co2', 122 | type: 'number' 123 | }, { 124 | name: '补光灯', 125 | icon: 'iconfont icon-light3', 126 | key: 'light', 127 | state: false, 128 | type: 'switch' 129 | }, { 130 | name: '水泵', 131 | icon: 'iconfont icon-waterpump', 132 | key: 'waterpump', 133 | state: false, 134 | type: 'switch' 135 | }, 136 | { 137 | name: '风扇', 138 | icon: 'iconfont icon-fan', 139 | key: 'fan', 140 | state: false, 141 | type: 'switch' 142 | }, { 143 | name: '加热器', 144 | icon: 'iconfont icon-heater', 145 | key: 'heater', 146 | state: false, 147 | type: 'switch' 148 | }, { 149 | name: '显示屏', 150 | icon: 'iconfont icon-display', 151 | key: 'display', 152 | state: false, 153 | type: 'input' 154 | }] 155 | } 156 | 157 | export const WEATHERSTATION_CONFIG = { 158 | name: '气象站', 159 | background: 'assets/img/weatherstation-bg.webp', 160 | widgets: [{ 161 | name: '风速', 162 | icon: 'iconfont icon-wind-speed', 163 | unit: 'm/s', 164 | key: 'windspeed', 165 | type: 'number' 166 | }, { 167 | name: '风向', 168 | icon: 'iconfont icon-wind-direction', 169 | key: 'winddirection', 170 | state: false, 171 | type: 'number' 172 | }, { 173 | name: '雨量', 174 | icon: 'iconfont icon-rainfall', 175 | unit: 'mm', 176 | key: 'rainfall', 177 | type: 'number' 178 | }, { 179 | name: '环境温度', 180 | icon: 'iconfont icon-temperature', 181 | unit: '℃', 182 | key: 'temperature', 183 | type: 'number' 184 | }, { 185 | name: '环境湿度', 186 | icon: 'iconfont icon-humidity', 187 | unit: '%', 188 | key: 'humidity', 189 | type: 'number' 190 | }, { 191 | name: '气压', 192 | icon: 'iconfont icon-pressure', 193 | unit: 'KPa', 194 | key: 'pressure', 195 | type: 'number' 196 | }, 197 | { 198 | name: '光照强度', 199 | icon: 'iconfont icon-sun', 200 | unit: 'LX', 201 | key: 'sun', 202 | type: 'number' 203 | }, { 204 | name: '空气质量', 205 | icon: 'iconfont icon-pm', 206 | key: 'pm', 207 | state: false, 208 | type: 'number' 209 | }, { 210 | name: '语音播报', 211 | icon: 'iconfont icon-voiceplay', 212 | key: 'voice', 213 | state: false, 214 | type: 'button' 215 | }, { 216 | name: '显示屏', 217 | icon: 'iconfont icon-display', 218 | key: 'display', 219 | state: false, 220 | type: 'input' 221 | }] 222 | } 223 | 224 | export const HEALTH_CONFIG = { 225 | name: '健康监测', 226 | background: 'assets/img/health-bg.webp', 227 | widgets: [{ 228 | name: '心率', 229 | icon: 'iconfont icon-heartrate', 230 | unit: 'bpm', 231 | key: 'heartrate', 232 | type: 'number' 233 | }, { 234 | name: '血氧', 235 | icon: 'iconfont icon-bloodoxygen', 236 | unit: 'mol/l', 237 | key: 'bloodoxygen', 238 | type: 'number' 239 | }, { 240 | name: '血压', 241 | icon: 'iconfont icon-bloodpressure', 242 | unit: 'mmHg', 243 | key: 'bloodpressure', 244 | type: 'number' 245 | }, { 246 | name: '体温', 247 | icon: 'iconfont icon-temperature', 248 | unit: '℃', 249 | key: 'temperature', 250 | type: 'number' 251 | }, { 252 | name: '睡眠状态', 253 | icon: 'iconfont icon-sleep', 254 | key: 'sleep', 255 | type: 'number' 256 | }, { 257 | name: '电导率', 258 | icon: 'iconfont icon-gsr', 259 | unit: 'μS', 260 | key: 'gsr', 261 | type: 'number' 262 | }, { 263 | name: '震动提醒', 264 | icon: 'iconfont icon-vibrate', 265 | key: 'vibrate', 266 | state: false, 267 | type: 'button' 268 | }, { 269 | name: '语音播报', 270 | icon: 'iconfont icon-voiceplay', 271 | key: 'voice', 272 | state: false, 273 | type: 'button' 274 | }, { 275 | name: '屏幕提示', 276 | icon: 'iconfont icon-display', 277 | key: 'display', 278 | state: false, 279 | type: 'input' 280 | }] 281 | } 282 | 283 | export const ENVIRON_CONFIG = { 284 | name: '环境监测', 285 | background: 'assets/img/environment-bg.webp', 286 | widgets: [{ 287 | name: '温度', 288 | icon: 'iconfont icon-temperature', 289 | unit: '℃', 290 | key: 'temperature', 291 | type: 'number' 292 | }, { 293 | name: '湿度', 294 | icon: 'iconfont icon-humidity', 295 | unit: '%', 296 | key: 'humidity', 297 | type: 'number' 298 | }, { 299 | name: '气压', 300 | icon: 'iconfont icon-pressure', 301 | unit: 'KPa', 302 | key: 'pressure', 303 | type: 'number' 304 | }, { 305 | name: '光照', 306 | icon: 'iconfont icon-sun', 307 | unit: 'LX', 308 | key: 'sun', 309 | type: 'number' 310 | }, { 311 | name: '空气质量', 312 | icon: 'iconfont icon-pm', 313 | unit: 'pm', 314 | key: 'air', 315 | type: 'number' 316 | }, { 317 | name: '噪音', 318 | icon: 'iconfont icon-noise', 319 | unit: 'db', 320 | key: 'noise', 321 | type: 'number' 322 | }, 323 | { 324 | name: 'PH值', 325 | icon: 'iconfont icon-ph', 326 | key: 'ph', 327 | type: 'number' 328 | }, { 329 | name: '水位', 330 | icon: 'iconfont icon-water-level', 331 | unit: 'm', 332 | key: 'waterlevel', 333 | type: 'number' 334 | }, { 335 | name: '水质TDS', 336 | icon: 'iconfont icon-water', 337 | unit: 'mg/L', 338 | key: 'tds', 339 | type: 'number' 340 | }, { 341 | name: '紫外线', 342 | icon: 'iconfont icon-ultraviolet', 343 | unit: 'w/㎡', 344 | key: 'ultraviolet', 345 | type: 'number' 346 | }, { 347 | name: '显示屏', 348 | icon: 'iconfont icon-display', 349 | key: 'display', 350 | state: false, 351 | type: 'input' 352 | }, { 353 | name: '语音播报', 354 | icon: 'iconfont icon-voiceplay', 355 | key: 'voice', 356 | state: false, 357 | type: 'button' 358 | }] 359 | } 360 | 361 | export const INDUSTRY_CONFIG = { 362 | name: '工业自动化', 363 | background: 'assets/img/industry-bg.webp', 364 | widgets: [ 365 | { 366 | name: '开关状态', 367 | icon: 'iconfont icon-switch1', 368 | key: 'switch', 369 | type: 'number' 370 | }, { 371 | name: '电流', 372 | icon: 'iconfont icon-current', 373 | unit: 'A', 374 | key: 'current', 375 | type: 'number' 376 | }, { 377 | name: '电压', 378 | icon: 'iconfont icon-voltage', 379 | unit: 'V', 380 | key: 'voltage', 381 | type: 'number' 382 | }, { 383 | name: '功率', 384 | icon: 'iconfont icon-power', 385 | unit: 'W', 386 | key: 'power', 387 | type: 'number' 388 | }, { 389 | name: '颜色信息', 390 | icon: 'iconfont icon-rgb', 391 | key: 'color', 392 | type: 'number' 393 | }, { 394 | name: '重量', 395 | icon: 'iconfont icon-weight', 396 | unit: 'kg', 397 | key: 'weight', 398 | type: 'number' 399 | }, 400 | { 401 | name: '计数', 402 | icon: 'iconfont icon-count', 403 | key: 'count', 404 | type: 'number' 405 | }, { 406 | name: '身份信息', 407 | icon: 'iconfont icon-id', 408 | key: 'id', 409 | state: false, 410 | type: 'input' 411 | }, { 412 | name: '屏幕显示', 413 | icon: 'iconfont icon-display', 414 | key: 'display', 415 | state: false, 416 | type: 'input' 417 | }, { 418 | name: '语音播报', 419 | icon: 'iconfont icon-voiceplay', 420 | key: 'indvoice', 421 | state: false, 422 | type: 'switch' 423 | }, { 424 | name: '继电器1', 425 | icon: 'iconfont icon-switch1', 426 | key: 'relay1', 427 | state: false, 428 | type: 'switch' 429 | }, { 430 | name: '继电器2', 431 | icon: 'iconfont icon-switch1', 432 | key: 'relay2', 433 | state: false, 434 | type: 'switch' 435 | }, { 436 | name: '继电器3', 437 | icon: 'iconfont icon-switch1', 438 | key: 'relay3', 439 | state: false, 440 | type: 'switch' 441 | }, { 442 | name: '继电器4', 443 | icon: 'iconfont icon-switch1', 444 | key: 'relay4', 445 | state: false, 446 | type: 'switch' 447 | }] 448 | } 449 | -------------------------------------------------------------------------------- /src/app/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { UTCTimestamp } from 'lightweight-charts'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class DataService { 8 | 9 | manager = {} 10 | managerSeries = {} 11 | 12 | constructor() { } 13 | 14 | 15 | processData(data) { 16 | let entries = data.split('\n'); // 按行拆分 17 | for (let entry of entries) { 18 | let [key, value] = entry.split(':'); // 按冒号拆分 19 | key = key.trim() 20 | value = value.trim() 21 | this.manager[key] = value; // 去除可能的空格 22 | 23 | if (isNumeric(value)) { 24 | const localTime = new Date(); 25 | const time: UTCTimestamp = Math.floor(localTime.getTime() / 1000) - localTime.getTimezoneOffset() * 60 as UTCTimestamp; 26 | let valueN = Number(value) 27 | if (this.managerSeries[key]) { 28 | if (time - this.managerSeries[key][this.managerSeries[key].length - 1].time > 1) { 29 | this.managerSeries[key].push({ time, value: valueN }); 30 | } else { 31 | this.managerSeries[key][this.managerSeries[key].length - 1].value = valueN; 32 | } 33 | while (this.managerSeries[key].length > 0 && (Date.now() / 1000 - this.managerSeries[key][0].time) > 60) { 34 | this.managerSeries[key].shift(); 35 | } 36 | } else { 37 | this.managerSeries[key] = [{ time, value: valueN }] 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | function isNumeric(str: string): boolean { 45 | return /^-?\d+(\.\d+)?$/.test(str); 46 | } 47 | -------------------------------------------------------------------------------- /src/app/device.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class DeviceService { 7 | 8 | isPC = false; 9 | 10 | constructor() { } 11 | 12 | init() { 13 | var userAgentInfo = navigator.userAgent; 14 | var Agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]; 15 | var flag = true; 16 | for (var v = 0; v < Agents.length; v++) { 17 | if (userAgentInfo.indexOf(Agents[v]) > 0) { flag = false; break; } 18 | } 19 | this.isPC = flag; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/connect/connect.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
点灯物联网-教育版
4 | 5 |
6 | 7 |
连接蓝牙BLE设备
8 |
9 | 10 | 11 | 12 |
13 | 14 |
15 |
选择设备操作界面
16 |
17 | 18 |
19 |
20 | 21 |
22 | {{item.name}} 23 |
24 |
25 |
26 |
27 |
28 | 29 | arduino UNO R4 30 | 31 |
32 | 38 |
-------------------------------------------------------------------------------- /src/app/pages/connect/connect.component.scss: -------------------------------------------------------------------------------- 1 | @import "../page.scss"; 2 | 3 | .footer { 4 | position: absolute; 5 | width: calc(100% - 30px); 6 | bottom: 15px; 7 | text-align: center; 8 | color: #808080; 9 | font-size: 12px; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | flex-direction: column; 14 | 15 | img { 16 | height: 30px; 17 | margin-left: -34px; 18 | } 19 | } 20 | 21 | .logo-box { 22 | position: relative; 23 | width: 100%; 24 | text-align: center; 25 | 26 | img { 27 | width: 90%; 28 | } 29 | } 30 | 31 | .text-d { 32 | font-size: 12px; 33 | text-align: center; 34 | } 35 | 36 | .text { 37 | font-size: 12px; 38 | font-weight: 700; 39 | } 40 | 41 | .connect-btn { 42 | width: 100%; 43 | height: 35px; 44 | cursor: pointer; 45 | } 46 | 47 | .help { 48 | position: absolute; 49 | right: 0; 50 | top: 0; 51 | width: 30px; 52 | height: 30px; 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | } 57 | 58 | .connect-box { 59 | margin-top: 30px; 60 | 61 | .connect-btn { 62 | margin: 11px 0; 63 | background: rgb(102, 176, 255); 64 | border: none; 65 | color: #FFF; 66 | border-radius: 8px; 67 | font-size: 14px; 68 | display: flex; 69 | justify-content: center; 70 | align-items: center; 71 | height: 45px; 72 | 73 | &:hover { 74 | background: rgb(150, 200, 255); 75 | } 76 | } 77 | } 78 | 79 | .btn-box { 80 | display: grid; 81 | grid-template-columns: repeat(3, 1fr); 82 | grid-gap: 15px; 83 | font-size: 12px; 84 | margin-top: 15px; 85 | position: relative; 86 | 87 | .item { 88 | display: flex; 89 | justify-content: center; 90 | align-items: center; 91 | flex-direction: column; 92 | background: #FFF; 93 | border-radius: 8px; 94 | box-shadow: 0 0 5px #CCC; 95 | padding: 8px; 96 | cursor: pointer; 97 | 98 | &:hover { 99 | box-shadow: 0 0 5px #AAA; 100 | background: #2581de; 101 | color: #FFF; 102 | 103 | i { 104 | color: #FFF !important; 105 | } 106 | } 107 | 108 | .icon-box { 109 | height: 44px; 110 | width: 44px; 111 | margin-bottom: 7px; 112 | border-radius: 10px; 113 | display: flex; 114 | justify-content: center; 115 | align-items: center; 116 | 117 | i { 118 | font-size: 4em; 119 | color: #2581de; 120 | } 121 | 122 | } 123 | } 124 | 125 | } 126 | 127 | .add { 128 | position: absolute; 129 | width: calc(100% - 30px); 130 | bottom: 75px; 131 | display: flex; 132 | justify-content: center; 133 | align-items: center; 134 | 135 | img { 136 | width: 100%; 137 | } 138 | 139 | } 140 | 141 | @media screen and (min-width: 500px) { 142 | .add { 143 | display: none; 144 | } 145 | } 146 | 147 | @media screen and (max-height: 770px) { 148 | .add { 149 | display: none; 150 | } 151 | } -------------------------------------------------------------------------------- /src/app/pages/connect/connect.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BleService } from '../../ble.service'; 4 | import { RouterModule } from '@angular/router'; 5 | import { MENU } from '../../configs/app.config'; 6 | // import { SerialService } from '../../serial.service'; 7 | 8 | @Component({ 9 | selector: 'app-connect', 10 | standalone: true, 11 | imports: [CommonModule, RouterModule], 12 | templateUrl: './connect.component.html', 13 | styleUrl: './connect.component.scss' 14 | }) 15 | export class ConnectComponent { 16 | 17 | MENU = MENU 18 | 19 | get device() { 20 | return this.bleService.device 21 | } 22 | 23 | constructor( 24 | private bleService: BleService, 25 | ) { } 26 | 27 | search() { 28 | this.bleService.searchDevice(); 29 | } 30 | 31 | disconnect() { 32 | this.bleService.disconnect(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/pages/device-title.scss: -------------------------------------------------------------------------------- 1 | device-title { 2 | ::ng-deep { 3 | .device-title { 4 | .name { 5 | text-shadow: none !important; 6 | } 7 | 8 | .state { 9 | text-shadow: none !important; 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/pages/iot-page/iot-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | {{config?.name}} 6 |
7 |
8 | 数据面板 9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
近期 {{currentItem?.name}} 数据
17 | 18 |
19 |
20 | 控制中心 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 | 38 |
-------------------------------------------------------------------------------- /src/app/pages/iot-page/iot-page.component.scss: -------------------------------------------------------------------------------- 1 | @import "../page.scss"; 2 | 3 | setting-btn { 4 | ::ng-deep { 5 | i.icon-setting { 6 | color: #FFF; 7 | } 8 | } 9 | } 10 | 11 | device-title{ 12 | ::ng-deep{ 13 | .name{ 14 | color: #e1e1e1 !important; 15 | } 16 | .state{ 17 | color: #e9e9e9; 18 | } 19 | } 20 | } 21 | 22 | .bg-box { 23 | width: calc(100% + 15px); 24 | position: absolute; 25 | left: -15px; 26 | top: -15px; 27 | overflow: hidden; 28 | 29 | img { 30 | width: 100%; 31 | } 32 | } 33 | 34 | .content { 35 | // margin-top: 70px; 36 | position: absolute; 37 | width: calc(100% - 30px); 38 | z-index: 1; 39 | } 40 | 41 | .data-list { 42 | display: grid; 43 | grid-template-columns: repeat(3, minmax(100px, 1fr)); 44 | gap: 15px; 45 | 46 | >div { 47 | background: #FFF; 48 | box-shadow: 0 0 5px 5px rgba(224, 224, 224, 0.5); 49 | border-radius: 8px; 50 | } 51 | 52 | .data-box { 53 | position: relative; 54 | width: 100%; 55 | height: 65px; 56 | } 57 | 58 | 59 | } 60 | 61 | .data-list2 { 62 | margin-top: 15px; 63 | grid-template-columns: repeat(2, minmax(100px, 1fr)); 64 | } 65 | 66 | .line { 67 | margin-top: 15px; 68 | padding: 10px 0 0 10px; 69 | background: #FFF; 70 | box-shadow: 0 0 5px 5px rgba(224, 224, 224, 0.5); 71 | border-radius: 8px; 72 | .title{ 73 | font-size: 14px; 74 | font-weight: bold; 75 | } 76 | } 77 | 78 | .text { 79 | margin-top: 15px; 80 | margin-bottom: 15px; 81 | background: none; 82 | box-shadow: none; 83 | border-radius: none; 84 | font-weight: bold; 85 | text-shadow: 1px 1px 0 #b6b6b6; 86 | color: #222222; 87 | } 88 | 89 | .ctrl-list { 90 | display: grid; 91 | grid-template-columns: repeat(2, 1fr); 92 | gap: 15px; 93 | 94 | >div { 95 | background: #FFF; 96 | box-shadow: 0 0 5px 5px rgba(224, 224, 224, 0.5); 97 | border-radius: 8px; 98 | } 99 | 100 | .ctrl-box { 101 | position: relative; 102 | width: 100%; 103 | height: 100px; 104 | } 105 | } 106 | 107 | 108 | 109 | @media screen and (min-width: 500px) { 110 | .content { 111 | margin-top: 25px; 112 | } 113 | 114 | .data-list { 115 | 116 | grid-template-columns: repeat(2, 1fr); 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /src/app/pages/iot-page/iot-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { LineChartComponent } from '../../components/line-chart/line-chart.component'; 4 | import { SettingBtnComponent } from '../../components/setting-btn/setting-btn.component'; 5 | import { DataBoxComponent } from '../../components/data-box/data-box.component'; 6 | import { CtrlBoxComponent } from '../../components/ctrl-box/ctrl-box.component'; 7 | import { BleService } from '../../ble.service'; 8 | import { InputBoxComponent } from '../../components/input-box/input-box.component'; 9 | import { DeviceTitleComponent } from '../../components/device-title/device-title.component'; 10 | import { ENVIRON_CONFIG, HEALTH_CONFIG, INDUSTRY_CONFIG, SMARTFARMING_CONFIG, SMARTHOME_CONFIG, WEATHERSTATION_CONFIG } from '../../configs/device.config'; 11 | import { ActivatedRoute } from '@angular/router'; 12 | import { BtnBoxComponent } from '../../components/btn-box/btn-box.component'; 13 | 14 | @Component({ 15 | selector: 'iot-page', 16 | standalone: true, 17 | imports: [CommonModule, LineChartComponent, SettingBtnComponent, DataBoxComponent, CtrlBoxComponent, 18 | InputBoxComponent, BtnBoxComponent, DeviceTitleComponent], 19 | templateUrl: './iot-page.component.html', 20 | styleUrl: './iot-page.component.scss' 21 | }) 22 | export class IotPageComponent { 23 | 24 | items1 = [] 25 | 26 | items3 = [] 27 | 28 | currentItem; 29 | 30 | config; 31 | 32 | constructor( 33 | private bleService: BleService, 34 | private activatedRoute: ActivatedRoute 35 | ) { } 36 | 37 | ngOnInit(): void { 38 | this.activatedRoute.params.subscribe(params => { 39 | switch (params['devicename']) { 40 | case 'smarthome': 41 | this.config = SMARTHOME_CONFIG 42 | break; 43 | case 'weather': 44 | this.config = WEATHERSTATION_CONFIG 45 | break; 46 | case 'farming': 47 | this.config = SMARTFARMING_CONFIG 48 | break; 49 | case 'health': 50 | this.config = HEALTH_CONFIG 51 | break; 52 | case 'environment': 53 | this.config = ENVIRON_CONFIG 54 | break; 55 | case 'industry': 56 | this.config = INDUSTRY_CONFIG 57 | break; 58 | default: 59 | break; 60 | } 61 | 62 | this.items1 = [] 63 | this.items3 = [] 64 | this.currentItem = null; 65 | 66 | this.config.widgets.filter(item => item.type == 'number').forEach(item => { 67 | this.items1.push(item) 68 | }) 69 | this.config.widgets.filter(item => 70 | item.type == 'switch' || 71 | item.type === 'input' || 72 | item.type === 'button' 73 | ).forEach(item => { 74 | this.items3.push(item) 75 | }) 76 | this.currentItem = this.items1[0] 77 | }); 78 | 79 | } 80 | 81 | selectItem(item) { 82 | this.currentItem = item 83 | } 84 | 85 | stateChange($event, item) { 86 | console.log('stateChange', $event); 87 | this.bleService.sendData(`${item.key}:${$event ? 'on' : 'off'}\n`) 88 | } 89 | 90 | textChange(text) { 91 | this.bleService.sendData(`text:${text}\n`) 92 | } 93 | 94 | onClick(item) { 95 | this.bleService.sendData(`${item.key}:'tap'\n`) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/app/pages/joystick-page/joystick-page.component.html: -------------------------------------------------------------------------------- 1 |
2 | 小车遥控 3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 |
B1
11 |
B2
12 |
B3
13 |
B4
14 |
15 | 16 |
-------------------------------------------------------------------------------- /src/app/pages/joystick-page/joystick-page.component.scss: -------------------------------------------------------------------------------- 1 | @import "../page.scss"; 2 | @import "../device-title.scss"; 3 | 4 | .page { 5 | overflow: hidden; 6 | } 7 | 8 | .bg { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | height: 100%; 13 | 14 | img { 15 | height: 100%; 16 | transform: translate(-20%, 0%); 17 | opacity: 0.6; 18 | touch-action: manipulation; 19 | } 20 | } 21 | 22 | device-title { 23 | ::ng-deep { 24 | .device-title { 25 | transform: rotate(90deg); 26 | position: absolute; 27 | top: calc(50vh - 36px); 28 | right: 0; 29 | z-index: 9; 30 | } 31 | } 32 | } 33 | 34 | $block-size: 70px; 35 | 36 | .btn { 37 | border-radius: 50%; 38 | transition: all 0.3s; 39 | background-color: rgb(197, 197, 197); 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | transform: rotate(90deg); 44 | color: #FFF; 45 | font-size: 24px; 46 | 47 | &:active, 48 | &:focus { 49 | transform: scale(0.95) rotate(90deg); 50 | background-color: rgb(51, 163, 255); 51 | } 52 | } 53 | 54 | @media screen and (max-width: 500px) { 55 | .btn-box { 56 | width: 60vw; 57 | height: 60vw; 58 | position: absolute; 59 | 60 | &.left { 61 | top: 80px; 62 | left: 60px; 63 | } 64 | 65 | &.right { 66 | bottom: 80px; 67 | left: 60px; 68 | } 69 | 70 | .btn { 71 | width: 20vw; 72 | height: 20vw; 73 | position: absolute; 74 | 75 | img { 76 | width: 20vw; 77 | } 78 | 79 | &:first-child { 80 | top: 20vw; 81 | right: 0; 82 | 83 | img { 84 | transform: rotate(-90deg); 85 | transform-origin: left bottom; 86 | position: absolute; 87 | bottom: 0; 88 | left: 100%; 89 | } 90 | } 91 | 92 | &:nth-child(2) { 93 | bottom: 0; 94 | right: 20vw; 95 | 96 | img { 97 | position: absolute; 98 | bottom: 0; 99 | } 100 | } 101 | 102 | &:nth-child(3) { 103 | top: 0; 104 | left: 20vw; 105 | 106 | img { 107 | transform: rotate(-180deg); 108 | } 109 | } 110 | 111 | &:last-child { 112 | top: 20vw; 113 | left: 0; 114 | 115 | img { 116 | transform: rotate(90deg); 117 | transform-origin: bottom right; 118 | position: absolute; 119 | bottom: 0; 120 | right: 100%; 121 | } 122 | } 123 | } 124 | } 125 | 126 | } 127 | 128 | // PC端 129 | @media screen and (min-width: 501px) { 130 | $block-size: 60px; 131 | 132 | device-title { 133 | ::ng-deep { 134 | .device-title { 135 | top: calc(663px - 350px) !important; 136 | } 137 | } 138 | } 139 | 140 | .btn-box { 141 | width: 180px; 142 | height: 180px; 143 | position: absolute; 144 | 145 | &.left { 146 | top: 70px; 147 | left: 50px; 148 | } 149 | 150 | &.right { 151 | bottom: 70px; 152 | left: 50px; 153 | } 154 | 155 | .btn { 156 | width: $block-size; 157 | height: $block-size; 158 | position: absolute; 159 | 160 | img { 161 | width: $block-size; 162 | } 163 | 164 | &:first-child { 165 | top: $block-size; 166 | right: 0; 167 | 168 | img { 169 | transform: rotate(-90deg); 170 | transform-origin: left bottom; 171 | position: absolute; 172 | bottom: 0; 173 | left: 100%; 174 | } 175 | } 176 | 177 | &:nth-child(2) { 178 | bottom: 0; 179 | right: $block-size; 180 | 181 | img { 182 | position: absolute; 183 | bottom: 0; 184 | } 185 | } 186 | 187 | &:nth-child(3) { 188 | top: 0; 189 | left: $block-size; 190 | 191 | img { 192 | transform: rotate(-180deg); 193 | } 194 | } 195 | 196 | &:last-child { 197 | top: $block-size; 198 | left: 0; 199 | 200 | img { 201 | transform: rotate(90deg); 202 | transform-origin: bottom right; 203 | position: absolute; 204 | bottom: 0; 205 | right: 100%; 206 | } 207 | } 208 | } 209 | } 210 | 211 | } -------------------------------------------------------------------------------- /src/app/pages/joystick-page/joystick-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HammerModule } from '@angular/platform-browser'; 4 | import { SettingBtnComponent } from '../../components/setting-btn/setting-btn.component'; 5 | import { BleService } from '../../ble.service'; 6 | import { WidgetJoystickComponent } from '../../components/widget-joystick/widget-joystick'; 7 | import { DeviceService } from '../../device.service'; 8 | import { DeviceTitleComponent } from '../../components/device-title/device-title.component'; 9 | @Component({ 10 | selector: 'app-joystick-page', 11 | standalone: true, 12 | imports: [CommonModule, 13 | HammerModule, 14 | SettingBtnComponent, 15 | WidgetJoystickComponent, 16 | HammerModule, 17 | DeviceTitleComponent 18 | ], 19 | templateUrl: './joystick-page.component.html', 20 | styleUrl: './joystick-page.component.scss' 21 | }) 22 | export class JoystickPageComponent { 23 | 24 | private keydownHandler: any; 25 | 26 | constructor( 27 | private bleService: BleService, 28 | private deviceService: DeviceService, 29 | private render: Renderer2, 30 | ) { } 31 | 32 | ngOnInit() { 33 | 34 | if (this.deviceService.isPC) { 35 | this.keydownHandler = function (event) { 36 | console.log('按键字母:', event.key); 37 | } 38 | window.addEventListener('keydown', this.keydownHandler); 39 | } 40 | } 41 | 42 | ngAfterViewInit() { 43 | this.listenGesture(); 44 | } 45 | 46 | ngOnDestroy() { 47 | this.unlistenGesture(); 48 | // 当离开页面时,移除键盘输入事件监听器 49 | window.removeEventListener('keydown', this.keydownHandler); 50 | } 51 | 52 | valueChange(e) { 53 | this.bleService.sendData(`joy:${e}\n`) 54 | } 55 | 56 | timer1; 57 | timer2; 58 | timer3; 59 | timer4; 60 | startSend(btn) { 61 | console.log('startSend:', btn); 62 | 63 | switch (btn) { 64 | case 'B1': 65 | if (!this.timer1) { 66 | navigator.vibrate(100); 67 | this.timer1 = setInterval(() => { 68 | this.bleService.sendData(`${btn}:press\n`) 69 | }, 100) 70 | } 71 | break; 72 | case 'B2': 73 | if (!this.timer2) 74 | this.timer2 = setInterval(() => { 75 | this.bleService.sendData(`${btn}:press\n`) 76 | }, 100) 77 | break; 78 | case 'B3': 79 | if (!this.timer3) 80 | this.timer3 = setInterval(() => { 81 | this.bleService.sendData(`${btn}:press\n`) 82 | }, 100) 83 | break; 84 | case 'B4': 85 | if (!this.timer4) 86 | this.timer4 = setInterval(() => { 87 | this.bleService.sendData(`${btn}:press\n`) 88 | }, 100) 89 | break; 90 | default: 91 | break; 92 | } 93 | } 94 | 95 | stopSend(btn) { 96 | console.log('stopSend', btn); 97 | switch (btn) { 98 | case 'B1': 99 | this.bleService.sendData(`${btn}:pressup\n`) 100 | clearInterval(this.timer1) 101 | this.timer1 = null 102 | break; 103 | case 'B2': 104 | this.bleService.sendData(`${btn}:pressup\n`) 105 | clearInterval(this.timer2) 106 | this.timer2 = null 107 | break; 108 | case 'B3': 109 | this.bleService.sendData(`${btn}:pressup\n`) 110 | clearInterval(this.timer3) 111 | this.timer3 = null 112 | break; 113 | case 'B4': 114 | this.bleService.sendData(`${btn}:pressup\n`) 115 | clearInterval(this.timer4) 116 | this.timer4 = null 117 | break; 118 | default: 119 | break; 120 | } 121 | } 122 | 123 | tap(btn) { 124 | this.bleService.sendData(`${btn}:press\n`) 125 | } 126 | 127 | // 修复手指移动后无法触发pressup的问题 128 | @ViewChild("button1", { read: ElementRef, static: true }) button1: ElementRef; 129 | @ViewChild("button2", { read: ElementRef, static: true }) button2: ElementRef; 130 | @ViewChild("button3", { read: ElementRef, static: true }) button3: ElementRef; 131 | @ViewChild("button4", { read: ElementRef, static: true }) button4: ElementRef; 132 | mouseupEvent1; 133 | touchendEvent1; 134 | mouseupEvent2; 135 | touchendEvent2; 136 | mouseupEvent3; 137 | touchendEvent3; 138 | mouseupEvent4; 139 | touchendEvent4; 140 | listenGesture() { 141 | this.mouseupEvent1 = this.render.listen(this.button1.nativeElement, 'mouseup', e => this.stopSend('B1')); 142 | this.touchendEvent1 = this.render.listen(this.button1.nativeElement, 'touchend', e => this.stopSend('B1')); 143 | this.mouseupEvent2 = this.render.listen(this.button2.nativeElement, 'mouseup', e => this.stopSend('B2')); 144 | this.touchendEvent2 = this.render.listen(this.button2.nativeElement, 'touchend', e => this.stopSend('B2')); 145 | this.mouseupEvent3 = this.render.listen(this.button3.nativeElement, 'mouseup', e => this.stopSend('B3')); 146 | this.touchendEvent3 = this.render.listen(this.button3.nativeElement, 'touchend', e => this.stopSend('B3')); 147 | this.mouseupEvent4 = this.render.listen(this.button4.nativeElement, 'mouseup', e => this.stopSend('B4')); 148 | this.touchendEvent4 = this.render.listen(this.button4.nativeElement, 'touchend', e => this.stopSend('B4')); 149 | } 150 | 151 | unlistenGesture() { 152 | this.mouseupEvent1(); 153 | this.touchendEvent1(); 154 | this.mouseupEvent2(); 155 | this.touchendEvent2(); 156 | this.mouseupEvent3(); 157 | this.touchendEvent3(); 158 | this.mouseupEvent4(); 159 | this.touchendEvent4(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/app/pages/light-page/light-page.component.html: -------------------------------------------------------------------------------- 1 |
2 | 智能灯控 3 |
4 | 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 |
-------------------------------------------------------------------------------- /src/app/pages/light-page/light-page.component.scss: -------------------------------------------------------------------------------- 1 | @import "../page.scss"; 2 | @import "../device-title.scss"; 3 | 4 | .color-picker-box { 5 | margin-top: 50px; 6 | position: relative; 7 | width: 100%; 8 | } 9 | 10 | .widget-range-box { 11 | margin-top: 30px; 12 | position: relative; 13 | width: 100%; 14 | } 15 | 16 | .turn { 17 | width: calc(100% - 30px); 18 | position: absolute; 19 | bottom: 90px; 20 | display: flex; 21 | justify-content: center; 22 | 23 | .turn-btn { 24 | width: 21vw; 25 | height: 21vw; 26 | max-width: 80px; 27 | max-height: 80px; 28 | border-radius: 50%; 29 | background: #FFF; 30 | box-shadow: 0px 3px 11px 0px rgba(0, 0, 0, 0.11); 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | cursor: pointer; 35 | color: #306CFF; 36 | transition: all 0.3s; 37 | 38 | i { 39 | font-size: 2.5em; 40 | } 41 | 42 | &.on { 43 | color: #FFF; 44 | background: #306CFF; 45 | } 46 | 47 | &:active { 48 | transform: scale(0.92); 49 | } 50 | } 51 | 52 | 53 | 54 | } -------------------------------------------------------------------------------- /src/app/pages/light-page/light-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SettingBtnComponent } from '../../components/setting-btn/setting-btn.component'; 4 | import { ColorPickerComponent } from '../../components/color-picker/colorpicker'; 5 | import { WidgetRangeComponent } from '../../components/widget-range/widget-range'; 6 | import { BleService } from '../../ble.service'; 7 | import { DeviceTitleComponent } from '../../components/device-title/device-title.component'; 8 | 9 | 10 | @Component({ 11 | selector: 'light-page', 12 | standalone: true, 13 | imports: [CommonModule, SettingBtnComponent, ColorPickerComponent, WidgetRangeComponent, DeviceTitleComponent], 14 | templateUrl: './light-page.component.html', 15 | styleUrl: './light-page.component.scss' 16 | }) 17 | export class LightPageComponent { 18 | 19 | constructor( 20 | private bleService: BleService 21 | ) { 22 | } 23 | 24 | color; 25 | 26 | colorChange(e) { 27 | this.color = `rgb(${e[0]},${e[1]}, ${e[2]})` 28 | this.bleService.sendData(`rgb:${e[0]},${e[1]},${e[2]}\n`) 29 | } 30 | 31 | brightnessChange(e) { 32 | this.bleService.sendData(`brightness:${e}\n`) 33 | } 34 | 35 | state = 'off' 36 | turn() { 37 | if (this.state == 'on') { 38 | this.state = 'off' 39 | } else { 40 | this.state = 'on' 41 | } 42 | this.bleService.sendData(`turn:${this.state}\n`) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/pages/page.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | padding: 24px 15px 0 15px; 3 | position: relative; 4 | height: calc(100% - 24px); 5 | width: calc(100% - 30px); 6 | // background: #fbfbfb; 7 | background: #F4F6F8; 8 | user-select: none; 9 | overflow-y: auto; 10 | 11 | &::-webkit-scrollbar { 12 | width: 0; 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/pages/ppt-page/ppt-page.component.html: -------------------------------------------------------------------------------- 1 |
2 | PPT控制器 3 |
4 |
5 | 6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 |
A
32 |
B
33 |
C
34 |
D
35 |
36 |
37 | 38 |
-------------------------------------------------------------------------------- /src/app/pages/ppt-page/ppt-page.component.scss: -------------------------------------------------------------------------------- 1 | @import "../page.scss"; 2 | @import "../device-title.scss"; 3 | 4 | 5 | 6 | .top { 7 | margin-top: 15px; 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | 12 | >div { 13 | width: 18vw; 14 | height: 18vw; 15 | border-radius: 50%; 16 | background: #FFFFFF; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | color: #1D9BF0; 21 | font-size: 20px; 22 | box-shadow: 0px 3px 11px 0px rgba(0, 0, 0, 0.11); 23 | transition: all 0.3s ease-in-out; 24 | cursor: pointer; 25 | i { 26 | font-size: 1.5em; 27 | } 28 | 29 | &.active, 30 | &:hover { 31 | background: #1D9BF0; 32 | 33 | i { 34 | color: #FFFFFF; 35 | } 36 | } 37 | } 38 | } 39 | 40 | .mid { 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | 45 | .btn-box { 46 | margin: 5vh; 47 | height: 65vw; 48 | padding: 2vw; 49 | border-radius: 20vw; 50 | box-shadow: 0px 3px 11px 0px rgba(0, 0, 0, 0.11); 51 | background: #FFFFFF; 52 | display: flex; 53 | flex-direction: column; 54 | justify-content: space-between; 55 | align-items: center; 56 | 57 | >div { 58 | width: 24vw; 59 | height: 24vw; 60 | border-radius: 12vw; 61 | background: #D1D6E2; 62 | display: flex; 63 | flex-direction: column; 64 | justify-content: center; 65 | align-items: center; 66 | transition: all 0.3s ease-in-out; 67 | cursor: pointer; 68 | i { 69 | color: #FFFFFF; 70 | font-size: 2em; 71 | } 72 | 73 | i.down { 74 | transform: rotate(180deg); 75 | } 76 | 77 | &.active, 78 | &:hover { 79 | background: #1D9BF0; 80 | } 81 | } 82 | } 83 | } 84 | 85 | .bot-box { 86 | position: absolute; 87 | bottom: 25px; 88 | width: calc(100% - 30px); 89 | } 90 | 91 | .bot1, 92 | .bot2 { 93 | box-shadow: 0px 3px 11px 0px rgba(0, 0, 0, 0.11); 94 | background-color: #ffffff; 95 | display: flex; 96 | justify-content: space-between; 97 | align-items: center; 98 | padding: 10px; 99 | border-radius: 10px; 100 | margin-top: 15px; 101 | 102 | >div { 103 | width: 18vw; 104 | height: 18vw; 105 | border-radius: 50%; 106 | background: #D1D6E2; 107 | display: flex; 108 | justify-content: center; 109 | align-items: center; 110 | color: #ffffff; 111 | transition: all 0.3s ease-in-out; 112 | cursor: pointer; 113 | .iconfont { 114 | font-size: 1em; 115 | } 116 | 117 | font-size: 1.8em; 118 | 119 | &.active, 120 | &:hover { 121 | background: #1D9BF0; 122 | } 123 | } 124 | } 125 | 126 | 127 | // PC访问时 128 | @media screen and (min-width: 768px) { 129 | .top { 130 | margin-top: 15px; 131 | display: flex; 132 | justify-content: space-between; 133 | align-items: center; 134 | 135 | >div { 136 | width: 45px; 137 | height: 45px; 138 | 139 | i { 140 | font-size: 20px; 141 | } 142 | } 143 | } 144 | 145 | .mid { 146 | display: flex; 147 | justify-content: center; 148 | align-items: center; 149 | 150 | .btn-box { 151 | height: 200px; 152 | padding: 8px; 153 | 154 | >div { 155 | width: 66px; 156 | height: 66px; 157 | 158 | i { 159 | font-size: 28px; 160 | } 161 | } 162 | } 163 | } 164 | 165 | .bot1, 166 | .bot2 { 167 | >div { 168 | width: 45px; 169 | height: 45px; 170 | font-size: 20px; 171 | 172 | i { 173 | font-size: 20px !important; 174 | } 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /src/app/pages/ppt-page/ppt-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SettingBtnComponent } from '../../components/setting-btn/setting-btn.component'; 4 | import { ColorPickerComponent } from '../../components/color-picker/colorpicker'; 5 | import { WidgetRangeComponent } from '../../components/widget-range/widget-range'; 6 | import { BleService } from '../../ble.service'; 7 | import { DeviceTitleComponent } from '../../components/device-title/device-title.component'; 8 | 9 | 10 | @Component({ 11 | selector: 'ppt-page', 12 | standalone: true, 13 | imports: [CommonModule, SettingBtnComponent, ColorPickerComponent, WidgetRangeComponent, DeviceTitleComponent], 14 | templateUrl: './ppt-page.component.html', 15 | styleUrl: './ppt-page.component.scss' 16 | }) 17 | export class PptPageComponent { 18 | 19 | constructor( 20 | private bleService: BleService 21 | ) { 22 | } 23 | 24 | send(data) { 25 | this.bleService.sendData(data + '\n'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/pages/serial/serial.component.html: -------------------------------------------------------------------------------- 1 |
2 | 蓝牙串口调试 3 |
4 | 5 | 6 | 7 |
8 |
9 |
10 |
11 | 14 | 15 |
-------------------------------------------------------------------------------- /src/app/pages/serial/serial.component.scss: -------------------------------------------------------------------------------- 1 | @import "../page.scss"; 2 | 3 | .input-box { 4 | height: 46px; 5 | background: #FFF; 6 | display: flex; 7 | align-items: center; 8 | box-shadow: 0 0 5px 5px rgba(224, 224, 224, 0.5); 9 | border-radius: 8px; 10 | overflow: hidden; 11 | margin-top: 10px; 12 | position: relative; 13 | 14 | input { 15 | width: calc(100% - 115px); 16 | border: none; 17 | padding-left: 10px; 18 | height: 40px; 19 | outline: none; 20 | font-size: 16px; 21 | } 22 | 23 | button { 24 | position: absolute; 25 | right: 0; 26 | height: 40px; 27 | width: 48px; 28 | border: none; 29 | border-radius: 8px; 30 | margin-right: 3px; 31 | background: rgb(18, 90, 223); 32 | color: #FFF; 33 | flex-shrink: 0; 34 | cursor: pointer; 35 | 36 | &.clear{ 37 | right: 51px !important; 38 | color: rgb(18, 90, 223); 39 | background:transparent; 40 | } 41 | } 42 | } 43 | 44 | .r-box { 45 | margin-top: 10px; 46 | background: #FFF; 47 | box-shadow: 0 0 5px 5px rgba(224, 224, 224, 0.5); 48 | border-radius: 8px; 49 | height: calc(100% - 115px); 50 | padding: 10px; 51 | overflow: auto; 52 | 53 | .textzone {} 54 | } -------------------------------------------------------------------------------- /src/app/pages/serial/serial.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { DeviceTitleComponent } from '../../components/device-title/device-title.component'; 4 | import { BleService } from '../../ble.service'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { HtmlPipe } from '../../pipes/html.pipe'; 7 | import { SettingBtnComponent } from '../../components/setting-btn/setting-btn.component'; 8 | 9 | @Component({ 10 | selector: 'app-serial', 11 | standalone: true, 12 | imports: [ 13 | CommonModule, 14 | DeviceTitleComponent, 15 | FormsModule, 16 | HtmlPipe, 17 | SettingBtnComponent 18 | ], 19 | templateUrl: './serial.component.html', 20 | styleUrl: './serial.component.scss' 21 | }) 22 | export class SerialComponent { 23 | 24 | 25 | value; 26 | 27 | data = ''; 28 | 29 | get dataChanged() { 30 | return this.bleService.dataChanged; 31 | } 32 | 33 | constructor( 34 | private bleService: BleService 35 | ) { 36 | } 37 | 38 | send() { 39 | this.bleService.sendData(this.value); 40 | } 41 | 42 | ngAfterViewInit(): void { 43 | this.bleService.dataChanged.subscribe(data => { 44 | this.data = this.data + data; 45 | }); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/app/pipes/html.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { DomSanitizer } from '@angular/platform-browser'; 3 | 4 | @Pipe({ 5 | name: 'html', 6 | standalone: true, 7 | }) 8 | export class HtmlPipe implements PipeTransform { 9 | 10 | constructor(private sanitizer: DomSanitizer) { 11 | } 12 | 13 | transform(data) { 14 | // 将字符串中的\n替换为
15 | data = data.replace(/\n/g, '
'); 16 | return this.sanitizer.bypassSecurityTrustHtml(data); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/serial.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | const serial = (navigator as any).serial; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class SerialService { 10 | 11 | port = null; 12 | 13 | dataChanged = new Subject() 14 | 15 | constructor() { } 16 | 17 | async connectPort() { 18 | try { 19 | this.port = await serial.requestPort(); 20 | await this.port.open({ baudRate: 9600 }); 21 | // 发送字符串"Hello Serial" 22 | const textEncoder = new TextEncoder(); 23 | const data = textEncoder.encode("Hello Serial"); 24 | await this.sendData(data); 25 | } catch (error) { 26 | // 处理连接或发送数据时的错误 27 | console.error('There was an error opening the serial port:', error); 28 | } 29 | } 30 | 31 | 32 | 33 | async sendData(data) { 34 | const writer = this.port.writable.getWriter(); 35 | await writer.write(data); 36 | writer.releaseLock(); 37 | } 38 | 39 | handleCharacteristicValueChanged(event) { 40 | let value = event.target.value; 41 | let decoder = new TextDecoder(); 42 | let decodedValue = decoder.decode(value); 43 | console.log(`Received data: ${decodedValue}`); 44 | this.dataChanged.next(decodedValue); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/app/wifi.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class WifiService { 7 | 8 | constructor() { } 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/diandeng-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/diandeng-logo.png -------------------------------------------------------------------------------- /src/assets/iconfont/demo.css: -------------------------------------------------------------------------------- 1 | /* Logo 字体 */ 2 | @font-face { 3 | font-family: "iconfont logo"; 4 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); 5 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), 6 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), 7 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), 8 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); 9 | } 10 | 11 | .logo { 12 | font-family: "iconfont logo"; 13 | font-size: 160px; 14 | font-style: normal; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | /* tabs */ 20 | .nav-tabs { 21 | position: relative; 22 | } 23 | 24 | .nav-tabs .nav-more { 25 | position: absolute; 26 | right: 0; 27 | bottom: 0; 28 | height: 42px; 29 | line-height: 42px; 30 | color: #666; 31 | } 32 | 33 | #tabs { 34 | border-bottom: 1px solid #eee; 35 | } 36 | 37 | #tabs li { 38 | cursor: pointer; 39 | width: 100px; 40 | height: 40px; 41 | line-height: 40px; 42 | text-align: center; 43 | font-size: 16px; 44 | border-bottom: 2px solid transparent; 45 | position: relative; 46 | z-index: 1; 47 | margin-bottom: -1px; 48 | color: #666; 49 | } 50 | 51 | 52 | #tabs .active { 53 | border-bottom-color: #f00; 54 | color: #222; 55 | } 56 | 57 | .tab-container .content { 58 | display: none; 59 | } 60 | 61 | /* 页面布局 */ 62 | .main { 63 | padding: 30px 100px; 64 | width: 960px; 65 | margin: 0 auto; 66 | } 67 | 68 | .main .logo { 69 | color: #333; 70 | text-align: left; 71 | margin-bottom: 30px; 72 | line-height: 1; 73 | height: 110px; 74 | margin-top: -50px; 75 | overflow: hidden; 76 | *zoom: 1; 77 | } 78 | 79 | .main .logo a { 80 | font-size: 160px; 81 | color: #333; 82 | } 83 | 84 | .helps { 85 | margin-top: 40px; 86 | } 87 | 88 | .helps pre { 89 | padding: 20px; 90 | margin: 10px 0; 91 | border: solid 1px #e7e1cd; 92 | background-color: #fffdef; 93 | overflow: auto; 94 | } 95 | 96 | .icon_lists { 97 | width: 100% !important; 98 | overflow: hidden; 99 | *zoom: 1; 100 | } 101 | 102 | .icon_lists li { 103 | width: 100px; 104 | margin-bottom: 10px; 105 | margin-right: 20px; 106 | text-align: center; 107 | list-style: none !important; 108 | cursor: default; 109 | } 110 | 111 | .icon_lists li .code-name { 112 | line-height: 1.2; 113 | } 114 | 115 | .icon_lists .icon { 116 | display: block; 117 | height: 100px; 118 | line-height: 100px; 119 | font-size: 42px; 120 | margin: 10px auto; 121 | color: #333; 122 | -webkit-transition: font-size 0.25s linear, width 0.25s linear; 123 | -moz-transition: font-size 0.25s linear, width 0.25s linear; 124 | transition: font-size 0.25s linear, width 0.25s linear; 125 | } 126 | 127 | .icon_lists .icon:hover { 128 | font-size: 100px; 129 | } 130 | 131 | .icon_lists .svg-icon { 132 | /* 通过设置 font-size 来改变图标大小 */ 133 | width: 1em; 134 | /* 图标和文字相邻时,垂直对齐 */ 135 | vertical-align: -0.15em; 136 | /* 通过设置 color 来改变 SVG 的颜色/fill */ 137 | fill: currentColor; 138 | /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 139 | normalize.css 中也包含这行 */ 140 | overflow: hidden; 141 | } 142 | 143 | .icon_lists li .name, 144 | .icon_lists li .code-name { 145 | color: #666; 146 | } 147 | 148 | /* markdown 样式 */ 149 | .markdown { 150 | color: #666; 151 | font-size: 14px; 152 | line-height: 1.8; 153 | } 154 | 155 | .highlight { 156 | line-height: 1.5; 157 | } 158 | 159 | .markdown img { 160 | vertical-align: middle; 161 | max-width: 100%; 162 | } 163 | 164 | .markdown h1 { 165 | color: #404040; 166 | font-weight: 500; 167 | line-height: 40px; 168 | margin-bottom: 24px; 169 | } 170 | 171 | .markdown h2, 172 | .markdown h3, 173 | .markdown h4, 174 | .markdown h5, 175 | .markdown h6 { 176 | color: #404040; 177 | margin: 1.6em 0 0.6em 0; 178 | font-weight: 500; 179 | clear: both; 180 | } 181 | 182 | .markdown h1 { 183 | font-size: 28px; 184 | } 185 | 186 | .markdown h2 { 187 | font-size: 22px; 188 | } 189 | 190 | .markdown h3 { 191 | font-size: 16px; 192 | } 193 | 194 | .markdown h4 { 195 | font-size: 14px; 196 | } 197 | 198 | .markdown h5 { 199 | font-size: 12px; 200 | } 201 | 202 | .markdown h6 { 203 | font-size: 12px; 204 | } 205 | 206 | .markdown hr { 207 | height: 1px; 208 | border: 0; 209 | background: #e9e9e9; 210 | margin: 16px 0; 211 | clear: both; 212 | } 213 | 214 | .markdown p { 215 | margin: 1em 0; 216 | } 217 | 218 | .markdown>p, 219 | .markdown>blockquote, 220 | .markdown>.highlight, 221 | .markdown>ol, 222 | .markdown>ul { 223 | width: 80%; 224 | } 225 | 226 | .markdown ul>li { 227 | list-style: circle; 228 | } 229 | 230 | .markdown>ul li, 231 | .markdown blockquote ul>li { 232 | margin-left: 20px; 233 | padding-left: 4px; 234 | } 235 | 236 | .markdown>ul li p, 237 | .markdown>ol li p { 238 | margin: 0.6em 0; 239 | } 240 | 241 | .markdown ol>li { 242 | list-style: decimal; 243 | } 244 | 245 | .markdown>ol li, 246 | .markdown blockquote ol>li { 247 | margin-left: 20px; 248 | padding-left: 4px; 249 | } 250 | 251 | .markdown code { 252 | margin: 0 3px; 253 | padding: 0 5px; 254 | background: #eee; 255 | border-radius: 3px; 256 | } 257 | 258 | .markdown strong, 259 | .markdown b { 260 | font-weight: 600; 261 | } 262 | 263 | .markdown>table { 264 | border-collapse: collapse; 265 | border-spacing: 0px; 266 | empty-cells: show; 267 | border: 1px solid #e9e9e9; 268 | width: 95%; 269 | margin-bottom: 24px; 270 | } 271 | 272 | .markdown>table th { 273 | white-space: nowrap; 274 | color: #333; 275 | font-weight: 600; 276 | } 277 | 278 | .markdown>table th, 279 | .markdown>table td { 280 | border: 1px solid #e9e9e9; 281 | padding: 8px 16px; 282 | text-align: left; 283 | } 284 | 285 | .markdown>table th { 286 | background: #F7F7F7; 287 | } 288 | 289 | .markdown blockquote { 290 | font-size: 90%; 291 | color: #999; 292 | border-left: 4px solid #e9e9e9; 293 | padding-left: 0.8em; 294 | margin: 1em 0; 295 | } 296 | 297 | .markdown blockquote p { 298 | margin: 0; 299 | } 300 | 301 | .markdown .anchor { 302 | opacity: 0; 303 | transition: opacity 0.3s ease; 304 | margin-left: 8px; 305 | } 306 | 307 | .markdown .waiting { 308 | color: #ccc; 309 | } 310 | 311 | .markdown h1:hover .anchor, 312 | .markdown h2:hover .anchor, 313 | .markdown h3:hover .anchor, 314 | .markdown h4:hover .anchor, 315 | .markdown h5:hover .anchor, 316 | .markdown h6:hover .anchor { 317 | opacity: 1; 318 | display: inline-block; 319 | } 320 | 321 | .markdown>br, 322 | .markdown>p>br { 323 | clear: both; 324 | } 325 | 326 | 327 | .hljs { 328 | display: block; 329 | background: white; 330 | padding: 0.5em; 331 | color: #333333; 332 | overflow-x: auto; 333 | } 334 | 335 | .hljs-comment, 336 | .hljs-meta { 337 | color: #969896; 338 | } 339 | 340 | .hljs-string, 341 | .hljs-variable, 342 | .hljs-template-variable, 343 | .hljs-strong, 344 | .hljs-emphasis, 345 | .hljs-quote { 346 | color: #df5000; 347 | } 348 | 349 | .hljs-keyword, 350 | .hljs-selector-tag, 351 | .hljs-type { 352 | color: #a71d5d; 353 | } 354 | 355 | .hljs-literal, 356 | .hljs-symbol, 357 | .hljs-bullet, 358 | .hljs-attribute { 359 | color: #0086b3; 360 | } 361 | 362 | .hljs-section, 363 | .hljs-name { 364 | color: #63a35c; 365 | } 366 | 367 | .hljs-tag { 368 | color: #333333; 369 | } 370 | 371 | .hljs-title, 372 | .hljs-attr, 373 | .hljs-selector-id, 374 | .hljs-selector-class, 375 | .hljs-selector-attr, 376 | .hljs-selector-pseudo { 377 | color: #795da3; 378 | } 379 | 380 | .hljs-addition { 381 | color: #55a532; 382 | background-color: #eaffea; 383 | } 384 | 385 | .hljs-deletion { 386 | color: #bd2c00; 387 | background-color: #ffecec; 388 | } 389 | 390 | .hljs-link { 391 | text-decoration: underline; 392 | } 393 | 394 | /* 代码高亮 */ 395 | /* PrismJS 1.15.0 396 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 397 | /** 398 | * prism.js default theme for JavaScript, CSS and HTML 399 | * Based on dabblet (http://dabblet.com) 400 | * @author Lea Verou 401 | */ 402 | code[class*="language-"], 403 | pre[class*="language-"] { 404 | color: black; 405 | background: none; 406 | text-shadow: 0 1px white; 407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 408 | text-align: left; 409 | white-space: pre; 410 | word-spacing: normal; 411 | word-break: normal; 412 | word-wrap: normal; 413 | line-height: 1.5; 414 | 415 | -moz-tab-size: 4; 416 | -o-tab-size: 4; 417 | tab-size: 4; 418 | 419 | -webkit-hyphens: none; 420 | -moz-hyphens: none; 421 | -ms-hyphens: none; 422 | hyphens: none; 423 | } 424 | 425 | pre[class*="language-"]::-moz-selection, 426 | pre[class*="language-"] ::-moz-selection, 427 | code[class*="language-"]::-moz-selection, 428 | code[class*="language-"] ::-moz-selection { 429 | text-shadow: none; 430 | background: #b3d4fc; 431 | } 432 | 433 | pre[class*="language-"]::selection, 434 | pre[class*="language-"] ::selection, 435 | code[class*="language-"]::selection, 436 | code[class*="language-"] ::selection { 437 | text-shadow: none; 438 | background: #b3d4fc; 439 | } 440 | 441 | @media print { 442 | 443 | code[class*="language-"], 444 | pre[class*="language-"] { 445 | text-shadow: none; 446 | } 447 | } 448 | 449 | /* Code blocks */ 450 | pre[class*="language-"] { 451 | padding: 1em; 452 | margin: .5em 0; 453 | overflow: auto; 454 | } 455 | 456 | :not(pre)>code[class*="language-"], 457 | pre[class*="language-"] { 458 | background: #f5f2f0; 459 | } 460 | 461 | /* Inline code */ 462 | :not(pre)>code[class*="language-"] { 463 | padding: .1em; 464 | border-radius: .3em; 465 | white-space: normal; 466 | } 467 | 468 | .token.comment, 469 | .token.prolog, 470 | .token.doctype, 471 | .token.cdata { 472 | color: slategray; 473 | } 474 | 475 | .token.punctuation { 476 | color: #999; 477 | } 478 | 479 | .namespace { 480 | opacity: .7; 481 | } 482 | 483 | .token.property, 484 | .token.tag, 485 | .token.boolean, 486 | .token.number, 487 | .token.constant, 488 | .token.symbol, 489 | .token.deleted { 490 | color: #905; 491 | } 492 | 493 | .token.selector, 494 | .token.attr-name, 495 | .token.string, 496 | .token.char, 497 | .token.builtin, 498 | .token.inserted { 499 | color: #690; 500 | } 501 | 502 | .token.operator, 503 | .token.entity, 504 | .token.url, 505 | .language-css .token.string, 506 | .style .token.string { 507 | color: #9a6e3a; 508 | background: hsla(0, 0%, 100%, .5); 509 | } 510 | 511 | .token.atrule, 512 | .token.attr-value, 513 | .token.keyword { 514 | color: #07a; 515 | } 516 | 517 | .token.function, 518 | .token.class-name { 519 | color: #DD4A68; 520 | } 521 | 522 | .token.regex, 523 | .token.important, 524 | .token.variable { 525 | color: #e90; 526 | } 527 | 528 | .token.important, 529 | .token.bold { 530 | font-weight: bold; 531 | } 532 | 533 | .token.italic { 534 | font-style: italic; 535 | } 536 | 537 | .token.entity { 538 | cursor: help; 539 | } 540 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 4375727 */ 3 | src: url('iconfont.woff2?t=1736222146368') format('woff2'), 4 | url('iconfont.woff?t=1736222146368') format('woff'), 5 | url('iconfont.ttf?t=1736222146368') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-ble:before { 17 | content: "\e6b1"; 18 | } 19 | 20 | .icon-vibrate:before { 21 | content: "\e654"; 22 | } 23 | 24 | .icon-rgb:before { 25 | content: "\e655"; 26 | } 27 | 28 | .icon-home_light:before { 29 | content: "\e7d4"; 30 | } 31 | 32 | .icon-display:before { 33 | content: "\e6be"; 34 | } 35 | 36 | .icon-do:before { 37 | content: "\e6bf"; 38 | } 39 | 40 | .icon-heater:before { 41 | content: "\e6c1"; 42 | } 43 | 44 | .icon-light3:before { 45 | content: "\e6c2"; 46 | } 47 | 48 | .icon-return:before { 49 | content: "\e6c4"; 50 | } 51 | 52 | .icon-v-2:before { 53 | content: "\e6c5"; 54 | } 55 | 56 | .icon-edit:before { 57 | content: "\e6c6"; 58 | } 59 | 60 | .icon-pressure:before { 61 | content: "\e6c7"; 62 | } 63 | 64 | .icon-id:before { 65 | content: "\e6c8"; 66 | } 67 | 68 | .icon-windmill2:before { 69 | content: "\e6ca"; 70 | } 71 | 72 | .icon-a-v:before { 73 | content: "\e6cb"; 74 | } 75 | 76 | .icon-other:before { 77 | content: "\e6cc"; 78 | } 79 | 80 | .icon-switch1:before { 81 | content: "\e6cd"; 82 | } 83 | 84 | .icon-play:before { 85 | content: "\e67b"; 86 | } 87 | 88 | .icon-v:before { 89 | content: "\e67c"; 90 | } 91 | 92 | .icon-v-:before { 93 | content: "\e67d"; 94 | } 95 | 96 | .icon-up:before { 97 | content: "\e67e"; 98 | } 99 | 100 | .icon-switch:before { 101 | content: "\e67f"; 102 | } 103 | 104 | .icon-joystick:before { 105 | content: "\e643"; 106 | } 107 | 108 | .icon-smarthome:before { 109 | content: "\e644"; 110 | } 111 | 112 | .icon-fan:before { 113 | content: "\e645"; 114 | } 115 | 116 | .icon-light:before { 117 | content: "\e646"; 118 | } 119 | 120 | .icon-light2:before { 121 | content: "\e647"; 122 | } 123 | 124 | .icon-pipe:before { 125 | content: "\e648"; 126 | } 127 | 128 | .icon-door:before { 129 | content: "\e649"; 130 | } 131 | 132 | .icon-water:before { 133 | content: "\e64a"; 134 | } 135 | 136 | .icon-setting:before { 137 | content: "\e64b"; 138 | } 139 | 140 | .icon-farming:before { 141 | content: "\e64c"; 142 | } 143 | 144 | .icon-windmill:before { 145 | content: "\e64d"; 146 | } 147 | 148 | .icon-curtain:before { 149 | content: "\e64e"; 150 | } 151 | 152 | .icon-remote-control:before { 153 | content: "\e64f"; 154 | } 155 | 156 | .icon-weather-station:before { 157 | content: "\e650"; 158 | } 159 | 160 | .icon-current:before { 161 | content: "\e651"; 162 | } 163 | 164 | .icon-gsr:before { 165 | content: "\e652"; 166 | } 167 | 168 | .icon-count:before { 169 | content: "\e653"; 170 | } 171 | 172 | .icon-color:before { 173 | content: "\e656"; 174 | } 175 | 176 | .icon-humidity:before { 177 | content: "\e657"; 178 | } 179 | 180 | .icon-fan1:before { 181 | content: "\e659"; 182 | } 183 | 184 | .icon-ph:before { 185 | content: "\e65b"; 186 | } 187 | 188 | .icon-illumination:before { 189 | content: "\e65c"; 190 | } 191 | 192 | .icon-co2:before { 193 | content: "\e65d"; 194 | } 195 | 196 | .icon-music:before { 197 | content: "\e65e"; 198 | } 199 | 200 | .icon-bloodpressure:before { 201 | content: "\e65f"; 202 | } 203 | 204 | .icon-heartrate:before { 205 | content: "\e661"; 206 | } 207 | 208 | .icon-bodytemperature:before { 209 | content: "\e662"; 210 | } 211 | 212 | .icon-soil_moisture:before { 213 | content: "\e663"; 214 | } 215 | 216 | .icon-sleep:before { 217 | content: "\e664"; 218 | } 219 | 220 | .icon-smoke:before { 221 | content: "\e665"; 222 | } 223 | 224 | .icon-voiceplay:before { 225 | content: "\e667"; 226 | } 227 | 228 | .icon-bloodoxygen:before { 229 | content: "\e668"; 230 | } 231 | 232 | .icon-temperature:before { 233 | content: "\e66a"; 234 | } 235 | 236 | .icon-sun:before { 237 | content: "\e66b"; 238 | } 239 | 240 | .icon-power:before { 241 | content: "\e66c"; 242 | } 243 | 244 | .icon-pm:before { 245 | content: "\e66d"; 246 | } 247 | 248 | .icon-voltage:before { 249 | content: "\e66e"; 250 | } 251 | 252 | .icon-wind-direction:before { 253 | content: "\e66f"; 254 | } 255 | 256 | .icon-rainfall:before { 257 | content: "\e670"; 258 | } 259 | 260 | .icon-waterpump:before { 261 | content: "\e671"; 262 | } 263 | 264 | .icon-wind-speed:before { 265 | content: "\e672"; 266 | } 267 | 268 | .icon-water-level:before { 269 | content: "\e673"; 270 | } 271 | 272 | .icon-noise:before { 273 | content: "\e674"; 274 | } 275 | 276 | .icon-weight:before { 277 | content: "\e675"; 278 | } 279 | 280 | .icon-ultraviolet:before { 281 | content: "\e677"; 282 | } 283 | 284 | .icon-environment:before { 285 | content: "\e678"; 286 | } 287 | 288 | .icon-industry:before { 289 | content: "\e679"; 290 | } 291 | 292 | .icon-health:before { 293 | content: "\e67a"; 294 | } 295 | 296 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4375727", 3 | "name": "web-ble", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "43063520", 10 | "name": "ble", 11 | "font_class": "ble", 12 | "unicode": "e6b1", 13 | "unicode_decimal": 59057 14 | }, 15 | { 16 | "icon_id": "38612481", 17 | "name": "vibrate", 18 | "font_class": "vibrate", 19 | "unicode": "e654", 20 | "unicode_decimal": 58964 21 | }, 22 | { 23 | "icon_id": "38612490", 24 | "name": "rgb", 25 | "font_class": "rgb", 26 | "unicode": "e655", 27 | "unicode_decimal": 58965 28 | }, 29 | { 30 | "icon_id": "1492012", 31 | "name": "home_light", 32 | "font_class": "home_light", 33 | "unicode": "e7d4", 34 | "unicode_decimal": 59348 35 | }, 36 | { 37 | "icon_id": "38611481", 38 | "name": "display", 39 | "font_class": "display", 40 | "unicode": "e6be", 41 | "unicode_decimal": 59070 42 | }, 43 | { 44 | "icon_id": "38611482", 45 | "name": "do", 46 | "font_class": "do", 47 | "unicode": "e6bf", 48 | "unicode_decimal": 59071 49 | }, 50 | { 51 | "icon_id": "38611485", 52 | "name": "heater", 53 | "font_class": "heater", 54 | "unicode": "e6c1", 55 | "unicode_decimal": 59073 56 | }, 57 | { 58 | "icon_id": "38611487", 59 | "name": "light3", 60 | "font_class": "light3", 61 | "unicode": "e6c2", 62 | "unicode_decimal": 59074 63 | }, 64 | { 65 | "icon_id": "38611491", 66 | "name": "return", 67 | "font_class": "return", 68 | "unicode": "e6c4", 69 | "unicode_decimal": 59076 70 | }, 71 | { 72 | "icon_id": "38611492", 73 | "name": "v-", 74 | "font_class": "v-2", 75 | "unicode": "e6c5", 76 | "unicode_decimal": 59077 77 | }, 78 | { 79 | "icon_id": "38611493", 80 | "name": "edit", 81 | "font_class": "edit", 82 | "unicode": "e6c6", 83 | "unicode_decimal": 59078 84 | }, 85 | { 86 | "icon_id": "38611496", 87 | "name": "pressure", 88 | "font_class": "pressure", 89 | "unicode": "e6c7", 90 | "unicode_decimal": 59079 91 | }, 92 | { 93 | "icon_id": "38611498", 94 | "name": "id", 95 | "font_class": "id", 96 | "unicode": "e6c8", 97 | "unicode_decimal": 59080 98 | }, 99 | { 100 | "icon_id": "38611505", 101 | "name": "windmill", 102 | "font_class": "windmill2", 103 | "unicode": "e6ca", 104 | "unicode_decimal": 59082 105 | }, 106 | { 107 | "icon_id": "38611506", 108 | "name": "v+", 109 | "font_class": "a-v", 110 | "unicode": "e6cb", 111 | "unicode_decimal": 59083 112 | }, 113 | { 114 | "icon_id": "38611507", 115 | "name": "other", 116 | "font_class": "other", 117 | "unicode": "e6cc", 118 | "unicode_decimal": 59084 119 | }, 120 | { 121 | "icon_id": "38611508", 122 | "name": "switch1", 123 | "font_class": "switch1", 124 | "unicode": "e6cd", 125 | "unicode_decimal": 59085 126 | }, 127 | { 128 | "icon_id": "38567626", 129 | "name": "play", 130 | "font_class": "play", 131 | "unicode": "e67b", 132 | "unicode_decimal": 59003 133 | }, 134 | { 135 | "icon_id": "38567627", 136 | "name": "v", 137 | "font_class": "v", 138 | "unicode": "e67c", 139 | "unicode_decimal": 59004 140 | }, 141 | { 142 | "icon_id": "38567628", 143 | "name": "v-", 144 | "font_class": "v-", 145 | "unicode": "e67d", 146 | "unicode_decimal": 59005 147 | }, 148 | { 149 | "icon_id": "38567629", 150 | "name": "up", 151 | "font_class": "up", 152 | "unicode": "e67e", 153 | "unicode_decimal": 59006 154 | }, 155 | { 156 | "icon_id": "38567630", 157 | "name": "switch", 158 | "font_class": "switch", 159 | "unicode": "e67f", 160 | "unicode_decimal": 59007 161 | }, 162 | { 163 | "icon_id": "38286428", 164 | "name": "joystick", 165 | "font_class": "joystick", 166 | "unicode": "e643", 167 | "unicode_decimal": 58947 168 | }, 169 | { 170 | "icon_id": "38286431", 171 | "name": "smarthome", 172 | "font_class": "smarthome", 173 | "unicode": "e644", 174 | "unicode_decimal": 58948 175 | }, 176 | { 177 | "icon_id": "38286436", 178 | "name": "fan", 179 | "font_class": "fan", 180 | "unicode": "e645", 181 | "unicode_decimal": 58949 182 | }, 183 | { 184 | "icon_id": "38286437", 185 | "name": "light", 186 | "font_class": "light", 187 | "unicode": "e646", 188 | "unicode_decimal": 58950 189 | }, 190 | { 191 | "icon_id": "38286438", 192 | "name": "light2", 193 | "font_class": "light2", 194 | "unicode": "e647", 195 | "unicode_decimal": 58951 196 | }, 197 | { 198 | "icon_id": "38286439", 199 | "name": "pipe", 200 | "font_class": "pipe", 201 | "unicode": "e648", 202 | "unicode_decimal": 58952 203 | }, 204 | { 205 | "icon_id": "38286440", 206 | "name": "door", 207 | "font_class": "door", 208 | "unicode": "e649", 209 | "unicode_decimal": 58953 210 | }, 211 | { 212 | "icon_id": "38286441", 213 | "name": "water", 214 | "font_class": "water", 215 | "unicode": "e64a", 216 | "unicode_decimal": 58954 217 | }, 218 | { 219 | "icon_id": "38286442", 220 | "name": "setting", 221 | "font_class": "setting", 222 | "unicode": "e64b", 223 | "unicode_decimal": 58955 224 | }, 225 | { 226 | "icon_id": "38286444", 227 | "name": "farming", 228 | "font_class": "farming", 229 | "unicode": "e64c", 230 | "unicode_decimal": 58956 231 | }, 232 | { 233 | "icon_id": "38286445", 234 | "name": "windmill", 235 | "font_class": "windmill", 236 | "unicode": "e64d", 237 | "unicode_decimal": 58957 238 | }, 239 | { 240 | "icon_id": "38286447", 241 | "name": "curtain", 242 | "font_class": "curtain", 243 | "unicode": "e64e", 244 | "unicode_decimal": 58958 245 | }, 246 | { 247 | "icon_id": "38567631", 248 | "name": "remote-control", 249 | "font_class": "remote-control", 250 | "unicode": "e64f", 251 | "unicode_decimal": 58959 252 | }, 253 | { 254 | "icon_id": "38567632", 255 | "name": "weather-station", 256 | "font_class": "weather-station", 257 | "unicode": "e650", 258 | "unicode_decimal": 58960 259 | }, 260 | { 261 | "icon_id": "38587488", 262 | "name": "current", 263 | "font_class": "current", 264 | "unicode": "e651", 265 | "unicode_decimal": 58961 266 | }, 267 | { 268 | "icon_id": "38587489", 269 | "name": "gsr", 270 | "font_class": "gsr", 271 | "unicode": "e652", 272 | "unicode_decimal": 58962 273 | }, 274 | { 275 | "icon_id": "38587490", 276 | "name": "count", 277 | "font_class": "count", 278 | "unicode": "e653", 279 | "unicode_decimal": 58963 280 | }, 281 | { 282 | "icon_id": "38587493", 283 | "name": "color", 284 | "font_class": "color", 285 | "unicode": "e656", 286 | "unicode_decimal": 58966 287 | }, 288 | { 289 | "icon_id": "38587494", 290 | "name": "humidity", 291 | "font_class": "humidity", 292 | "unicode": "e657", 293 | "unicode_decimal": 58967 294 | }, 295 | { 296 | "icon_id": "38587496", 297 | "name": "fan", 298 | "font_class": "fan1", 299 | "unicode": "e659", 300 | "unicode_decimal": 58969 301 | }, 302 | { 303 | "icon_id": "38587498", 304 | "name": "ph", 305 | "font_class": "ph", 306 | "unicode": "e65b", 307 | "unicode_decimal": 58971 308 | }, 309 | { 310 | "icon_id": "38587499", 311 | "name": "illumination", 312 | "font_class": "illumination", 313 | "unicode": "e65c", 314 | "unicode_decimal": 58972 315 | }, 316 | { 317 | "icon_id": "38587500", 318 | "name": "co2", 319 | "font_class": "co2", 320 | "unicode": "e65d", 321 | "unicode_decimal": 58973 322 | }, 323 | { 324 | "icon_id": "38587501", 325 | "name": "music", 326 | "font_class": "music", 327 | "unicode": "e65e", 328 | "unicode_decimal": 58974 329 | }, 330 | { 331 | "icon_id": "38587502", 332 | "name": "bloodpressure", 333 | "font_class": "bloodpressure", 334 | "unicode": "e65f", 335 | "unicode_decimal": 58975 336 | }, 337 | { 338 | "icon_id": "38587504", 339 | "name": "heartrate", 340 | "font_class": "heartrate", 341 | "unicode": "e661", 342 | "unicode_decimal": 58977 343 | }, 344 | { 345 | "icon_id": "38587505", 346 | "name": "bodytemperature", 347 | "font_class": "bodytemperature", 348 | "unicode": "e662", 349 | "unicode_decimal": 58978 350 | }, 351 | { 352 | "icon_id": "38587506", 353 | "name": "soil_moisture", 354 | "font_class": "soil_moisture", 355 | "unicode": "e663", 356 | "unicode_decimal": 58979 357 | }, 358 | { 359 | "icon_id": "38587507", 360 | "name": "sleep", 361 | "font_class": "sleep", 362 | "unicode": "e664", 363 | "unicode_decimal": 58980 364 | }, 365 | { 366 | "icon_id": "38587508", 367 | "name": "smoke", 368 | "font_class": "smoke", 369 | "unicode": "e665", 370 | "unicode_decimal": 58981 371 | }, 372 | { 373 | "icon_id": "38587510", 374 | "name": "voiceplay", 375 | "font_class": "voiceplay", 376 | "unicode": "e667", 377 | "unicode_decimal": 58983 378 | }, 379 | { 380 | "icon_id": "38587511", 381 | "name": "bloodoxygen", 382 | "font_class": "bloodoxygen", 383 | "unicode": "e668", 384 | "unicode_decimal": 58984 385 | }, 386 | { 387 | "icon_id": "38587513", 388 | "name": "temperature", 389 | "font_class": "temperature", 390 | "unicode": "e66a", 391 | "unicode_decimal": 58986 392 | }, 393 | { 394 | "icon_id": "38587514", 395 | "name": "sun", 396 | "font_class": "sun", 397 | "unicode": "e66b", 398 | "unicode_decimal": 58987 399 | }, 400 | { 401 | "icon_id": "38587515", 402 | "name": "power", 403 | "font_class": "power", 404 | "unicode": "e66c", 405 | "unicode_decimal": 58988 406 | }, 407 | { 408 | "icon_id": "38587516", 409 | "name": "pm", 410 | "font_class": "pm", 411 | "unicode": "e66d", 412 | "unicode_decimal": 58989 413 | }, 414 | { 415 | "icon_id": "38587517", 416 | "name": "voltage", 417 | "font_class": "voltage", 418 | "unicode": "e66e", 419 | "unicode_decimal": 58990 420 | }, 421 | { 422 | "icon_id": "38587518", 423 | "name": "wind-direction", 424 | "font_class": "wind-direction", 425 | "unicode": "e66f", 426 | "unicode_decimal": 58991 427 | }, 428 | { 429 | "icon_id": "38587519", 430 | "name": "rainfall", 431 | "font_class": "rainfall", 432 | "unicode": "e670", 433 | "unicode_decimal": 58992 434 | }, 435 | { 436 | "icon_id": "38587520", 437 | "name": "waterpump", 438 | "font_class": "waterpump", 439 | "unicode": "e671", 440 | "unicode_decimal": 58993 441 | }, 442 | { 443 | "icon_id": "38587521", 444 | "name": "wind-speed", 445 | "font_class": "wind-speed", 446 | "unicode": "e672", 447 | "unicode_decimal": 58994 448 | }, 449 | { 450 | "icon_id": "38587522", 451 | "name": "water-level", 452 | "font_class": "water-level", 453 | "unicode": "e673", 454 | "unicode_decimal": 58995 455 | }, 456 | { 457 | "icon_id": "38587523", 458 | "name": "noise", 459 | "font_class": "noise", 460 | "unicode": "e674", 461 | "unicode_decimal": 58996 462 | }, 463 | { 464 | "icon_id": "38587524", 465 | "name": "weight", 466 | "font_class": "weight", 467 | "unicode": "e675", 468 | "unicode_decimal": 58997 469 | }, 470 | { 471 | "icon_id": "38587526", 472 | "name": "ultraviolet", 473 | "font_class": "ultraviolet", 474 | "unicode": "e677", 475 | "unicode_decimal": 58999 476 | }, 477 | { 478 | "icon_id": "38593484", 479 | "name": "environment", 480 | "font_class": "environment", 481 | "unicode": "e678", 482 | "unicode_decimal": 59000 483 | }, 484 | { 485 | "icon_id": "38593485", 486 | "name": "industry", 487 | "font_class": "industry", 488 | "unicode": "e679", 489 | "unicode_decimal": 59001 490 | }, 491 | { 492 | "icon_id": "38593486", 493 | "name": "health", 494 | "font_class": "health", 495 | "unicode": "e67a", 496 | "unicode_decimal": 59002 497 | } 498 | ] 499 | } 500 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/img/colorpicker.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/colorpicker.webp -------------------------------------------------------------------------------- /src/assets/img/environment-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/environment-bg.webp -------------------------------------------------------------------------------- /src/assets/img/health-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/health-bg.webp -------------------------------------------------------------------------------- /src/assets/img/industry-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/industry-bg.webp -------------------------------------------------------------------------------- /src/assets/img/joyout.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/joyout.webp -------------------------------------------------------------------------------- /src/assets/img/joystick.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/joystick.webp -------------------------------------------------------------------------------- /src/assets/img/js-btn.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/js-btn.webp -------------------------------------------------------------------------------- /src/assets/img/js-btn2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/js-btn2.webp -------------------------------------------------------------------------------- /src/assets/img/smartfarming-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/smartfarming-bg.webp -------------------------------------------------------------------------------- /src/assets/img/smarthome-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/smarthome-bg.webp -------------------------------------------------------------------------------- /src/assets/img/weatherstation-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/img/weatherstation-bg.webp -------------------------------------------------------------------------------- /src/assets/iphone.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/iphone.webp -------------------------------------------------------------------------------- /src/assets/oj-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/assets/oj-logo.png -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinker-iot/web-ble/8b80b41a5e9ddf0dc6274b0b9983d6c5e2880468/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebBle 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "蓝牙物联网", 3 | "short_name": "web-ble", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/icon-152x152.png", 36 | "sizes": "152x152", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/icon-192x192.png", 42 | "sizes": "192x192", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "assets/icons/icon-384x384.png", 48 | "sizes": "384x384", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "assets/icons/icon-512x512.png", 54 | "sizes": "512x512", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | body{ 3 | margin: 0; 4 | overflow: hidden; 5 | width: 100vw; 6 | height: 100vh; 7 | } -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": false, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "esModuleInterop": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "useDefineForClassFields": false, 21 | "lib": [ 22 | "ES2022", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------