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.
Files changed (34) hide show
  1. rvt_monitor-0.2.4/PKG-INFO +182 -0
  2. rvt_monitor-0.2.4/README.md +168 -0
  3. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/pyproject.toml +1 -1
  4. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/manager.py +18 -39
  5. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/protocol.py +22 -27
  6. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/static/css/style.css +20 -0
  7. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/static/js/app.js +54 -16
  8. rvt_monitor-0.2.3/PKG-INFO +0 -31
  9. rvt_monitor-0.2.3/README.md +0 -17
  10. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/.gitignore +0 -0
  11. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/__init__.py +0 -0
  12. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/__main__.py +0 -0
  13. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/__init__.py +0 -0
  14. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/profiles.py +0 -0
  15. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/ble/scanner.py +0 -0
  16. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/core/__init__.py +0 -0
  17. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/core/logger.py +0 -0
  18. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/__init__.py +0 -0
  19. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/cli.py +0 -0
  20. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/protocol.py +0 -0
  21. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/serial_port.py +0 -0
  22. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/server.py +0 -0
  23. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/isv2_config/static/index.html +0 -0
  24. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/main.py +0 -0
  25. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/__init__.py +0 -0
  26. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/app.py +0 -0
  27. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/__init__.py +0 -0
  28. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/config.py +0 -0
  29. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/control.py +0 -0
  30. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/device.py +0 -0
  31. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/routes/logs.py +0 -0
  32. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/state.py +0 -0
  33. {rvt_monitor-0.2.3 → rvt_monitor-0.2.4}/rvt_monitor/server/websocket.py +0 -0
  34. {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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rvt-monitor"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "BLE Device Monitor for Ceily/Wally"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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: bottom, bit 1: top)
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 = bottom, bit 1 = top
624
+ # Limit switch: bit 0 = top ON, bit 4 = bottom ON
623
625
  status.limit_switch_state = data[23]
624
- status.bottom_limit = bool(data[23] & 0x01)
625
- status.top_limit = bool(data[23] & 0x02)
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 = bottom, bit 1 = top
650
+ # Limit switch: bit 0 = top ON, bit 4 = bottom ON
649
651
  status.limit_switch_state = data[7]
650
- status.bottom_limit = bool(data[7] & 0x01)
651
- status.top_limit = bool(data[7] & 0x02)
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 (8 bytes)."""
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 (22 bytes)."""
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
- const input = document.createElement('input');
734
- input.type = 'number';
735
- input.value = dim.value;
736
- input.dataset.dimId = dim.id;
737
- input.dataset.originalValue = dim.value;
738
- // Auto-apply on blur if value changed
739
- input.addEventListener('blur', () => {
740
- const newValue = parseInt(input.value, 10);
741
- const originalValue = parseInt(input.dataset.originalValue, 10);
742
- if (!isNaN(newValue) && newValue !== originalValue) {
743
- sendWs('write_dimension', { id: dim.id, value: newValue });
744
- input.dataset.originalValue = newValue;
745
- }
746
- });
747
- tdValue.appendChild(input);
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
@@ -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
@@ -1,17 +0,0 @@
1
- # RVT-Monitor
2
-
3
- BLE Device Monitor for Ceily/Wally
4
-
5
- ## Install
6
-
7
- ```bash
8
- pip install rvt-monitor
9
- ```
10
-
11
- ## Run
12
-
13
- ```bash
14
- rvt-monitor
15
- ```
16
-
17
- Opens browser at http://127.0.0.1:8000
File without changes