rvt-monitor 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rvt_monitor-0.2.4/PKG-INFO +182 -0
- rvt_monitor-0.2.4/README.md +168 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/pyproject.toml +1 -1
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/manager.py +18 -39
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/protocol.py +22 -27
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/static/css/style.css +20 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/static/js/app.js +54 -16
- rvt_monitor-0.2.3/PKG-INFO +0 -31
- rvt_monitor-0.2.3/README.md +0 -17
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/.gitignore +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/__init__.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/__main__.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/__init__.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/profiles.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/scanner.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/core/__init__.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/core/logger.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/__init__.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/cli.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/protocol.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/serial_port.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/server.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/static/index.html +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/main.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/__init__.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/app.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/__init__.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/config.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/control.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/device.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/logs.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/state.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/websocket.py +0 -0
- {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/static/index.html +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rvt-monitor
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: BLE Device Monitor for Ceily/Wally
|
|
5
|
+
Author-email: Rovothome <chandler.kim@rovothome.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: bleak>=0.21.0
|
|
9
|
+
Requires-Dist: fastapi>=0.109.0
|
|
10
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
11
|
+
Requires-Dist: pyserial>=3.5
|
|
12
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# RVT-Monitor
|
|
16
|
+
|
|
17
|
+
BLE Device Monitor and Control Tool for Ceily/Wally devices.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install rvt-monitor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install from source:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd rvt-monitor
|
|
29
|
+
pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
rvt-monitor
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Opens web UI at http://127.0.0.1:8000
|
|
39
|
+
|
|
40
|
+
## BLE Protocol Reference
|
|
41
|
+
|
|
42
|
+
### Device Information Service (0x180A)
|
|
43
|
+
|
|
44
|
+
Standard BLE SIG service for device identification.
|
|
45
|
+
|
|
46
|
+
| Characteristic | UUID | Format | Example |
|
|
47
|
+
|----------------|------|--------|---------|
|
|
48
|
+
| Model Number | 0x2A24 | `{device}-p{protocol}` | `ceily-p2`, `wally-p2` |
|
|
49
|
+
| Hardware Revision | 0x2A27 | `v{version}` | `v0`, `v1` |
|
|
50
|
+
| Firmware Revision | 0x2A26 | semver | `2.0.0` |
|
|
51
|
+
|
|
52
|
+
### Custom Services
|
|
53
|
+
|
|
54
|
+
| Service | UUID | Description |
|
|
55
|
+
|---------|------|-------------|
|
|
56
|
+
| Control | `0000ff20-...` | Motion control |
|
|
57
|
+
| System Status | `501a8cf5-98a7-4370-bb97-632c84910000` | Device state |
|
|
58
|
+
| Dimension | `cc9762a6-bbb7-4b21-b2e2-153059030000` | Device config |
|
|
59
|
+
| LED | `07ecc81b-b952-47e6-a1f1-0999577f0000` | LED control |
|
|
60
|
+
| Log | `cc9762a6-bbb7-4b21-b2e2-153059032200` | Event logs |
|
|
61
|
+
|
|
62
|
+
### Motion Commands
|
|
63
|
+
|
|
64
|
+
Write to `MOTION_CONTROL` characteristic (`0x0000ff21-...`):
|
|
65
|
+
|
|
66
|
+
| Command | Value | Ceily | Wally |
|
|
67
|
+
|---------|-------|-------|-------|
|
|
68
|
+
| STOP | 0x00 | Stop | Stop |
|
|
69
|
+
| UP/OPEN | 0x01 | Up | Open |
|
|
70
|
+
| DOWN/CLOSE | 0x02 | Down | Close |
|
|
71
|
+
|
|
72
|
+
### Motion States
|
|
73
|
+
|
|
74
|
+
Read from `SYSTEM_STATUS` characteristic (byte 0):
|
|
75
|
+
|
|
76
|
+
| State | Value | Description |
|
|
77
|
+
|-------|-------|-------------|
|
|
78
|
+
| DOWN/CLOSED | 0 | At bottom/closed position |
|
|
79
|
+
| UP/OPENED | 1 | At top/opened position |
|
|
80
|
+
| MOVING_DOWN | 2 | Moving down/closing |
|
|
81
|
+
| MOVING_UP | 3 | Moving up/opening |
|
|
82
|
+
| STOP | 4 | Stopped mid-motion |
|
|
83
|
+
| EMERGENCY | 5 | Emergency stop |
|
|
84
|
+
| INIT | 255 | Initializing |
|
|
85
|
+
|
|
86
|
+
## Protocol Version Detection
|
|
87
|
+
|
|
88
|
+
### By Device Info Service
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from bleak import BleakClient
|
|
92
|
+
|
|
93
|
+
MODEL_NUMBER_UUID = "00002a24-0000-1000-8000-00805f9b34fb"
|
|
94
|
+
|
|
95
|
+
async def detect_protocol(client: BleakClient) -> int:
|
|
96
|
+
"""Detect protocol version from Model Number."""
|
|
97
|
+
try:
|
|
98
|
+
data = await client.read_gatt_char(MODEL_NUMBER_UUID)
|
|
99
|
+
model = data.decode("utf-8").rstrip("\x00")
|
|
100
|
+
# Format: "ceily-p2" or "wally-p2"
|
|
101
|
+
if "-p" in model:
|
|
102
|
+
return int(model.split("-p")[1])
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
return 1 # Legacy fallback
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### By Service Discovery
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
DEVICE_INFO_SERVICE = "0000180a-0000-1000-8000-00805f9b34fb"
|
|
112
|
+
|
|
113
|
+
async def detect_by_services(client: BleakClient) -> int:
|
|
114
|
+
"""Detect by checking for Device Info Service."""
|
|
115
|
+
for service in client.services:
|
|
116
|
+
if service.uuid.lower() == DEVICE_INFO_SERVICE:
|
|
117
|
+
return 2
|
|
118
|
+
return 1 # Legacy (no Device Info Service)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Example: Connect and Control
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from bleak import BleakClient
|
|
125
|
+
|
|
126
|
+
MOTION_CONTROL_UUID = "0000ff21-0000-1000-8000-00805f9b34fb"
|
|
127
|
+
SYSTEM_STATUS_UUID = "501a8cf5-98a7-4370-bb97-632c84910001"
|
|
128
|
+
|
|
129
|
+
async def control_device(address: str):
|
|
130
|
+
async with BleakClient(address) as client:
|
|
131
|
+
# Read status
|
|
132
|
+
data = await client.read_gatt_char(SYSTEM_STATUS_UUID)
|
|
133
|
+
state = data[0]
|
|
134
|
+
print(f"Current state: {state}")
|
|
135
|
+
|
|
136
|
+
# Send UP command
|
|
137
|
+
await client.write_gatt_char(MOTION_CONTROL_UUID, bytes([0x01]))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Project Structure
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
rvt-monitor/
|
|
144
|
+
├── rvt_monitor/
|
|
145
|
+
│ ├── ble/
|
|
146
|
+
│ │ ├── manager.py # BLE connection management
|
|
147
|
+
│ │ ├── profiles.py # Protocol version profiles
|
|
148
|
+
│ │ ├── protocol.py # UUID definitions & parsers
|
|
149
|
+
│ │ └── scanner.py # Device discovery
|
|
150
|
+
│ ├── server/
|
|
151
|
+
│ │ ├── app.py # FastAPI server
|
|
152
|
+
│ │ └── routes/ # API endpoints
|
|
153
|
+
│ └── static/ # Web UI
|
|
154
|
+
└── pyproject.toml
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Publishing to PyPI
|
|
158
|
+
|
|
159
|
+
### Via Git Tag (Recommended)
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Update version in pyproject.toml first
|
|
163
|
+
git add pyproject.toml
|
|
164
|
+
git commit -m "chore(rvt-monitor): Bump version to x.y.z"
|
|
165
|
+
|
|
166
|
+
# Create and push tag
|
|
167
|
+
git tag rvt-monitor-vX.Y.Z
|
|
168
|
+
git push origin main --tags
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
CI/CD will automatically build and publish to PyPI.
|
|
172
|
+
|
|
173
|
+
### Manual Trigger
|
|
174
|
+
|
|
175
|
+
1. Go to GitHub → Actions
|
|
176
|
+
2. Select "Publish rvt-monitor to PyPI"
|
|
177
|
+
3. Click "Run workflow"
|
|
178
|
+
|
|
179
|
+
## Related
|
|
180
|
+
|
|
181
|
+
- Firmware: `v1/common_components/` - Device-side BLE implementation
|
|
182
|
+
- OTA Tool: `ota/ota.py` - Firmware deployment
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# RVT-Monitor
|
|
2
|
+
|
|
3
|
+
BLE Device Monitor and Control Tool for Ceily/Wally devices.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install rvt-monitor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd rvt-monitor
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
rvt-monitor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Opens web UI at http://127.0.0.1:8000
|
|
25
|
+
|
|
26
|
+
## BLE Protocol Reference
|
|
27
|
+
|
|
28
|
+
### Device Information Service (0x180A)
|
|
29
|
+
|
|
30
|
+
Standard BLE SIG service for device identification.
|
|
31
|
+
|
|
32
|
+
| Characteristic | UUID | Format | Example |
|
|
33
|
+
|----------------|------|--------|---------|
|
|
34
|
+
| Model Number | 0x2A24 | `{device}-p{protocol}` | `ceily-p2`, `wally-p2` |
|
|
35
|
+
| Hardware Revision | 0x2A27 | `v{version}` | `v0`, `v1` |
|
|
36
|
+
| Firmware Revision | 0x2A26 | semver | `2.0.0` |
|
|
37
|
+
|
|
38
|
+
### Custom Services
|
|
39
|
+
|
|
40
|
+
| Service | UUID | Description |
|
|
41
|
+
|---------|------|-------------|
|
|
42
|
+
| Control | `0000ff20-...` | Motion control |
|
|
43
|
+
| System Status | `501a8cf5-98a7-4370-bb97-632c84910000` | Device state |
|
|
44
|
+
| Dimension | `cc9762a6-bbb7-4b21-b2e2-153059030000` | Device config |
|
|
45
|
+
| LED | `07ecc81b-b952-47e6-a1f1-0999577f0000` | LED control |
|
|
46
|
+
| Log | `cc9762a6-bbb7-4b21-b2e2-153059032200` | Event logs |
|
|
47
|
+
|
|
48
|
+
### Motion Commands
|
|
49
|
+
|
|
50
|
+
Write to `MOTION_CONTROL` characteristic (`0x0000ff21-...`):
|
|
51
|
+
|
|
52
|
+
| Command | Value | Ceily | Wally |
|
|
53
|
+
|---------|-------|-------|-------|
|
|
54
|
+
| STOP | 0x00 | Stop | Stop |
|
|
55
|
+
| UP/OPEN | 0x01 | Up | Open |
|
|
56
|
+
| DOWN/CLOSE | 0x02 | Down | Close |
|
|
57
|
+
|
|
58
|
+
### Motion States
|
|
59
|
+
|
|
60
|
+
Read from `SYSTEM_STATUS` characteristic (byte 0):
|
|
61
|
+
|
|
62
|
+
| State | Value | Description |
|
|
63
|
+
|-------|-------|-------------|
|
|
64
|
+
| DOWN/CLOSED | 0 | At bottom/closed position |
|
|
65
|
+
| UP/OPENED | 1 | At top/opened position |
|
|
66
|
+
| MOVING_DOWN | 2 | Moving down/closing |
|
|
67
|
+
| MOVING_UP | 3 | Moving up/opening |
|
|
68
|
+
| STOP | 4 | Stopped mid-motion |
|
|
69
|
+
| EMERGENCY | 5 | Emergency stop |
|
|
70
|
+
| INIT | 255 | Initializing |
|
|
71
|
+
|
|
72
|
+
## Protocol Version Detection
|
|
73
|
+
|
|
74
|
+
### By Device Info Service
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from bleak import BleakClient
|
|
78
|
+
|
|
79
|
+
MODEL_NUMBER_UUID = "00002a24-0000-1000-8000-00805f9b34fb"
|
|
80
|
+
|
|
81
|
+
async def detect_protocol(client: BleakClient) -> int:
|
|
82
|
+
"""Detect protocol version from Model Number."""
|
|
83
|
+
try:
|
|
84
|
+
data = await client.read_gatt_char(MODEL_NUMBER_UUID)
|
|
85
|
+
model = data.decode("utf-8").rstrip("\x00")
|
|
86
|
+
# Format: "ceily-p2" or "wally-p2"
|
|
87
|
+
if "-p" in model:
|
|
88
|
+
return int(model.split("-p")[1])
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
return 1 # Legacy fallback
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### By Service Discovery
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
DEVICE_INFO_SERVICE = "0000180a-0000-1000-8000-00805f9b34fb"
|
|
98
|
+
|
|
99
|
+
async def detect_by_services(client: BleakClient) -> int:
|
|
100
|
+
"""Detect by checking for Device Info Service."""
|
|
101
|
+
for service in client.services:
|
|
102
|
+
if service.uuid.lower() == DEVICE_INFO_SERVICE:
|
|
103
|
+
return 2
|
|
104
|
+
return 1 # Legacy (no Device Info Service)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Example: Connect and Control
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from bleak import BleakClient
|
|
111
|
+
|
|
112
|
+
MOTION_CONTROL_UUID = "0000ff21-0000-1000-8000-00805f9b34fb"
|
|
113
|
+
SYSTEM_STATUS_UUID = "501a8cf5-98a7-4370-bb97-632c84910001"
|
|
114
|
+
|
|
115
|
+
async def control_device(address: str):
|
|
116
|
+
async with BleakClient(address) as client:
|
|
117
|
+
# Read status
|
|
118
|
+
data = await client.read_gatt_char(SYSTEM_STATUS_UUID)
|
|
119
|
+
state = data[0]
|
|
120
|
+
print(f"Current state: {state}")
|
|
121
|
+
|
|
122
|
+
# Send UP command
|
|
123
|
+
await client.write_gatt_char(MOTION_CONTROL_UUID, bytes([0x01]))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Project Structure
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
rvt-monitor/
|
|
130
|
+
├── rvt_monitor/
|
|
131
|
+
│ ├── ble/
|
|
132
|
+
│ │ ├── manager.py # BLE connection management
|
|
133
|
+
│ │ ├── profiles.py # Protocol version profiles
|
|
134
|
+
│ │ ├── protocol.py # UUID definitions & parsers
|
|
135
|
+
│ │ └── scanner.py # Device discovery
|
|
136
|
+
│ ├── server/
|
|
137
|
+
│ │ ├── app.py # FastAPI server
|
|
138
|
+
│ │ └── routes/ # API endpoints
|
|
139
|
+
│ └── static/ # Web UI
|
|
140
|
+
└── pyproject.toml
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Publishing to PyPI
|
|
144
|
+
|
|
145
|
+
### Via Git Tag (Recommended)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Update version in pyproject.toml first
|
|
149
|
+
git add pyproject.toml
|
|
150
|
+
git commit -m "chore(rvt-monitor): Bump version to x.y.z"
|
|
151
|
+
|
|
152
|
+
# Create and push tag
|
|
153
|
+
git tag rvt-monitor-vX.Y.Z
|
|
154
|
+
git push origin main --tags
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
CI/CD will automatically build and publish to PyPI.
|
|
158
|
+
|
|
159
|
+
### Manual Trigger
|
|
160
|
+
|
|
161
|
+
1. Go to GitHub → Actions
|
|
162
|
+
2. Select "Publish rvt-monitor to PyPI"
|
|
163
|
+
3. Click "Run workflow"
|
|
164
|
+
|
|
165
|
+
## Related
|
|
166
|
+
|
|
167
|
+
- Firmware: `v1/common_components/` - Device-side BLE implementation
|
|
168
|
+
- OTA Tool: `ota/ota.py` - Firmware deployment
|
|
@@ -18,10 +18,8 @@ from .protocol import (
|
|
|
18
18
|
build_motion_command,
|
|
19
19
|
build_led_brightness,
|
|
20
20
|
build_led_color_index,
|
|
21
|
-
build_torque_command,
|
|
22
21
|
build_dimension_write,
|
|
23
22
|
build_wifi_config,
|
|
24
|
-
TorqueCommand,
|
|
25
23
|
DimensionCommand,
|
|
26
24
|
LEDTarget,
|
|
27
25
|
)
|
|
@@ -102,7 +100,7 @@ class CeilyStatus:
|
|
|
102
100
|
tof_front: TofStatus = field(default_factory=TofStatus)
|
|
103
101
|
tof_left: TofStatus = field(default_factory=TofStatus)
|
|
104
102
|
tof_right: TofStatus = field(default_factory=TofStatus)
|
|
105
|
-
# Limit switches (bit 0:
|
|
103
|
+
# Limit switches (bit 0: top ON, bit 4: bottom ON)
|
|
106
104
|
limit_switch_state: int = 0
|
|
107
105
|
top_limit: bool = False
|
|
108
106
|
bottom_limit: bool = False
|
|
@@ -261,8 +259,10 @@ class WallyStatus:
|
|
|
261
259
|
# ToF sensors
|
|
262
260
|
tof_left: WallyTofStatus = field(default_factory=WallyTofStatus)
|
|
263
261
|
tof_right: WallyTofStatus = field(default_factory=WallyTofStatus)
|
|
264
|
-
# Limit switch
|
|
262
|
+
# Limit switch (bit 0: open ON, bit 4: close ON)
|
|
265
263
|
limit_switch_state: int = 0
|
|
264
|
+
open_limit: bool = False
|
|
265
|
+
close_limit: bool = False
|
|
266
266
|
photo_sensor_state: int = 0
|
|
267
267
|
# Motors
|
|
268
268
|
motor_left: WallyMotorStatus = field(default_factory=WallyMotorStatus)
|
|
@@ -296,6 +296,8 @@ class WallyStatus:
|
|
|
296
296
|
"tof_left": self.tof_left.to_dict(),
|
|
297
297
|
"tof_right": self.tof_right.to_dict(),
|
|
298
298
|
"limit_switch_state": self.limit_switch_state,
|
|
299
|
+
"open_limit": self.open_limit,
|
|
300
|
+
"close_limit": self.close_limit,
|
|
299
301
|
"photo_sensor_state": self.photo_sensor_state,
|
|
300
302
|
"motor_left": self.motor_left.to_dict(),
|
|
301
303
|
"motor_right": self.motor_right.to_dict(),
|
|
@@ -619,10 +621,10 @@ class BLEManager:
|
|
|
619
621
|
status.tof_front = TofStatus.from_bytes_v2(data[17], data[18])
|
|
620
622
|
status.tof_left = TofStatus.from_bytes_v2(data[19], data[20])
|
|
621
623
|
status.tof_right = TofStatus.from_bytes_v2(data[21], data[22])
|
|
622
|
-
# Limit switch: bit 0 =
|
|
624
|
+
# Limit switch: bit 0 = top ON, bit 4 = bottom ON
|
|
623
625
|
status.limit_switch_state = data[23]
|
|
624
|
-
status.
|
|
625
|
-
status.
|
|
626
|
+
status.top_limit = bool(data[23] & 0x01)
|
|
627
|
+
status.bottom_limit = bool(data[23] & 0x10)
|
|
626
628
|
# Motor (13 bytes, offset 24)
|
|
627
629
|
# bit 0: is_connected, bit 1: is_enabled
|
|
628
630
|
motor_status_byte = data[24]
|
|
@@ -645,10 +647,10 @@ class BLEManager:
|
|
|
645
647
|
status.tof_front = TofStatus.from_bytes_v1(data[1], bool(data[2]))
|
|
646
648
|
status.tof_left = TofStatus.from_bytes_v1(data[3], bool(data[4]))
|
|
647
649
|
status.tof_right = TofStatus.from_bytes_v1(data[5], bool(data[6]))
|
|
648
|
-
# Limit switch: bit 0 =
|
|
650
|
+
# Limit switch: bit 0 = top ON, bit 4 = bottom ON
|
|
649
651
|
status.limit_switch_state = data[7]
|
|
650
|
-
status.
|
|
651
|
-
status.
|
|
652
|
+
status.top_limit = bool(data[7] & 0x01)
|
|
653
|
+
status.bottom_limit = bool(data[7] & 0x10)
|
|
652
654
|
# Servo
|
|
653
655
|
# bit 0: is_connected, bit 1: is_enabled
|
|
654
656
|
motor_status_byte = data[8]
|
|
@@ -705,8 +707,10 @@ class BLEManager:
|
|
|
705
707
|
tof_right_dist = struct.unpack("<f", data[7:11])[0]
|
|
706
708
|
status.tof_right = WallyTofStatus.from_bytes_v1(tof_right_status, tof_right_dist)
|
|
707
709
|
|
|
708
|
-
# Limit switch
|
|
710
|
+
# Limit switch: bit 0 = open ON, bit 4 = close ON
|
|
709
711
|
status.limit_switch_state = data[11]
|
|
712
|
+
status.open_limit = bool(data[11] & 0x01)
|
|
713
|
+
status.close_limit = bool(data[11] & 0x10)
|
|
710
714
|
|
|
711
715
|
# Left motor (bytes 12-35)
|
|
712
716
|
status.motor_left.current_alarm = struct.unpack("<H", data[12:14])[0]
|
|
@@ -766,7 +770,10 @@ class BLEManager:
|
|
|
766
770
|
# Sensors (6 bytes, offset 17)
|
|
767
771
|
status.tof_left = WallyTofStatus.from_bytes_v2(data[17], data[18])
|
|
768
772
|
status.tof_right = WallyTofStatus.from_bytes_v2(data[19], data[20])
|
|
773
|
+
# Limit switch: bit 0 = open ON, bit 4 = close ON
|
|
769
774
|
status.limit_switch_state = data[21]
|
|
775
|
+
status.open_limit = bool(data[21] & 0x01)
|
|
776
|
+
status.close_limit = bool(data[21] & 0x10)
|
|
770
777
|
status.photo_sensor_state = data[22]
|
|
771
778
|
|
|
772
779
|
# Left motor (13 bytes, offset 23)
|
|
@@ -926,34 +933,6 @@ class BLEManager:
|
|
|
926
933
|
self.command_error_count += 1
|
|
927
934
|
return False
|
|
928
935
|
|
|
929
|
-
async def reset_torque_model(self) -> bool:
|
|
930
|
-
"""Reset torque model."""
|
|
931
|
-
if not self.connected:
|
|
932
|
-
return False
|
|
933
|
-
|
|
934
|
-
try:
|
|
935
|
-
data = build_torque_command(TorqueCommand.RESET_MODEL)
|
|
936
|
-
await self.client.write_gatt_char(CharUUID.TORQUE_CONTROL, data)
|
|
937
|
-
self.command_success_count += 1
|
|
938
|
-
return True
|
|
939
|
-
except Exception:
|
|
940
|
-
self.command_error_count += 1
|
|
941
|
-
return False
|
|
942
|
-
|
|
943
|
-
async def save_torque_model(self) -> bool:
|
|
944
|
-
"""Save torque model."""
|
|
945
|
-
if not self.connected:
|
|
946
|
-
return False
|
|
947
|
-
|
|
948
|
-
try:
|
|
949
|
-
data = build_torque_command(TorqueCommand.SAVE_MODEL)
|
|
950
|
-
await self.client.write_gatt_char(CharUUID.TORQUE_CONTROL, data)
|
|
951
|
-
self.command_success_count += 1
|
|
952
|
-
return True
|
|
953
|
-
except Exception:
|
|
954
|
-
self.command_error_count += 1
|
|
955
|
-
return False
|
|
956
|
-
|
|
957
936
|
async def read_dimension(self, dimension_id: int) -> Optional[int]:
|
|
958
937
|
"""Read dimension value."""
|
|
959
938
|
if not self.connected:
|
|
@@ -19,7 +19,6 @@ class ServiceUUID:
|
|
|
19
19
|
CONTROL = "0000ff20-0000-1000-8000-00805f9b34fb" # 16-bit: 0xFF20
|
|
20
20
|
SYSTEM_STATUS = "501a8cf5-98a7-4370-bb97-632c84910000"
|
|
21
21
|
DIMENSION = "cc9762a6-bbb7-4b21-b2e2-153059030000"
|
|
22
|
-
TORQUE = "acc2a064-99f7-404e-a7df-b1714f3d0000"
|
|
23
22
|
LED = "07ecc81b-b952-47e6-a1f1-0999577f0000"
|
|
24
23
|
LOG = "cc9762a6-bbb7-4b21-b2e2-153059032200"
|
|
25
24
|
|
|
@@ -46,13 +45,6 @@ class CharUUID:
|
|
|
46
45
|
# Dimension (Read/Write)
|
|
47
46
|
DIMENSION = "cc9762a6-bbb7-4b21-b2e2-153059030001"
|
|
48
47
|
|
|
49
|
-
# Torque Control (Write)
|
|
50
|
-
TORQUE_CONTROL = "acc2a064-99f7-404e-a7df-b1714f3d00fd"
|
|
51
|
-
# Torque Status (Read)
|
|
52
|
-
TORQUE_STATUS_1 = "acc2a064-99f7-404e-a7df-b1714f3d0001"
|
|
53
|
-
TORQUE_STATUS_2 = "acc2a064-99f7-404e-a7df-b1714f3d0002"
|
|
54
|
-
TORQUE_GRAPH = "acc2a064-99f7-404e-a7df-b1714f3d0010"
|
|
55
|
-
|
|
56
48
|
# LED Control (Write)
|
|
57
49
|
LED_CONTROL = "07ecc81b-b952-47e6-a1f1-0999577f0001"
|
|
58
50
|
# LED Status (Read)
|
|
@@ -115,17 +107,6 @@ class LEDTarget(IntEnum):
|
|
|
115
107
|
MOOD = 3
|
|
116
108
|
|
|
117
109
|
|
|
118
|
-
# =============================================================================
|
|
119
|
-
# Torque Commands
|
|
120
|
-
# =============================================================================
|
|
121
|
-
|
|
122
|
-
class TorqueCommand(IntEnum):
|
|
123
|
-
"""Torque model control commands."""
|
|
124
|
-
SET_SENSITIVITY = 0x11
|
|
125
|
-
RESET_MODEL = 0xA4
|
|
126
|
-
SAVE_MODEL = 0xEA
|
|
127
|
-
|
|
128
|
-
|
|
129
110
|
# =============================================================================
|
|
130
111
|
# Dimension Commands and IDs
|
|
131
112
|
# =============================================================================
|
|
@@ -142,6 +123,15 @@ class CeilyDimensionID(IntEnum):
|
|
|
142
123
|
STROKE_MM = 0x03
|
|
143
124
|
GEAR_RATIO = 0x04
|
|
144
125
|
PULLEY_RADIUS_MM = 0x05
|
|
126
|
+
TOF_ENABLED = 0x06
|
|
127
|
+
TORQUE_THRESHOLD_MNM = 0x07
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ToF enabled bit flags
|
|
131
|
+
TOF_ENABLED_FRONT = 1 << 0
|
|
132
|
+
TOF_ENABLED_LEFT = 1 << 1
|
|
133
|
+
TOF_ENABLED_RIGHT = 1 << 2
|
|
134
|
+
TOF_ENABLED_ALL = TOF_ENABLED_FRONT | TOF_ENABLED_LEFT | TOF_ENABLED_RIGHT
|
|
145
135
|
|
|
146
136
|
|
|
147
137
|
class WallyDimensionID(IntEnum):
|
|
@@ -157,6 +147,7 @@ class WallyDimensionID(IntEnum):
|
|
|
157
147
|
GEAR_RATIO = 0x0B
|
|
158
148
|
KEEP_DISTANCE_MM = 0x0C
|
|
159
149
|
MISSION_SPEED = 0x0D
|
|
150
|
+
TORQUE_THRESHOLD_MNM = 0x0E
|
|
160
151
|
CONTROL_BUTTON_VERSION = 0xEE
|
|
161
152
|
|
|
162
153
|
|
|
@@ -167,6 +158,8 @@ CEILY_DIMENSIONS = [
|
|
|
167
158
|
{"id": CeilyDimensionID.STROKE_MM, "name": "Stroke", "unit": "mm", "type": "uint16"},
|
|
168
159
|
{"id": CeilyDimensionID.GEAR_RATIO, "name": "Gear Ratio", "unit": "", "type": "uint8"},
|
|
169
160
|
{"id": CeilyDimensionID.PULLEY_RADIUS_MM, "name": "Pulley Radius", "unit": "mm", "type": "uint8"},
|
|
161
|
+
{"id": CeilyDimensionID.TOF_ENABLED, "name": "ToF Enabled", "unit": "", "type": "uint8"},
|
|
162
|
+
{"id": CeilyDimensionID.TORQUE_THRESHOLD_MNM, "name": "Torque Threshold", "unit": "mNm", "type": "uint16"},
|
|
170
163
|
]
|
|
171
164
|
|
|
172
165
|
WALLY_DIMENSIONS = [
|
|
@@ -181,6 +174,7 @@ WALLY_DIMENSIONS = [
|
|
|
181
174
|
{"id": WallyDimensionID.GEAR_RATIO, "name": "Gear Ratio", "unit": "", "type": "uint8"},
|
|
182
175
|
{"id": WallyDimensionID.KEEP_DISTANCE_MM, "name": "Keep Distance", "unit": "mm", "type": "uint8"},
|
|
183
176
|
{"id": WallyDimensionID.MISSION_SPEED, "name": "Mission Speed", "unit": "", "type": "uint8", "readonly": True},
|
|
177
|
+
{"id": WallyDimensionID.TORQUE_THRESHOLD_MNM, "name": "Torque Threshold", "unit": "mNm", "type": "uint16"},
|
|
184
178
|
{"id": WallyDimensionID.CONTROL_BUTTON_VERSION, "name": "Button Version", "unit": "", "type": "uint8", "readonly": True},
|
|
185
179
|
]
|
|
186
180
|
|
|
@@ -210,12 +204,6 @@ def build_led_color_rgb(r: int, g: int, b: int) -> bytes:
|
|
|
210
204
|
return bytes([LEDCommand.CHANGE_COLOR, r, g, b])
|
|
211
205
|
|
|
212
206
|
|
|
213
|
-
def build_torque_command(command: TorqueCommand, arg: int = 0) -> bytes:
|
|
214
|
-
"""Build torque control command (8 bytes: 4 cmd + 4 arg)."""
|
|
215
|
-
import struct
|
|
216
|
-
return struct.pack("<II", command, arg)
|
|
217
|
-
|
|
218
|
-
|
|
219
207
|
def build_dimension_write(dimension_id: int, value: int) -> bytes:
|
|
220
208
|
"""Build dimension write command (8 bytes: 4 id + 4 value)."""
|
|
221
209
|
import struct
|
|
@@ -230,24 +218,28 @@ def build_wifi_config(ssid: str, password: str) -> bytes:
|
|
|
230
218
|
|
|
231
219
|
|
|
232
220
|
def parse_ceily_dimensions(data: bytes) -> dict:
|
|
233
|
-
"""Parse Ceily dimension data (
|
|
221
|
+
"""Parse Ceily dimension data (11 bytes)."""
|
|
234
222
|
import struct
|
|
235
223
|
if len(data) < 8:
|
|
236
224
|
return {}
|
|
237
225
|
width, length, stroke = struct.unpack_from("<HHH", data, 0)
|
|
238
226
|
gear_ratio = data[6]
|
|
239
227
|
pulley_radius = data[7]
|
|
228
|
+
tof_enabled = data[8] if len(data) >= 9 else TOF_ENABLED_ALL
|
|
229
|
+
torque_threshold_mnm = struct.unpack_from("<H", data, 9)[0] if len(data) >= 11 else 1150
|
|
240
230
|
return {
|
|
241
231
|
CeilyDimensionID.WIDTH_MM: width,
|
|
242
232
|
CeilyDimensionID.LENGTH_MM: length,
|
|
243
233
|
CeilyDimensionID.STROKE_MM: stroke,
|
|
244
234
|
CeilyDimensionID.GEAR_RATIO: gear_ratio,
|
|
245
235
|
CeilyDimensionID.PULLEY_RADIUS_MM: pulley_radius,
|
|
236
|
+
CeilyDimensionID.TOF_ENABLED: tof_enabled,
|
|
237
|
+
CeilyDimensionID.TORQUE_THRESHOLD_MNM: torque_threshold_mnm,
|
|
246
238
|
}
|
|
247
239
|
|
|
248
240
|
|
|
249
241
|
def parse_wally_dimensions(data: bytes) -> dict:
|
|
250
|
-
"""Parse Wally dimension data (
|
|
242
|
+
"""Parse Wally dimension data (24 bytes)."""
|
|
251
243
|
import struct
|
|
252
244
|
if len(data) < 22:
|
|
253
245
|
return {}
|
|
@@ -264,6 +256,7 @@ def parse_wally_dimensions(data: bytes) -> dict:
|
|
|
264
256
|
# Byte 19: keep_distance
|
|
265
257
|
# Byte 20: mission_speed
|
|
266
258
|
# Byte 21: control_button_version
|
|
259
|
+
# Bytes 22-23: torque_threshold_mnm
|
|
267
260
|
open_limit = struct.unpack_from("<H", data, 0)[0]
|
|
268
261
|
left_wheel = struct.unpack_from("<h", data, 6)[0]
|
|
269
262
|
right_wheel = struct.unpack_from("<h", data, 8)[0]
|
|
@@ -276,6 +269,7 @@ def parse_wally_dimensions(data: bytes) -> dict:
|
|
|
276
269
|
keep_distance = data[19]
|
|
277
270
|
mission_speed = data[20]
|
|
278
271
|
button_version = data[21]
|
|
272
|
+
torque_threshold_mnm = struct.unpack_from("<H", data, 22)[0] if len(data) >= 24 else 1000
|
|
279
273
|
return {
|
|
280
274
|
WallyDimensionID.OPEN_LIMIT_DISTANCE_MM: open_limit,
|
|
281
275
|
WallyDimensionID.LEFT_WHEEL_FROM_TOF_MM: left_wheel,
|
|
@@ -288,6 +282,7 @@ def parse_wally_dimensions(data: bytes) -> dict:
|
|
|
288
282
|
WallyDimensionID.GEAR_RATIO: gear_ratio,
|
|
289
283
|
WallyDimensionID.KEEP_DISTANCE_MM: keep_distance,
|
|
290
284
|
WallyDimensionID.MISSION_SPEED: mission_speed,
|
|
285
|
+
WallyDimensionID.TORQUE_THRESHOLD_MNM: torque_threshold_mnm,
|
|
291
286
|
WallyDimensionID.CONTROL_BUTTON_VERSION: button_version,
|
|
292
287
|
}
|
|
293
288
|
|
|
@@ -454,6 +454,26 @@ select:disabled {
|
|
|
454
454
|
color: var(--color-text-secondary);
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
/* ToF Checkboxes */
|
|
458
|
+
.tof-checkboxes {
|
|
459
|
+
display: flex;
|
|
460
|
+
gap: 16px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.tof-checkbox-label {
|
|
464
|
+
display: flex;
|
|
465
|
+
align-items: center;
|
|
466
|
+
gap: 4px;
|
|
467
|
+
font-size: 14px;
|
|
468
|
+
cursor: pointer;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.tof-checkbox-label input[type="checkbox"] {
|
|
472
|
+
width: 16px;
|
|
473
|
+
height: 16px;
|
|
474
|
+
cursor: pointer;
|
|
475
|
+
}
|
|
476
|
+
|
|
457
477
|
#dimension-table .btn-small {
|
|
458
478
|
padding: 4px 8px;
|
|
459
479
|
font-size: 12px;
|
|
@@ -728,23 +728,61 @@ function renderDimensionTable(data) {
|
|
|
728
728
|
tdName.textContent = dim.name;
|
|
729
729
|
tr.appendChild(tdName);
|
|
730
730
|
|
|
731
|
-
// Value (editable input)
|
|
731
|
+
// Value (editable input or checkboxes for ToF)
|
|
732
732
|
const tdValue = document.createElement('td');
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
733
|
+
|
|
734
|
+
// Special handling for ToF Enabled (bit flags)
|
|
735
|
+
if (dim.name === 'ToF Enabled') {
|
|
736
|
+
const container = document.createElement('div');
|
|
737
|
+
container.className = 'tof-checkboxes';
|
|
738
|
+
|
|
739
|
+
const sensors = [
|
|
740
|
+
{ bit: 0, label: 'Front' },
|
|
741
|
+
{ bit: 1, label: 'Left' },
|
|
742
|
+
{ bit: 2, label: 'Right' },
|
|
743
|
+
];
|
|
744
|
+
|
|
745
|
+
sensors.forEach(sensor => {
|
|
746
|
+
const label = document.createElement('label');
|
|
747
|
+
label.className = 'tof-checkbox-label';
|
|
748
|
+
const checkbox = document.createElement('input');
|
|
749
|
+
checkbox.type = 'checkbox';
|
|
750
|
+
checkbox.checked = (dim.value & (1 << sensor.bit)) !== 0;
|
|
751
|
+
checkbox.dataset.bit = sensor.bit;
|
|
752
|
+
checkbox.dataset.dimId = dim.id;
|
|
753
|
+
checkbox.addEventListener('change', () => {
|
|
754
|
+
// Calculate new value from all checkboxes
|
|
755
|
+
let newValue = 0;
|
|
756
|
+
container.querySelectorAll('input[type="checkbox"]').forEach(cb => {
|
|
757
|
+
if (cb.checked) {
|
|
758
|
+
newValue |= (1 << parseInt(cb.dataset.bit, 10));
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
sendWs('write_dimension', { id: dim.id, value: newValue });
|
|
762
|
+
});
|
|
763
|
+
label.appendChild(checkbox);
|
|
764
|
+
label.appendChild(document.createTextNode(` ${sensor.label}`));
|
|
765
|
+
container.appendChild(label);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
tdValue.appendChild(container);
|
|
769
|
+
} else {
|
|
770
|
+
const input = document.createElement('input');
|
|
771
|
+
input.type = 'number';
|
|
772
|
+
input.value = dim.value;
|
|
773
|
+
input.dataset.dimId = dim.id;
|
|
774
|
+
input.dataset.originalValue = dim.value;
|
|
775
|
+
// Auto-apply on blur if value changed
|
|
776
|
+
input.addEventListener('blur', () => {
|
|
777
|
+
const newValue = parseInt(input.value, 10);
|
|
778
|
+
const originalValue = parseInt(input.dataset.originalValue, 10);
|
|
779
|
+
if (!isNaN(newValue) && newValue !== originalValue) {
|
|
780
|
+
sendWs('write_dimension', { id: dim.id, value: newValue });
|
|
781
|
+
input.dataset.originalValue = newValue;
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
tdValue.appendChild(input);
|
|
785
|
+
}
|
|
748
786
|
tr.appendChild(tdValue);
|
|
749
787
|
|
|
750
788
|
// Unit
|
rvt_monitor-0.2.3/PKG-INFO
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: rvt-monitor
|
|
3
|
-
Version: 0.2.3
|
|
4
|
-
Summary: BLE Device Monitor for Ceily/Wally
|
|
5
|
-
Author-email: Rovothome <chandler.kim@rovothome.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Requires-Python: >=3.10
|
|
8
|
-
Requires-Dist: bleak>=0.21.0
|
|
9
|
-
Requires-Dist: fastapi>=0.109.0
|
|
10
|
-
Requires-Dist: platformdirs>=4.0.0
|
|
11
|
-
Requires-Dist: pyserial>=3.5
|
|
12
|
-
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
|
-
# RVT-Monitor
|
|
16
|
-
|
|
17
|
-
BLE Device Monitor for Ceily/Wally
|
|
18
|
-
|
|
19
|
-
## Install
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
pip install rvt-monitor
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Run
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
rvt-monitor
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Opens browser at http://127.0.0.1:8000
|
rvt_monitor-0.2.3/README.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|