├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------