Skip to content

Commit 3c11eb3

Browse files
committed
Somewhat large-ish change:
- Allow "led presets" (which specify 1 or more pin, colour settings) to be added to the user's config, and then apply them with the `apply_led_preset` command - Add the `list_devices` command, which enumerates serial devices and determines which ones are Busy Tag (or something compatible :-) - Change tool behaviour so that the config file is only updated when the `--device` flag is used - Bump the PyPI version to v0.1.1
1 parent a3a8fe2 commit 3c11eb3

File tree

5 files changed

+140
-31
lines changed

5 files changed

+140
-31
lines changed

.idea/busytag_tool.iml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# busytag_tool
2+
[![PyPI version](https://img.shields.io/pypi/v/busytag)](https://pypi.org/project/busytag/)
3+
![License](https://img.shields.io/pypi/l/busytag)
24

35
Python library and CLI to interact with [Busy Tag](https://www.busy-tag.com/) devices using
46
the [USB CDC interface]( https://luxafor.helpscoutdocs.com/article/47-busy-tag-usb-cdc-command-reference-guide).
@@ -11,14 +13,18 @@ $ pip install busytag
1113

1214
## CLI usage
1315

14-
The first time the tool is used, you should pass the device path through
15-
the flag `--device=/dev/whatever`. This will be saved in `~/.busytag.toml`
16-
and will be used in subsequent runs where `--device` is not passed.
16+
The first time the tool is used, you should pass the device path through the flag `--device=/dev/whatever`. This
17+
will be saved in `~/.busytag.toml` and will be used in later runs where `--device` is not passed. If you don't the port
18+
for your device, you can run `busytag-tool list_devices` to find it.
1719

1820
```shell
19-
$ busytag-tool
21+
$ busytag-tool
22+
23+
USAGE: busytag-tool [flags] <command> [<args>]
24+
2025
Available commands:
2126
help: Prints this message
27+
list_devices: Lists available devices
2228
info: Displays device information
2329
list_pictures: Lists pictures in device
2430
list_files: Lists files in device
@@ -28,15 +34,43 @@ Available commands:
2834
get <filename>: Copies <filename> from the device to the working directory
2935
rm <filename>: Deletes <filename>
3036
set_led_solid_color <6 hex RGB colour>: Sets the LEDs colour
37+
apply_led_preset <preset name>: Sets the LEDs colour according to a preset
3138
get_brightness: Gets current display brightness
3239
set_brightness <brightness>: Sets current display brightness (int between 1 and 100, inclusive
40+
41+
$ busytag-tool set_picture coding.png
42+
```
43+
44+
### Config
45+
46+
A config file with the device port is created at `~/.busytag.toml`. You can also add "led preset" entries there,
47+
which can then be used with the `apply_led_preset` to change the device's LED colours. For example, here are two
48+
entries, one that applies the same colour to all LEDs, and another that alternates colours:
49+
50+
```toml
51+
[[led_presets.red]]
52+
pins = 127
53+
color = 'FF0000'
54+
55+
[[led_presets.rb]]
56+
pins = 85
57+
color = 'FF0000'
58+
59+
[[led_presets.rb]]
60+
pins = 42
61+
color = '0000FF'
3362
```
3463
64+
The BusyTag device has seven LEDs (with the first one, 0, at the bottom left of the device), identified in this tool by
65+
powers of two. The `pins` entry in the config is the sum of which pins we want to apply the colour (so `127` applies
66+
to all, while `85` applies to pins 0, 2, 4 and 6).
67+
3568
## API usage
3669
3770
```python
38-
from busytag import Device
71+
from busytag import Device, LedConfig, LedPin
3972
4073
bt = Device('/dev/fooBar')
4174
bt.set_active_picture('coding.gif')
75+
bt.set_led_solid_color(LedConfig(LedPin.ALL, 'FF0000'))
4276
```

pyproject.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
[project]
22
name = "busytag"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
dynamic = ["dependencies"]
55
authors = [{ name = "Alex Coster", email = "[email protected]" }]
66
description = "A library and CLI tool to control Busy Tag devices."
77
readme = "README.md"
88
requires-python = ">=3.11"
99
license = "MIT"
1010
license-files = ["LICENSE"]
11-
classifiers = [
12-
"Development Status :: 2 - Pre-Alpha",
11+
classifiers = ["Development Status :: 2 - Pre-Alpha",
1312
"Environment :: Console",
14-
"Operating System :: OS Independent"
15-
]
13+
"Operating System :: OS Independent"]
1614

1715
[project.scripts]
1816
busytag-tool = "busytag.tool:run_main"

src/busytag/config.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,53 @@
11
# SPDX-License-Identifier: MIT
22

3-
import tomlkit
43
import os.path
4+
from typing import Optional, Sequence
5+
6+
import tomlkit
57

68
from .types import *
79

8-
from typing import Optional, Sequence
910

1011
class ToolConfig(object):
1112
def __init__(self, path: Optional[str] = None):
1213
self.device = None
1314
self.path = path
15+
self.led_presets = {}
1416
if path is not None:
1517
self.path = os.path.expanduser(path)
1618
if os.path.exists(self.path):
1719
self.__load_from_file()
1820

19-
2021
def write_to_file(self):
2122
assert self.path is not None
2223
conf = {}
24+
2325
if self.device is not None:
2426
conf['device'] = self.device
27+
28+
conf['led_presets'] = {}
29+
for name, settings in self.led_presets.items():
30+
preset = []
31+
for setting in settings:
32+
preset.append({'pins': int(setting.pins), 'color': setting.color})
33+
conf['led_presets'][name] = preset
34+
2535
with open(self.path, "w") as fp:
2636
tomlkit.dump(conf, fp)
2737

38+
def get_led_preset(self, name: str) -> Sequence[LedConfig]:
39+
if name not in self.led_presets:
40+
raise ValueError(f'LED preset {name} not found')
41+
return self.led_presets[name][::]
42+
2843
def __load_from_file(self):
2944
with open(self.path, 'rb') as fp:
3045
conf = tomlkit.load(fp)
31-
self.device = conf['device']
46+
if 'device' in conf:
47+
self.device = conf['device']
48+
49+
if 'led_presets' in conf:
50+
for key, entry in conf['led_presets'].items():
51+
self.led_presets[key] = []
52+
for pattern in entry:
53+
self.led_presets[key].append(LedConfig(LedPin(int(pattern['pins'])), pattern['color']))

src/busytag/tool.py

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/usr/bin/env python
22
# SPDX-License-Identifier: MIT
3-
4-
from absl import app, flags
5-
63
from os.path import basename, expanduser
7-
from typing import List
4+
from typing import List, Optional
5+
6+
import serial.tools.list_ports
7+
from absl import app, flags, logging
88

99
from .config import ToolConfig
1010
from .device import Device
@@ -15,30 +15,61 @@
1515
flags.DEFINE_string('device', None, 'Busy Tag\'s serial port.')
1616
flags.DEFINE_integer('baudrate', 115200, 'Connection baudrate.')
1717

18+
1819
def format_size(size: int) -> str:
1920
if size < 1_000:
2021
return f'{size} B'
2122
if size < 500_000:
22-
return f'{size/1_000:.2f} kB'
23-
return f'{size/1_000_000:.2f} MB'
24-
25-
26-
def main(argv: List[str]) -> None:
23+
return f'{size / 1_000:.2f} kB'
24+
return f'{size / 1_000_000:.2f} MB'
25+
26+
27+
def list_devices(baudrate: int) -> List[str]:
28+
devices = []
29+
ports = serial.tools.list_ports.comports()
30+
logging.debug(f'Probing {len(ports)} serial devices...')
31+
32+
for port in ports:
33+
try:
34+
logging.debug(f'Trying to connect to {port.device}')
35+
with serial.Serial(port.device, baudrate=baudrate, timeout=1.0) as conn:
36+
logging.debug(f'Connected to {port.device}')
37+
conn.write(b'AT+GDN\r\n')
38+
while True:
39+
response = conn.readline()
40+
logging.debug(f'Read from device: {response}')
41+
if response.startswith(b'+evn'):
42+
continue
43+
if response.startswith(b'+DN:busytag-'):
44+
devices.append(port.device)
45+
break
46+
except Exception:
47+
pass
48+
49+
return devices
50+
51+
52+
def main(argv: List[str]) -> Optional[int]:
2753
config = ToolConfig(FLAGS.config_file)
2854
if FLAGS.device is not None:
2955
config.device = FLAGS.device
30-
if config.device is None:
31-
raise Exception('Device must be specified')
56+
config.write_to_file()
3257

33-
bt:Device = None
58+
bt: Optional[Device] = None
3459

3560
# Remove argv[0]
36-
argv.pop(0)
61+
exec_name = argv.pop(0)
3762
command = 'help'
3863
if len(argv) > 0:
3964
command = argv.pop(0)
40-
bt = Device(config.device, baudrate=FLAGS.baudrate)
4165

66+
# Don't bother connecting for commands that don't need a device connection.
67+
if command not in ('list_devices', 'help'):
68+
if config.device is None:
69+
print('A device must be specified using the `--device` flag or be set in the config file!')
70+
print(f'You can run `{exec_name} list_devices` to find the available devices.')
71+
return 1
72+
bt = Device(config.device, baudrate=FLAGS.baudrate)
4273

4374
match command:
4475
case 'info':
@@ -49,6 +80,15 @@ def main(argv: List[str]) -> None:
4980
print(f'Storage capacity: {format_size(bt.capacity)}')
5081
print(f'Free storage: {format_size(bt.get_free_storage())}')
5182

83+
case 'list_devices':
84+
devices = list_devices(FLAGS.baudrate)
85+
if len(devices) == 0:
86+
print('No devices found')
87+
else:
88+
print('Available devices:')
89+
for device in devices:
90+
print(f' {device}')
91+
5292
case 'list_pictures':
5393
print('Pictures in device:')
5494
for picture in bt.list_pictures():
@@ -91,6 +131,15 @@ def main(argv: List[str]) -> None:
91131
led_config = LedConfig(LedPin.ALL, argv.pop(0).upper())
92132
bt.set_led_solid_color(led_config)
93133

134+
case 'apply_led_preset':
135+
assert len(argv) >= 1
136+
preset_name = argv.pop(0)
137+
138+
# Clear all LEDs first
139+
bt.set_led_solid_color(LedConfig(LedPin.ALL, '000000'))
140+
for e in config.led_presets.get(preset_name):
141+
bt.set_led_solid_color(e)
142+
94143
case 'get_brightness':
95144
print(f'Brightness: {bt.get_display_brightness()}')
96145

@@ -101,8 +150,10 @@ def main(argv: List[str]) -> None:
101150
bt.set_display_brightness(brightness)
102151

103152
case 'help':
153+
print(f'\n\tUSAGE: {exec_name} [flags] <command> [<args>]\n')
104154
print('Available commands:')
105155
print(' help: Prints this message')
156+
print(' list_devices: Lists available devices')
106157
print(' info: Displays device information')
107158
print(' list_pictures: Lists pictures in device')
108159
print(' list_files: Lists files in device')
@@ -112,16 +163,20 @@ def main(argv: List[str]) -> None:
112163
print(' get <filename>: Copies <filename> from the device to the working directory')
113164
print(' rm <filename>: Deletes <filename>')
114165
print(' set_led_solid_color <6 hex RGB colour>: Sets the LEDs colour')
166+
print(' apply_led_preset <preset name>: Sets the LEDs colour according to a preset')
115167
print(' get_brightness: Gets current display brightness')
116168
print(' set_brightness <brightness>: Sets current display brightness (int between 1 and 100, inclusive')
169+
print(f'\nFor flag documentation, run {exec_name} --help')
117170

118171
case _:
119172
print(f'Unknown command `{command}`. Please use the `help` to list available commands')
173+
return 1
174+
return 0
120175

121-
config.write_to_file()
122176

123177
def run_main():
124178
app.run(main)
125179

180+
126181
if __name__ == '__main__':
127-
run_main()
182+
run_main()

0 commit comments

Comments
 (0)