Skip to content

Commit ac5f410

Browse files
authored
DBus reports (#91)
* Support reports in DBus API * Support listening for reports in DBus client CLI * Various small API tweaks * Significant documentation improvements * Improved just recipes
1 parent 8832065 commit ac5f410

37 files changed

+707
-130
lines changed

crystalfontz/atx.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,6 @@ def as_dict(self: Self) -> Dict[str, Any]:
9494

9595
as_["functions"] = [fn.value for fn in self.functions]
9696

97-
print(as_)
98-
9997
return as_
10098

10199
def __repr__(self: Self) -> str:

crystalfontz/cli.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -676,10 +676,11 @@ async def detect(
676676
@pass_client(run_forever=True, report_handler_cls=CliReportHandler)
677677
async def listen(client: Client, for_: Optional[float]) -> None:
678678
"""
679-
Listen for key and temperature reports.
679+
Listen for key activity and temperature reports.
680680
681-
To configure which reports to receive, use 'crystalfontz keypad reporting' and
682-
'crystalfontz temperature reporting' respectively.
681+
To configure which reports to receive, use
682+
'python -m crystalfontz keypad reporting' and
683+
'python -m crystalfontz temperature reporting' respectively.
683684
"""
684685

685686
if for_ is not None:

crystalfontz/dbus/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from crystalfontz.dbus.service.cli import main
2+
3+
main()

crystalfontz/dbus/bus.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

crystalfontz/dbus/client/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@
77
from crystalfontz.dbus.config import StagedConfig
88
from crystalfontz.dbus.domain import ConfigM
99
from crystalfontz.dbus.interface import DBUS_NAME, DbusInterface
10+
from crystalfontz.dbus.report import DbusClientReportHandler
1011

1112

1213
class DbusClient(DbusInterface):
1314
"""
1415
A DBus client for the Crystalfontz device.
1516
"""
1617

17-
def __init__(self: Self, bus: Optional[SdBus] = None) -> None:
18+
def __init__(
19+
self: Self,
20+
bus: Optional[SdBus] = None,
21+
report_handler: Optional[DbusClientReportHandler] = None,
22+
) -> None:
1823
client = Mock(name="client", side_effect=NotImplementedError("client"))
1924
self.subscribe = Mock(name="client.subscribe")
20-
super().__init__(client)
25+
super().__init__(client, report_handler=report_handler)
2126

2227
cast(Any, self)._proxify(DBUS_NAME, "/", bus=bus)
2328

crystalfontz/dbus/client/cli.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
OutputMode,
3030
WATCHDOG_SETTING,
3131
)
32-
from crystalfontz.dbus.bus import select_session_bus, select_system_bus
3332
from crystalfontz.dbus.client import DbusClient
3433
from crystalfontz.dbus.config import StagedConfig
3534
from crystalfontz.dbus.domain import (
@@ -52,6 +51,12 @@
5251
VersionsM,
5352
)
5453
from crystalfontz.dbus.error import handle_dbus_error
54+
from crystalfontz.dbus.report import DbusClientCliReportHandler
55+
from crystalfontz.dbus.select import (
56+
select_default_bus,
57+
select_session_bus,
58+
select_system_bus,
59+
)
5560
from crystalfontz.gpio import GpioDriveMode, GpioFunction
5661
from crystalfontz.lcd import LcdRegister
5762
from crystalfontz.temperature import (
@@ -70,6 +75,7 @@ class Obj:
7075
output: OutputMode
7176
timeout: TimeoutT
7277
retry_times: RetryTimesT
78+
report_handler: DbusClientCliReportHandler
7379

7480

7581
def pass_config(fn: AsyncCommand) -> AsyncCommand:
@@ -101,6 +107,15 @@ async def wrapped(obj: Obj, *args, **kwargs) -> None:
101107
return wrapped
102108

103109

110+
def pass_report_handler(fn: AsyncCommand) -> AsyncCommand:
111+
@click.pass_obj
112+
@functools.wraps(fn)
113+
async def wrapped(obj: Obj, *args, **kwargs) -> None:
114+
await fn(obj.report_handler, *args, **kwargs)
115+
116+
return wrapped
117+
118+
104119
def should_sudo(config_file: str) -> bool:
105120
st = os.stat(config_file)
106121
return os.geteuid() != st.st_uid
@@ -202,14 +217,20 @@ async def load() -> None:
202217
select_session_bus()
203218
elif user is None:
204219
select_system_bus()
220+
else:
221+
select_default_bus()
222+
223+
report_handler = DbusClientCliReportHandler()
224+
report_handler.mode = output
205225

206-
client = DbusClient()
226+
client = DbusClient(report_handler=report_handler)
207227
ctx.obj = Obj(
208228
client=client,
209229
log_level=log_level,
210230
output=output,
211231
timeout=TimeoutM.pack(timeout),
212232
retry_times=RetryTimesM.pack(retry_times),
233+
report_handler=report_handler,
213234
)
214235

215236
asyncio.run(load())
@@ -355,16 +376,26 @@ async def detect(
355376
@main.command()
356377
@click.option("--for", "for_", type=float, help="Amount of time to listen for reports")
357378
@async_command
379+
@pass_report_handler
358380
@pass_client
359-
async def listen(client: DbusClient, for_: Optional[float]) -> None:
381+
async def listen(
382+
client: DbusClient,
383+
report_handler: DbusClientCliReportHandler,
384+
for_: Optional[float],
385+
) -> None:
360386
"""
361387
Listen for key and temperature reports.
362388
363389
To configure which reports to receive, use 'crystalfontz keypad reporting' and
364390
'crystalfontz temperature reporting' respectively.
365391
"""
366392

367-
raise NotImplementedError("listen")
393+
await report_handler.listen()
394+
395+
if for_ is not None:
396+
await asyncio.sleep(for_)
397+
report_handler.stop()
398+
await report_handler.done
368399

369400

370401
@main.command(help="0 (0x00): Ping command")

crystalfontz/dbus/config.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
"""
2+
Manage a DBus service configuration.
3+
4+
Configuration for the DBus service is a little different than for the serial client.
5+
This is because the DBus service doesn't live reload a config after it changes. In
6+
other words, if you edit the config file, the DBus service's loaded config will
7+
show drift.
8+
9+
This is captured in the `StagedConfig` class, which holds both the config as served
10+
by the live DBus service, and the config as loaded from the same file. These are
11+
called the "active" and "target" config, respectively.
12+
"""
13+
114
from dataclasses import asdict, dataclass, fields
215
import json
316
from typing import Any, Dict, Generic, Literal, Self, TypeVar
@@ -20,6 +33,11 @@ class StagedAttr(Generic[T]):
2033
"""
2134
A staged attribute. Shows both the active and target value, and how the value is
2235
expected to change when applied.
36+
37+
Attributes:
38+
type (StageType): The type of staged change. Either "set", "unset" or None.
39+
active (T): The attribute value from the active config.
40+
target (T): The attribute value from the target config.
2341
"""
2442

2543
type: StageType
@@ -45,6 +63,14 @@ class StagedConfig:
4563
"""
4664
A staged configuration. Shows both the active and target configurations, and how
4765
the attributes are expected to change.
66+
67+
Attributes:
68+
active_config (Config): The active configuration, as loaded from the live
69+
DBus service.
70+
71+
target_config (Config): The target configuration, as loaded from the service's
72+
config file.
73+
dirty (bool): When true, there is drift between the active and target config.
4874
"""
4975

5076
def __init__(self: Self, active_config: Config, target_config: Config) -> None:
@@ -72,6 +98,10 @@ def _check_config_dirty(self: Self) -> None:
7298

7399
@property
74100
def file(self: Self) -> str:
101+
"""
102+
The path to the config file.
103+
"""
104+
75105
file = self.target_config.file
76106
assert file is not None, "Target config must be from a file"
77107
return file

crystalfontz/dbus/domain/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,16 @@
8888
DowTransactionResultT,
8989
GpioReadM,
9090
GpioReadT,
91+
KeyActivityReportM,
92+
KeyActivityReportT,
9193
KeypadPolledM,
9294
KeypadPolledT,
9395
LcdMemoryM,
9496
LcdMemoryT,
9597
PongM,
9698
PongT,
99+
TemperatureReportM,
100+
TemperatureReportT,
97101
VersionsM,
98102
VersionsT,
99103
)
@@ -116,6 +120,8 @@
116120
"DowTransactionResultT",
117121
"GpioReadM",
118122
"GpioReadT",
123+
"KeyActivityReportM",
124+
"KeyActivityReportT",
119125
"KeypadBrightnessM",
120126
"KeypadBrightnessT",
121127
"KeypadPolledM",
@@ -142,6 +148,8 @@
142148
"TemperatureUnitT",
143149
"TemperatureDisplayItemM",
144150
"TemperatureDisplayItemT",
151+
"TemperatureReportM",
152+
"TemperatureReportT",
145153
"TimeoutM",
146154
"TimeoutT",
147155
"VersionsM",

crystalfontz/dbus/domain/keys.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from crystalfontz.dbus.domain.base import ByteM, OptFloatM, OptFloatT, struct
44
from crystalfontz.keys import (
5+
KeyActivity,
56
KeyPress,
67
KeyState,
78
KeyStates,
@@ -95,3 +96,22 @@ class KeypadBrightnessM(OptFloatM):
9596
"""
9697

9798
t: ClassVar[str] = OptFloatM.t
99+
100+
101+
KeyActivityT = int
102+
103+
104+
class KeyActivityM:
105+
"""
106+
Map `KeyActivity` to and from `KeyActivityT` (`int`).
107+
"""
108+
109+
t: ClassVar[str] = ByteM.t
110+
111+
@staticmethod
112+
def pack(activity: KeyActivity) -> KeyActivityT:
113+
return activity.to_byte()
114+
115+
@staticmethod
116+
def unpack(activity: KeyActivityT) -> KeyActivity:
117+
return KeyActivity.from_byte(activity)

crystalfontz/dbus/domain/response.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,21 @@
2323
GpioStateM,
2424
GpioStateT,
2525
)
26-
from crystalfontz.dbus.domain.keys import KeyStatesM, KeyStatesT
26+
from crystalfontz.dbus.domain.keys import (
27+
KeyActivityM,
28+
KeyActivityT,
29+
KeyStatesM,
30+
KeyStatesT,
31+
)
2732
from crystalfontz.response import (
2833
DowDeviceInformation,
2934
DowTransactionResult,
3035
GpioRead,
36+
KeyActivityReport,
3137
KeypadPolled,
3238
LcdMemory,
3339
Pong,
40+
TemperatureReport,
3441
UserFlashAreaRead,
3542
Versions,
3643
)
@@ -240,3 +247,42 @@ def unpack(gpio_read: GpioReadT) -> GpioRead:
240247
requested_level=requested_level,
241248
settings=GpioSettingsM.unpack(settings),
242249
)
250+
251+
252+
KeyActivityReportT = KeyActivityT
253+
254+
255+
class KeyActivityReportM:
256+
"""
257+
Map `KeyActivityReport` to and from `KeyActivityReportT`
258+
"""
259+
260+
t: ClassVar[str] = KeyActivityM.t
261+
262+
@staticmethod
263+
def pack(report: KeyActivityReport) -> KeyActivityReportT:
264+
return KeyActivityM.pack(report.activity)
265+
266+
@staticmethod
267+
def unpack(report: KeyActivityReportT) -> KeyActivityReport:
268+
return KeyActivityReport(KeyActivityM.unpack(report))
269+
270+
271+
TemperatureReportT = Tuple[IndexT, float, float]
272+
273+
274+
class TemperatureReportM:
275+
"""
276+
Map `TemperatureReport` to and from `KeyActivityReportT`
277+
"""
278+
279+
t: ClassVar[str] = t(IndexM, "dd")
280+
281+
@staticmethod
282+
def pack(report: TemperatureReport) -> TemperatureReportT:
283+
return (report.index, report.celsius, report.fahrenheit)
284+
285+
@staticmethod
286+
def unpack(report: TemperatureReportT) -> TemperatureReport:
287+
index, celsius, fahrenheit = report
288+
return TemperatureReport(index, celsius, fahrenheit)

0 commit comments

Comments
 (0)