Skip to content

Commit 71dd926

Browse files
sroebertnewAM
authored andcommitted
Updated to use the move characteristic for setting the desk position, which always stops at the right height and avoids having to monitor the height
1 parent 9608753 commit 71dd926

File tree

2 files changed

+70
-69
lines changed

2 files changed

+70
-69
lines changed

idasen/__init__.py

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from typing import Union
1111
import asyncio
1212
import logging
13+
import struct
1314
import sys
14-
import time
1515

1616

1717
_UUID_HEIGHT: str = "99fa0021-338a-1024-8a49-009c0215f78a"
@@ -28,18 +28,24 @@
2828

2929

3030
# height calculation offset in meters, assumed to be the same for all desks
31-
def _bytes_to_meters(raw: bytearray) -> float:
32-
"""Converts a value read from the desk in bytes to meters."""
31+
def _bytes_to_meters_and_speed(raw: bytearray) -> Tuple[float, int]:
32+
"""Converts a value read from the desk in bytes to height in meters and speed."""
3333
raw_len = len(raw)
3434
expected_len = 4
3535
assert (
3636
raw_len == expected_len
3737
), f"Expected raw value to be {expected_len} bytes long, got {raw_len} bytes"
3838

39-
high_byte: int = int(raw[1])
40-
low_byte: int = int(raw[0])
41-
int_raw: int = (high_byte << 8) + low_byte
42-
return float(int_raw / 10000) + IdasenDesk.MIN_HEIGHT
39+
int_raw, speed = struct.unpack("<Hh", raw)
40+
meters = float(int(int_raw) / 10000) + IdasenDesk.MIN_HEIGHT
41+
42+
return meters, int(speed)
43+
44+
45+
def _meters_to_bytes(meters: float) -> bytearray:
46+
"""Converts meters to bytes for setting the position on the desk"""
47+
int_raw: int = int((meters - IdasenDesk.MIN_HEIGHT) * 10000)
48+
return bytearray(struct.pack("<H", int_raw))
4349

4450

4551
def _is_desk(device: BLEDevice, adv: AdvertisementData) -> bool:
@@ -184,7 +190,7 @@ async def monitor(self, callback: Callable[[float], Awaitable[None]]):
184190
previous_height = 0.0
185191

186192
async def output_listener(char: BleakGATTCharacteristic, data: bytearray):
187-
height = _bytes_to_meters(data)
193+
height, _ = _bytes_to_meters_and_speed(data)
188194
self._logger.debug(f"Got data: {height}m")
189195

190196
nonlocal previous_height
@@ -248,7 +254,7 @@ async def wakeup(self):
248254
This exists for compatibility with the Linak DPG1C controller,
249255
it is not necessary with the original idasen controller.
250256
251-
>>> async def example() -> str:
257+
>>> async def example():
252258
... async with IdasenDesk(mac="AA:AA:AA:AA:AA:AA") as desk:
253259
... await desk.wakeup()
254260
>>> asyncio.run(example())
@@ -322,46 +328,26 @@ async def move_to_target(self, target: float):
322328
self._moving = True
323329

324330
async def do_move() -> None:
325-
previous_height = await self.get_height()
326-
will_move_up = target > previous_height
327-
last_move_time: Optional[float] = None
331+
current_height = await self.get_height()
332+
if current_height == target:
333+
return
334+
335+
# Wakeup and stop commands are needed in order to
336+
# start the reference input for setting the position
337+
await self._client.write_gatt_char(_UUID_COMMAND, _COMMAND_WAKEUP)
338+
await self._client.write_gatt_char(_UUID_COMMAND, _COMMAND_STOP)
339+
340+
data = _meters_to_bytes(target)
341+
328342
while True:
329-
height = await self.get_height()
330-
difference = target - height
331-
self._logger.debug(f"{target=} {height=} {difference=}")
332-
if (height < previous_height and will_move_up) or (
333-
height > previous_height and not will_move_up
334-
):
335-
self._logger.warning(
336-
"stopped moving because desk safety feature kicked in"
337-
)
338-
return
339-
340-
if height == previous_height:
341-
if (
342-
last_move_time is not None
343-
and time.time() - last_move_time > 0.5
344-
):
345-
self._logger.warning(
346-
"desk is not moving anymore. physical button probably "
347-
"pressed"
348-
)
349-
return
350-
else:
351-
last_move_time = time.time()
352-
353-
if not self._moving:
354-
return
355-
356-
if abs(difference) < 0.005: # tolerance of 0.005 meters
357-
self._logger.info(f"reached target of {target:.3f}")
358-
await self._stop()
359-
return
360-
elif difference > 0:
361-
await self.move_up()
362-
elif difference < 0:
363-
await self.move_down()
364-
previous_height = height
343+
await self._client.write_gatt_char(_UUID_REFERENCE_INPUT, data)
344+
await asyncio.sleep(0.4)
345+
346+
# Stop as soon as the speed is 0,
347+
# which means the desk has reached the target position
348+
speed = await self._get_speed()
349+
if speed == 0:
350+
break
365351

366352
self._move_task = asyncio.create_task(do_move())
367353
await self._move_task
@@ -400,7 +386,16 @@ async def get_height(self) -> float:
400386
>>> asyncio.run(example())
401387
1.0
402388
"""
403-
return _bytes_to_meters(await self._client.read_gatt_char(_UUID_HEIGHT))
389+
height, _ = await self._get_height_and_speed()
390+
return height
391+
392+
async def _get_speed(self) -> int:
393+
_, speed = await self._get_height_and_speed()
394+
return speed
395+
396+
async def _get_height_and_speed(self) -> Tuple[float, int]:
397+
raw = await self._client.read_gatt_char(_UUID_HEIGHT)
398+
return _bytes_to_meters_and_speed(raw)
404399

405400
@staticmethod
406401
async def discover() -> Optional[str]:

tests/test_idasen.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from idasen import _bytes_to_meters, _is_desk
1+
from idasen import _bytes_to_meters_and_speed, _meters_to_bytes, _is_desk
22
from idasen import IdasenDesk
33
from types import SimpleNamespace
44
from typing import AsyncGenerator
@@ -31,10 +31,12 @@ class MockBleakClient:
3131

3232
def __init__(self):
3333
self._height = 1.0
34+
self._is_moving = False
3435
self.is_connected = False
3536

3637
async def __aenter__(self):
3738
self._height = 1.0
39+
self._is_moving = False
3840
self.is_connected = True
3941
return self
4042

@@ -52,22 +54,25 @@ async def start_notify(self, uuid: str, callback: Callable):
5254
await callback(uuid, bytearray([0x00, 0x00, 0x00, 0x00]))
5355
await callback(None, bytearray([0x10, 0x00, 0x00, 0x00]))
5456

55-
async def write_gatt_char(
56-
self, uuid: str, command: bytearray, response: bool = False
57-
):
57+
async def write_gatt_char(self, uuid: str, data: bytearray, response: bool = False):
5858
if uuid == idasen._UUID_COMMAND:
59-
if command == idasen._COMMAND_UP:
59+
if data == idasen._COMMAND_UP:
6060
self._height += 0.001
61-
elif command == idasen._COMMAND_DOWN:
61+
elif data == idasen._COMMAND_DOWN:
6262
self._height -= 0.001
63+
if uuid == idasen._UUID_REFERENCE_INPUT:
64+
assert len(data) == 2
65+
66+
data_with_speed = bytearray([data[0], data[1], 0, 0])
67+
requested_height, _ = _bytes_to_meters_and_speed(data_with_speed)
68+
self._height += min(0.1, max(-0.1, requested_height - self._height))
69+
70+
self._is_moving = self._height != requested_height
6371

6472
async def read_gatt_char(self, uuid: str) -> bytearray:
65-
norm = self._height - IdasenDesk.MIN_HEIGHT
66-
norm *= 10000
67-
norm = int(norm)
68-
low_byte = norm & 0xFF
69-
high_byte = (norm >> 8) & 0xFF
70-
return bytearray([low_byte, high_byte, 0x00, 0x00])
73+
height_bytes = _meters_to_bytes(self._height)
74+
speed_byte = 0x01 if self._is_moving else 0x00
75+
return bytearray([height_bytes[0], height_bytes[1], 0x00, speed_byte])
7176

7277
@property
7378
def address(self) -> str:
@@ -139,7 +144,7 @@ async def test_move_to_target_raises(desk: IdasenDesk, target: float):
139144
@pytest.mark.parametrize("target", [0.7, 1.1])
140145
async def test_move_to_target(desk: IdasenDesk, target: float):
141146
await desk.move_to_target(target)
142-
assert abs(await desk.get_height() - target) < 0.005
147+
assert abs(await desk.get_height() - target) < 0.001
143148

144149

145150
async def test_move_abort_when_no_movement():
@@ -190,16 +195,17 @@ async def write_gatt_char_mock(
190195

191196

192197
@pytest.mark.parametrize(
193-
"raw, result",
198+
"raw, height, speed",
194199
[
195-
(bytearray([0x64, 0x19, 0x00, 0x00]), IdasenDesk.MAX_HEIGHT),
196-
(bytearray([0x00, 0x00, 0x00, 0x00]), IdasenDesk.MIN_HEIGHT),
197-
(bytearray([0x51, 0x04, 0x00, 0x00]), 0.7305),
198-
(bytearray([0x08, 0x08, 0x00, 0x00]), 0.8256),
200+
(bytearray([0x64, 0x19, 0x00, 0x00]), IdasenDesk.MAX_HEIGHT, 0),
201+
(bytearray([0x00, 0x00, 0x00, 0x00]), IdasenDesk.MIN_HEIGHT, 0),
202+
(bytearray([0x51, 0x04, 0x00, 0x00]), 0.7305, 0),
203+
(bytearray([0x08, 0x08, 0x00, 0x00]), 0.8256, 0),
204+
(bytearray([0x08, 0x08, 0x02, 0x01]), 0.8256, 258),
199205
],
200206
)
201-
def test_bytes_to_meters(raw: bytearray, result: float):
202-
assert _bytes_to_meters(raw) == result
207+
def test_bytes_to_meters_and_speed(raw: bytearray, height: float, speed: int):
208+
assert _bytes_to_meters_and_speed(raw) == (height, speed)
203209

204210

205211
async def test_fail_to_connect(caplog, monkeypatch):

0 commit comments

Comments
 (0)