Small refactorings, cleanup, documentation

pull/8/head^2
Ben Sartori 7 months ago
parent 6de3b1dce8
commit 94a2559b4a
  1. 194
      README.md
  2. 270
      lednamebadge.py
  3. 184
      tests/test_lednamebadge_select_method.py

@ -1,4 +1,5 @@
# Led-Badge-44x11
Upload tool for a LED name tag with USB-HID interface
![LED Mini Board](photos/blueBadge.jpg)
@ -44,7 +45,7 @@ access to the badge via USB.
sudo apt install python3-usb python3-pil
#### manually using a python virtual environment
Using a venv will allow to use pip to install dependencies without the danger
that the installed modules will interfere with the system installed ones.
@ -98,34 +99,44 @@ To reuse the venv again at a later point:
Find the inf-wizard.exe in the bin folder. Right click 'Run as Administrator'
Then continue as with windows 10 above.
#### Examples:
Sudo may or may not be needed for accessing the USB device, depending on your system.
sudo python3 ./led-badge-11x44.py "Hello World!"
loads the text 'Hello World!' as the first message, and scrolls it from right to left (default scroll mode=0) and speed 4 (default). After an upload the device shows the first message once and returns to the charging screen if still connected to USB. Either pull the plug or press the small button next to the USB connector.
loads the text 'Hello World!' as the first message, and scrolls it from right to left (default scroll mode=0) and speed
4 (default). After an upload the device shows the first message once and returns to the charging screen if still
connected to USB. Either pull the plug or press the small button next to the USB connector.
sudo python3 ./led-badge-11x44.py -m 6 -s 8 "Hello" "World!"
loads the text 'Hello' as message one and 'World!' as message two. Compare the difference in quoting to the previous example. Up to 8 messages can be uploaded. This example uses mode 6, which drops the words with a nice little animation vertically into the display area. Speed is set to maximum here, for smoothness.
loads the text 'Hello' as message one and 'World!' as message two. Compare the difference in quoting to the previous
example. Up to 8 messages can be uploaded. This example uses mode 6, which drops the words with a nice little animation
vertically into the display area. Speed is set to maximum here, for smoothness.
Per default you will only see 'Hello'. To see all messages, press the small button next to the USB connector multiple times, until you briefly see 'M1-8'. Now the display loops through all uploaded messages.
Per default, you will only see 'Hello'. To see all messages, press the small button next to the USB connector multiple
times, until you briefly see 'M1-8'. Now the display loops through all uploaded messages.
sudo python3 ./led-badge-11x44.py -m 5 :gfx/fablabnbg_logo_44x11.png:
loads a fullscreen still image. Avoid whitespace between colons and name. If you receive a message `ImportError: cannot import name '_imaging'`, then try to update the corresponding package: `sudo pip install -U Pillow`
loads a fullscreen still image. Avoid whitespace between colons and name. If you receive a
message `ImportError: cannot import name '_imaging'`, then try to update the corresponding
package: `sudo pip install -U Pillow`
sudo python3 ./led-badge-11x44.py "I:HEART2:my:gfx/fablab_logo_16x11.png:fablab:1:"
uses one builtin and one loaded image. The heart is builtin, and the fablab-logo is loaded from file. The fablab logo is used twice, once before the word 'fablab' and again behind through the reference ':1:' (which references the first loaded image).
uses one builtin and one loaded image. The heart is builtin, and the fablab-logo is loaded from file. The fablab logo is
used twice, once before the word 'fablab' and again behind through the reference ':1:' (which references the first
loaded image).
![LED Mini Board](photos/love_my_fablab.jpg)
sudo python3 ./led-badge-11x44.py -s7 -m0,1 :bicycle: :bicycle_r:
shows a bicycle crossing the display in left-to-right and right-to-left (as a second message). If you select the 'M1-8' mode, the bike permanently runs back and forth the display. You may add a short message to one or both, to make it appear the bike is pulling the text around.
shows a bicycle crossing the display in left-to-right and right-to-left (as a second message). If you select the 'M1-8'
mode, the bike permanently runs back and forth the display. You may add a short message to one or both, to make it
appear the bike is pulling the text around.
![LED Mini Board](photos/bicycle.gif)
@ -139,70 +150,108 @@ shows a simple animation of a slowly beating heart on the first message, and a b
python3 ./led-badge-11x44.py --list-names
prints the list of builtin icon names, including :happy: :happy2: :heart: :HEART: :heart2: :HEART2: :fablab: :bicycle: :bicycle_r: :owncloud: ::
prints the list of builtin icon names, including :happy: :happy2: :heart: :HEART: :heart2: :HEART2: :fablab: :bicycle: :
bicycle_r: :owncloud: ::
python3 ./led-badge-11x44.py --help
lists all write methods. Does not write anything to the device.
python3 ./led-badge-11x44.py -M list "dummy message"
lists all devices available with write method 'hidapi'. Does not write anything to the device.
python3 ./led-badge-11x44.py -M hidapi -D list "dummy message"
programs a specific device with a specific write method.
python3 ./led-badge-11x44.py -M hidapi -D "3-1:1.0" "Hello World!"
prints some condensed help:
python3 ./led-badge-11x44.py -h
<pre>
usage: lednamebadge.py [-h] [-t TYPE] [-H HID] [-M METHOD] [-E ENDPOINT]
[-s SPEED] [-B BRIGHTNESS] [-m MODE] [-b BLINK] [-a ANTS]
[-l]
usage: lednamebadge.py [-h] [-t TYPE] [-H HID] [-M METHOD] [-D DEVICE_ID]
[-s SPEED] [-B BRIGHTNESS] [-m MODE] [-b BLINK]
[-a ANTS] [-l]
MESSAGE [MESSAGE ...]
Upload messages or graphics to a 11x44 led badge via USB HID.
Version 0.12 from https://github.com/jnweiger/led-name-badge-ls32
Version 0.14 from https://github.com/jnweiger/led-badge-ls32
-- see there for more examples and for updates.
positional arguments:
MESSAGE Up to 8 message texts with embedded builtin icons or
loaded images within colons(:) -- See -l for a list of
builtins
builtins.
options:
-h, --help show this help message and exit
-t TYPE, --type TYPE Type of display: supported values are 12x48 or (default)
11x44. Rename the program to led-badge-12x48, to switch
the default.
-H HID, --hid HID Deprecated, only for backwards compatibility, please use
-M! Set to 1 to ensure connect via HID API, program will
then not fallback to usb.core library
-t TYPE, --type TYPE Type of display: supported values are 12x48 or
(default) 11x44. Rename the program to led-
badge-12x48, to switch the default.
-H HID, --hid HID Deprecated, only for backwards compatibility, please
use -M! Set to 1 to ensure connect via HID API,
program will then not fallback to usb.core library.
-M METHOD, --method METHOD
Force using the given write method ('hidapi' or 'libusb')
-E ENDPOINT, --endpoint ENDPOINT
Force using the given device endpoint
Force using the given write method. Use one of 'auto',
'list' or whatever list is printing.
-D DEVICE_ID, --device-id DEVICE_ID
Force using the given device id, if ambiguous. Usue
one of 'auto', 'list' or whatever list is printing.
-s SPEED, --speed SPEED
Scroll speed (Range 1..8). Up to 8 comma-separated values
Scroll speed (Range 1..8). Up to 8 comma-separated
values.
-B BRIGHTNESS, --brightness BRIGHTNESS
Brightness for the display in percent: 25, 50, 75, or 100
Brightness for the display in percent: 25, 50, 75, or
100.
-m MODE, --mode MODE Up to 8 mode values: Scroll-left(0) -right(1) -up(2)
-down(3); still-centered(4); animation(5); drop-down(6);
curtain(7); laser(8); See '--mode-help' for more details.
-down(3); still-centered(4); animation(5); drop-
down(6); curtain(7); laser(8); See '--mode-help' for
more details.
-b BLINK, --blink BLINK
1: blinking, 0: normal. Up to 8 comma-separated values
1: blinking, 0: normal. Up to 8 comma-separated
values.
-a ANTS, --ants ANTS 1: animated border, 0: normal. Up to 8 comma-separated
values
-l, --list-names list named icons to be embedded in messages and exit
values.
-l, --list-names list named icons to be embedded in messages and exit.
Example combining image and text:
sudo ./led-badge-11x44.py "I:HEART2:you"
sudo lednamebadge.py "I:HEART2:you"
</pre>
There are some options defining the default type:
- use lednamebadge.py directly: default type is 11x44
- rename lednamebadge.py to something with '12' and use that: default type is 12x48
- use led-badge-11x44.py: default type is 11x44
- use led-badge-12x48.py: default type is 12x48
For all these options you can override the default type with -t
- use `lednamebadge.py` directly: default type is 11x44
- rename `lednamebadge.py` to something with `12` and use that: default type is 12x48
- use `led-badge-11x44.py`: default type is 11x44
- use `led-badge-12x48.py`: default type is 12x48
For all these options you can override the default type with command line option `-t`
There are two options to controll which device is programmed with which method. At this time there are two write
methods:
one is using the python package pyusb (`libusb`), the other one is using pyhidapi (`hidapi`).
Depending on your execution environment both methods can be used, but sometime one does not work as expected. Then
you can choose the method to be used explicitly with option `-M`. With `-M list` you can print a list of available write
methods. If you have connected multiple devices, you can list up the ids with option `-D list` or give one of the
listed device ids to program that specific device. The default for both options is `auto`, which programs just the first
device found with preferably the write method `hidapi`. The IDs for the same device are different depending on the
write method. Also, they can change between computer startups or reconnects.
### Animations
See the gfx/starfield folder for examples. An animation of N frames is provided as an image N*48 pixels wide, for both 48 and 44 pixel wide devices.
See the gfx/starfield folder for examples. An animation of N frames is provided as an image N*48 pixels wide,
for both 48 and 44 pixel wide devices.
## Usage as module
### Writing to the device
You can use lednamebadge.py as a module in your own content creation code for writing your generated scenes to the device.
You can use lednamebadge.py as a module in your own content creation code for writing your generated scenes to
the device.
- create the header
- append your own content
@ -211,10 +260,10 @@ You can use lednamebadge.py as a module in your own content creation code for wr
The method `header()` takes a number of parameters:
- up to 8 lengths as a tuple of numbers
- each length is the number of byte-columns for the corresponding bitmap data, that is the number of bytes of the
corresponding bitmap data divided by 11 (for the 11x44 devices) respective 12 (for the 12x48-devices), where one
byte is 8 pixels wide.
- arguments comparable to the command line arguments: up to 8 speeds, modes, blink-flags, ants-flags each as tuple of
- each length is the number of byte-columns for the corresponding bitmap data, that is the number of bytes of the
corresponding bitmap data divided by 11 (for the 11x44 devices) respective 12 (for the 12x48-devices), where one
byte is 8 pixels wide.
- arguments comparable to the command line arguments: up to 8 speeds, modes, blink-flags, ants-flags each as tuple of
numbers, and an (optional) brightness as number.
- Optionally, you can give a timestamp as datetime. It is written to the device as part of the header, but not visible
at the devices display.
@ -246,12 +295,52 @@ This would be achieved by these calls:
from lednamebadge import LedNameBadge
buf = array('B')
buf.extend(LedNameBadge.header((4, 8), (3, 2), (4,), (0,), (0,1), 50))
buf.extend(LedNameBadge.header((4, 8), (3, 2), (4,), (0,), (0, 1), 50))
buf.extend(scene_one_bytes)
buf.extend(scene_two_bytes)
LedNameBadge.write(buf)
```
#### Specifying a write method or device id
There are two more parameters on the method `write`: the write method and the device id. They work exactly like the
command line option '-M' and '-D'. Both default to `auto`.
```
LedNameBadge.write(buf, 'libusb', '3:10:2')
```
Even with `list` you get the respective list of the available choices printed to stdout, which is less handy,
if used as a module. Therefore, there are 2 methods for retrieving this information as normal data objects:
1. `get_available_methods()` which returns all implemented write methods as a dict with the method names as
the keys and a boolean each as the values. The boolean indicates if the method is basically usable (means the
corresponding import succeeded)
1. `get_available_device_ids(method)` which returns information about all connected / available devices, also as
a dict with the device ids as the keys and a descriptive string each as the values.
```
>>> import lednamebadge
>>> lednamebadge.LedNameBadge.get_available_methods()
{'hidapi': True, 'libusb': True}
>>> lednamebadge.LedNameBadge.get_available_methods('hidapi')
{'3-6:1.0': 'LSicroelectronics - LS32 Custm HID (if=0)', '3-7.3:1.0': 'LSicroelectronics - LS32 Custm HID (if=0)', '3-1:1.0': 'wch.cn - CH583 (if=0)'}
>>> lednamebadge.LedNameBadge.get_available_methods('libusb')
{'3:20:1': 'LSicroelectronics - LS32 Custm HID (bus=3 dev=20 endpoint=1)', '3:21:1': 'LSicroelectronics - LS32 Custm HID (bus=3 dev=21 endpoint=1)', '3:18:2': 'wch.cn - CH583 (bus=3 dev=18 endpoint=2)'}
```
This way you can connect multiple devices to one computer and program them one by another with different calls
to `write`.
If you have mor than one with the same description string, it is difficult distinguish which real device belongs to
which id. Esp. after a reconnect or restart, the ids may change or exchange. If you have different USB buses, connect
only one device to a bus. So you can decide by bus number. Or keep a specific connect order (while the computer is
already running), then you can decide by device number. Maybe the hidapi method is a bit more reliable. You have
to experiment a bit.
### Using the text generation
You can also use the text/icon/graphic generation of this module to get the corresponding byte buffers.
@ -269,10 +358,10 @@ scene_c_bitmap = creator.bitmap("gfx/starfield/starfield_020.png")
```
The resulting bitmaps are tuples with the byte array and the length each. These lengths can be used in header() directly
and the byte arrays can be concatenated to the header. Examle:
and the byte arrays can be concatenated to the header. Example:
```python
from lednamebadge import *
from lednamebadge import *
creator = SimpleTextAndIcons()
scene_x_bitmap = creator.bitmap("Hello :HEART2: World!")
@ -281,7 +370,7 @@ your_own_stuff = create_own_bitmap_data()
lengths = (scene_x_bitmap[1], scene_y_bitmap[1], your_own_stuff.len)
buf = array('B')
buf.extend(LedNameBadge.header(lengths, (3,), (0,), (0,1,0), (0,0,1), 100))
buf.extend(LedNameBadge.header(lengths, (3,), (0,), (0, 1, 0), (0, 0, 1), 100))
buf.extend(scene_x_bitmap[0])
buf.extend(scene_y_bitmap[0])
buf.extend(your_own_stuff.bytes)
@ -301,9 +390,10 @@ Just run `plantuml "*.puml"` from the `photos` directory to regenerate all diagr
Run `python run_tests.py` from the `tests` directory.
## Related References (for USB-Serial devices)
* https://github.com/Caerbannog/led-mini-board
* http://zunkworks.com/projects/programmablelednamebadges/
* https://github.com/DirkReiners/LEDBadgeProgrammer
* https://bitbucket.org/bartj/led/src
* http://www.daveakerman.com/?p=1440
* https://github.com/stoggi/ledbadge
* https://github.com/Caerbannog/led-mini-board
* http://zunkworks.com/projects/programmablelednamebadges/
* https://github.com/DirkReiners/LEDBadgeProgrammer
* https://bitbucket.org/bartj/led/src
* http://www.daveakerman.com/?p=1440
* https://github.com/stoggi/ledbadge

@ -60,7 +60,9 @@
# * There is some initialization code executed in the classes not needed, if not imported. This is nagging me
# somehow, but it is acceptable, as we do not need to save every processor cycle, here :)
# * Have fun!
# v0.14, 2024-06-02, bs preparation for automatic or manual endpoint and bluetooth.
# v0.14, 2024-06-02, bs extending write methods.
# * Preparation for further write methods, like bluetooth.
# * Automatic or manual device and endpoint selection, See -M and -D (substituting -H)
import argparse
@ -369,7 +371,7 @@ class SimpleTextAndIcons:
except:
print("If you like to use images, the module pillow is needed. Try:")
print("$ pip install pillow")
LedNameBadge._print_common_install_hints()
LedNameBadge._print_common_install_hints('pillow', 'python3-pillow')
sys.exit(1)
im = Image.open(file)
@ -410,35 +412,41 @@ class SimpleTextAndIcons:
class WriteMethod:
def __init__(self):
self.devices = None
self.devices = {}
def __del__(self):
self.close()
def get_name(self):
raise NotImplementedError()
def get_description(self):
raise NotImplementedError()
def open(self, device_id):
if self.is_ready():
self.devices = self._get_available_devices()
if self.devices and len(self.devices) > 0:
if device_id == 'auto' and len(self.devices) > 0:
# /***/ was, wenn kein gerät angeschlossen?
device_id = sorted(self.devices.keys())[0]
elif device_id == 'list':
self.print_devices()
sys.exit(0)
elif device_id not in self.devices.keys():
print("Device ID '%s' is unknown. Please choose from:" % (device_id,))
self.print_devices()
sys.exit(1)
return self._open(device_id)
# /***/ was wenn kein Device? -> Alle Meldungen hier raus in die vorbereitung
return None
if self.is_ready() and self.is_device_present():
actual_device_id = None
if device_id == 'auto':
actual_device_id = sorted(self.devices.keys())[0]
else:
if device_id in self.devices.keys():
actual_device_id = device_id
if actual_device_id:
return self._open(actual_device_id)
return False
def close(self):
raise NotImplementedError()
def print_devices(self):
for k, d in self.devices.items():
print("'%s': %s" % (k, d[0]))
def get_available_devices(self):
if self.is_ready() and not self.devices:
self.devices = self._get_available_devices()
return {id: data[0] for id, data in self.devices.items()}
def is_device_present(self):
self.get_available_devices()
return self.devices and len(self.devices) > 0
def _open(self, device_id):
raise NotImplementedError()
@ -446,18 +454,6 @@ class WriteMethod:
def _get_available_devices(self):
raise NotImplementedError()
@staticmethod
def add_padding(buf, blocksize):
need_padding = len(buf) % blocksize
if need_padding:
buf.extend((0,) * (blocksize - need_padding))
@staticmethod
def check_length(buf, maxsize):
if len(buf) > maxsize:
print("Writing more than %d bytes damages the display!" % (maxsize,))
sys.exit(1)
def is_ready(self):
raise NotImplementedError()
@ -469,6 +465,18 @@ class WriteMethod:
self.check_length(buf, 8192)
self._write(buf)
@staticmethod
def add_padding(buf, blocksize):
need_padding = len(buf) % blocksize
if need_padding:
buf.extend((0,) * (blocksize - need_padding))
@staticmethod
def check_length(buf, maxsize):
if len(buf) > maxsize:
print("Writing more than %d bytes damages the display! Nothing written." % (maxsize,))
sys.exit(1)
def _write(self, buf):
raise NotImplementedError()
@ -488,6 +496,12 @@ class WriteLibUsb(WriteMethod):
self.dev = None
self.endpoint = None
def get_name(self):
return 'libusb'
def get_description(self):
return 'Program a device connected via USB using the pyusb package and libusb.'
def _open(self, device_id):
self.description = self.devices[device_id][0]
self.dev = self.devices[device_id][1]
@ -496,19 +510,14 @@ class WriteLibUsb(WriteMethod):
return True
def close(self):
if self.devices:
for k, d in self.devices.items():
d[1].reset()
WriteLibUsb.usb.util.dispose_resources(d[1])
for k, d in self.devices.items():
d[1].reset()
WriteLibUsb.usb.util.dispose_resources(d[1])
self.description = None
self.dev = None
self.endpoint = None
print("Libusb: device resources freed")
def _get_available_devices(self):
if not self.is_ready():
return {}
devs = WriteLibUsb.usb.core.find(idVendor=0x0416, idProduct=0x5020, find_all=True)
devices = {}
for d in devs:
@ -521,8 +530,8 @@ class WriteLibUsb(WriteMethod):
try:
d.set_configuration()
except(WriteLibUsb.usb.core.USBError):
# TODO: use all the nice output in _find_write_method(), somehow.
print("No write access to device!")
# /***/
print("Maybe, you have to run this program with administrator rights.")
if sys.platform.startswith('linux'):
print("* Try with sudo or add a udev rule like described in README.md.")
@ -533,7 +542,7 @@ class WriteLibUsb(WriteMethod):
WriteLibUsb.usb.util.endpoint_direction(e.bEndpointAddress) == WriteLibUsb.usb.util.ENDPOINT_OUT)
for ep in eps:
id = "%d:%d:%d" % (d.bus, d.address, ep.bEndpointAddress)
descr = "'%s %s' (bus=%d dev=%d endpoint=%d)" % (d.manufacturer, d.product, d.bus, d.address, ep.bEndpointAddress)
descr = "%s - %s (bus=%d dev=%d endpoint=%d)" % (d.manufacturer, d.product, d.bus, d.address, ep.bEndpointAddress)
devices[id] = (descr, d, ep)
return devices
@ -557,6 +566,7 @@ class WriteLibUsb(WriteMethod):
try:
self.dev.set_configuration()
except(WriteLibUsb.usb.core.USBError):
# TODO: use all the nice output in _find_write_method(), somehow.
print("No write access to device!")
print("Maybe, you have to run this program with administrator rights.")
if sys.platform.startswith('linux'):
@ -585,6 +595,12 @@ class WriteUsbHidApi(WriteMethod):
self.path = None
self.dev = None
def get_name(self):
return 'hidapi'
def get_description(self):
return 'Program a device connected via USB using the pyhidapi package and libhidapi.'
def _open(self, device_id):
self.description = self.devices[device_id][0]
self.path = self.devices[device_id][1]
@ -600,14 +616,13 @@ class WriteUsbHidApi(WriteMethod):
self.description = None
self.path = None
self.dev = None
print("Hidapi: device resources freed")
def _get_available_devices(self):
device_infos = WriteUsbHidApi.pyhidapi.hid_enumerate(0x0416, 0x5020)
devices = {}
for d in device_infos:
id = "%s" % (str(d.path.decode('ascii')),)
descr = "'%s %s' (if=%d)" % (d.manufacturer_string, d.product_string, d.interface_number)
descr = "%s - %s (if=%d)" % (d.manufacturer_string, d.product_string, d.interface_number)
devices[id] = (descr, d.path)
return devices
@ -720,126 +735,178 @@ class LedNameBadge:
write_method = LedNameBadge._find_write_method(method, device_id)
if write_method:
write_method.write(buf)
write_method.close()
@staticmethod
def get_available_methods():
auto_order_methods = LedNameBadge.get_auto_order_method_list()
return {m.get_name(): m.is_ready() for m in auto_order_methods}
@staticmethod
def get_available_device_ids(method):
auto_order_methods = LedNameBadge.get_auto_order_method_list()
wanted_method = [m for m in auto_order_methods if m.get_name() == method]
if wanted_method:
return wanted_method[0].get_available_devices()
return []
@staticmethod
def _find_write_method(method, device_id):
if method not in ('libusb', 'hidapi', 'auto'):
auto_order_methods = LedNameBadge.get_auto_order_method_list()
hidapi = [m for m in auto_order_methods if m.get_name() == 'hidapi'][0]
libusb = [m for m in auto_order_methods if m.get_name() == 'libusb'][0]
if method == 'list':
LedNameBadge._print_available_methods(auto_order_methods)
sys.exit(0)
if method not in [m.get_name() for m in auto_order_methods] and method != 'auto':
print("Unknown write method '%s'." % (method,))
print("Available options: 'libusb', 'hidapi' and 'auto' (default)")
LedNameBadge._print_available_methods(auto_order_methods)
sys.exit(1)
libusb = WriteLibUsb()
hidapi = WriteUsbHidApi()
# Python2 only with libusb
if method == 'auto':
if sys.version_info[0] < 3:
method = 'libusb'
print("Preferring method 'libusb' over 'hidapi' with Python 2.x because of https://github.com/jnweiger/led-badge-ls32/issues/9")
method = libusb.get_name()
print("Preferring method %s over %s with Python 2.x" % (libusb.get_name(), hidapi.get_name()))
print("because of https://github.com/jnweiger/led-badge-ls32/issues/9")
elif sys.platform.startswith('darwin'):
method = 'hidapi'
print("Selected method 'hidapi' with MacOs")
method = hidapi.get_name()
print("Selected method %s with MacOs" % (hidapi.get_name(),))
elif sys.platform.startswith('win'):
method = 'libusb'
print("Selected method 'libusb' with Windows")
method = libusb.get_name()
print("Selected method %s with Windows" % (libusb.get_name(),))
elif not libusb.is_ready() and not hidapi.is_ready():
if sys.version_info[0] < 3 or sys.platform.startswith('win'):
print("You need the usb.core module.")
LedNameBadge._print_libusb_install_hints()
LedNameBadge._print_libusb_install_hints(libusb.get_name())
sys.exit(1)
elif sys.platform.startswith('darwin'):
print("You need the pyhidapi module.")
LedNameBadge._print_hidapi_install_hints()
LedNameBadge._print_hidapi_install_hints(hidapi.get_name())
sys.exit(1)
else:
print("You need the pyhidapi or usb.core module.")
LedNameBadge._print_libusb_install_hints()
LedNameBadge._print_hidapi_install_hints()
print("One of the python packages 'pyhidapi' or 'pyusb' is needed to run this program (or both).")
LedNameBadge._print_libusb_install_hints(libusb.get_name())
LedNameBadge._print_hidapi_install_hints(hidapi.get_name())
sys.exit(1)
if method == 'libusb':
if method == libusb.get_name():
if sys.platform.startswith('darwin'):
print("For MacOs, please use method 'hidapi' or 'auto'.")
print("For MacOs, please use method '%s' or 'auto'." % (hidapi.get_name(),))
print("Or help us implementing support for MacOs.")
sys.exit(1)
elif not libusb.is_ready():
LedNameBadge._print_libusb_install_hints()
LedNameBadge._print_libusb_install_hints(libusb.get_name())
sys.exit(1)
if method == 'hidapi':
if method == hidapi.get_name():
if sys.platform.startswith('win'):
print("For Windows, please use method 'libusb' or 'auto'.")
print("For Windows, please use method '%s' or 'auto'." % (libusb.get_name(),))
print("Or help us implementing support for Windows.")
sys.exit(1)
elif sys.version_info[0] < 3:
print("Please use method 'libusb' or 'auto' with python-2.x because of https://github.com/jnweiger/led-badge-ls32/issues/9")
print("Please use method '%s' or 'auto' with python-2.x" % (libusb.get_name(),))
print("because of https://github.com/jnweiger/led-badge-ls32/issues/9")
sys.exit(1)
elif not hidapi.is_ready():
LedNameBadge._print_hidapi_install_hints()
LedNameBadge._print_hidapi_install_hints(hidapi.get_name())
sys.exit(1)
if (method == 'auto' or method == 'hidapi'):
method_obj = hidapi
if method_obj.open(device_id):
return method_obj
if (method == 'auto' or method == 'libusb'):
method_obj = libusb
if method_obj.open(device_id):
return method_obj
first_method_found = None
for m in auto_order_methods:
if method == 'auto' or method == m.get_name():
if not first_method_found:
first_method_found = m
if device_id == 'list':
LedNameBadge._print_available_devices(m)
sys.exit(0)
elif m.open(device_id):
return m
device_id_str = ''
if device_id != 'auto':
device_id_str = ' with device_id %s' % (device_id,)
print("The device is not available with write method '%s'%s." % (method, device_id_str))
if first_method_found:
LedNameBadge._print_available_devices(first_method_found)
print("* Is a led tag device with vendorID 0x0416 and productID 0x5020 connected?")
if device_id != 'auto':
print("* Have you given the right device_id?")
if sys.platform.startswith('linux'):
# /***/
print(" Try this to find the available endpoint addresses:")
print(' $ lsusb -d 0416:5020 -v | grep -i "endpoint.*out"')
print(" Find the available device ids with option -D list")
print("* If it is connected and still do not work, maybe you have to run")
print(" this program as root.")
sys.exit(1)
@staticmethod
def _print_libusb_install_hints():
print("The method 'libusb' is not possible to be used: The module usb.core could not be loaded.")
print("* Have you installed the Module? Try:")
def get_auto_order_method_list():
return [WriteUsbHidApi(), WriteLibUsb()]
@staticmethod
def _print_available_methods(methods):
print("Available write methods:")
print(" 'auto': selects the most appropriate of the available methods (default)")
for m in methods:
LedNameBadge._print_one_method(m)
@staticmethod
def _print_one_method(m):
print(" '%s': %s" % (m.get_name(), m.get_description()))
@staticmethod
def _print_available_devices(method_obj):
if method_obj.is_device_present():
print("Known device ids with method '%s' are:" % (method_obj.get_name(),))
for id, descr in sorted(method_obj.get_available_devices().items()):
LedNameBadge.print_one_device(id, descr)
else:
print("No devices with method '%s' found." % (method_obj.get_name(),))
@staticmethod
def print_one_device(id, descr):
print(" '%s': %s" % (id, descr))
@staticmethod
def _print_libusb_install_hints(name):
print("The method %s is not possible to be used:" % (name,))
print("The modules 'usb.core' and 'usb.util' could not be loaded.")
print("* Have you installed the corresponding python package 'pyusb'? Try:")
print(" $ pip install pyusb")
LedNameBadge._print_common_install_hints()
LedNameBadge._print_common_install_hints('pyusb', 'python3-usb')
if sys.platform.startswith('win'):
print("* Have you installed the libusb driver or libusb-filter for the device?")
elif sys.platform.startswith('linux'):
print("* Is the library itself installed? Try (or similar, suitable for your distro):")
print("* Is the library itself installed? Try the following")
print(" (or similar, suitable for your distro; the exact command and package name might be different):")
print(" $ sudo apt-get install libusb-1.0-0")
@staticmethod
def _print_hidapi_install_hints():
print("The method 'hidapi' is not possible to be used: The module pyhidapi could not be loaded.")
print("* Have you installed the Module? Try:")
def _print_hidapi_install_hints(name):
print("The method %s is not possible to be used:" % (name,))
print("The module 'pyhidapi' could not be loaded.")
print("* Have you installed the corresponding python package 'pyhidapi'? Try:")
print(" $ pip install pyhidapi")
LedNameBadge._print_common_install_hints()
LedNameBadge._print_common_install_hints('pyhidapi', 'python3-hidapi')
if sys.platform.startswith('darwin'):
print("* Have you installed the library itself? Try:")
print(" $ brew install hidapi")
elif sys.platform.startswith('linux'):
print("* Is the library itself installed? Try (or similar, suitable for your distro):")
print("* Is the library itself installed? Try the following")
print(" (or similar, suitable for your distro; the exact command and package name might be different):")
print(" $ sudo apt-get install libhidapi-hidraw0")
print("* If the library is still not found by the module, try (or similar, suitable for you distro):")
print("* If the library is still not found by the module. Try the following")
print(" (or similar, suitable for your distro; the exact command, library name and paths might be different):")
print(" $ sudo ln -s /usr/lib/x86_64-linux-gnu/libhidapi-hidraw.so.0 /usr/local/lib/")
@staticmethod
def _print_common_install_hints():
def _print_common_install_hints(pip_package, pm_package):
print(" (You may need to use pip3 or pip2 instead of pip depending on your python version.)")
if sys.platform.startswith('win'):
print(" (You may need to run cmd.exe as Administrator for system wide module installation.)")
if sys.platform.startswith('linux'):
print(" (You may need prepend 'sudo' for system wide module installation.)")
print(" (You may also use your package manager, but the exact package name might be different.")
print(" E.g. 'sudo apt install python3-usb' for pyusb)")
print(" (You may also use your package manager. Try the following, e.g for %s)" % (pip_package,))
print(" (or similar, suitable for your distro; the exact command and package name might be different):")
print(" $ sudo apt install %s" % (pm_package))
def main():
@ -849,7 +916,7 @@ def main():
parser.add_argument('-t', '--type', default='11x44',
help="Type of display: supported values are 12x48 or (default) 11x44. Rename the program to led-badge-12x48, to switch the default.")
parser.add_argument('-H', '--hid', default='0', help="Deprecated, only for backwards compatibility, please use -M! Set to 1 to ensure connect via HID API, program will then not fallback to usb.core library.")
parser.add_argument('-M', '--method', default='auto', help="Force using the given write method ('hidapi' or 'libusb').")
parser.add_argument('-M', '--method', default='auto', help="Force using the given write method. Use one of 'auto', 'list' or whatever list is printing.")
parser.add_argument('-D', '--device-id', default='auto', help="Force using the given device id, if ambiguous. Usue one of 'auto', 'list' or whatever list is printing.")
parser.add_argument('-s', '--speed', default='4', help="Scroll speed (Range 1..8). Up to 8 comma-separated values.")
parser.add_argument('-B', '--brightness', default='100',
@ -927,10 +994,11 @@ def main():
# Translate -H to -M parameter
method = args.method
if args.hid == 1:
print("Option -H is deprecated, please use -M!")
if not method or method == 'auto':
method = 'hidapi'
else:
sys.exit("Parameter values are ambiguous. Please use either -H or -M.")
sys.exit("Parameter values are ambiguous. Please use -M only.")
LedNameBadge.write(buf, method, args.device_id)

@ -4,159 +4,259 @@ from unittest.mock import patch, MagicMock
from io import StringIO
class USBError(BaseException):
pass
class Test(TestCase):
def setUp(self):
print("Real platform: " + sys.platform)
@patch('sys.platform', new='linux')
def test_list(self):
method, output = self.call_find(True, True, True, 'list', 'auto')
self.assertIn("Available write methods:", output)
self.assertIn("'auto'", output)
self.assertIn("'hidapi'", output)
self.assertIn("'libusb'", output)
method, output = self.call_find(True, True, True, 'hidapi', 'list')
self.assertIn("Known device ids with method 'hidapi' are:", output)
self.assertIn("'3-4:5-6': HidApi Test", output)
method, output = self.call_find(True, True, True, 'libusb', 'list')
self.assertIn("Known device ids with method 'libusb' are:", output)
self.assertIn("'3:4:2': LibUsb Test", output)
@patch('sys.platform', new='linux')
def test_unknown(self):
method, output = self.call_find(True, True, True, 'hello', 'auto')
self.assertIn("Unknown write method 'hello'", output)
self.assertIn("Available write methods:", output)
@patch('sys.platform', new='linux')
def test_all_in_linux_positive(self):
method, output = self.call_it(True, True, True, None, None)
method, output = self.call_find(True, True, True, 'auto', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
method, output = self.call_it(True, True, True, 'libusb', None)
method, output = self.call_find(True, True, True, 'libusb', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
method, output = self.call_it(True, True, True, 'hidapi', None)
method, output = self.call_find(True, True, True, 'hidapi', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
@patch('sys.platform', new='linux')
def test_only_one_lib_linux_positive(self):
method, output = self.call_it(False, True, True, None, None)
method, output = self.call_find(False, True, True, 'auto', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
method, output = self.call_it(False, True, True, 'hidapi', None)
method, output = self.call_find(False, True, True, 'hidapi', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
method, output = self.call_it(True, False, True, None, None)
method, output = self.call_find(True, False, True, 'auto', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
method, output = self.call_it(True, False, True, 'libusb', None)
method, output = self.call_find(True, False, True, 'libusb', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
@patch('sys.platform', new='windows')
def test_windows_positive(self):
method, output = self.call_it(True, False, True, None, None)
method, output = self.call_find(True, False, True, 'auto', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
method, output = self.call_it(True, False, True, 'libusb', None)
method, output = self.call_find(True, False, True, 'libusb', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
@patch('sys.platform', new='darwin')
def test_macos_positive(self):
method, output = self.call_it(False, True, True, None, None)
method, output = self.call_find(False, True, True, 'auto', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
method, output = self.call_it(False, True, True, 'hidapi', None)
method, output = self.call_find(False, True, True, 'hidapi', 'auto')
self.assertIn('device initialized', output)
self.assertIsNotNone(method)
@patch('sys.version_info', new=[2])
def test_python2_positive(self):
method, output = self.call_find(True, True, True, 'auto', 'auto')
self.assertIn('device initialized', output)
self.assertIn('Preferring method libusb', output)
self.assertIsNotNone(method)
#--------------------------------------------------------------------------
# -------------------------------------------------------------------------
@patch('sys.platform', new='linux')
def test_all_in_linux_negative(self):
method, output = self.call_it(True, True, False, None, None)
method, output = self.call_find(True, True, False, 'auto', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('device is not available', output)
self.assertIsNone(method)
method, output = self.call_it(True, True, False, 'libusb', None)
method, output = self.call_find(True, True, False, 'libusb', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('device is not available', output)
self.assertIsNone(method)
method, output = self.call_it(True, True, False, 'hidapi', None)
method, output = self.call_find(True, True, False, 'hidapi', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('device is not available', output)
self.assertIsNone(method)
@patch('sys.platform', new='linux')
def test_all_out_linux_negative(self):
method, output = self.call_it(False, False, False, None, None)
method, output = self.call_find(False, False, False, 'auto', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('You need', output)
self.assertIn('One of the python packages', output)
self.assertIsNone(method)
method, output = self.call_it(False, False, False, 'libusb', None)
method, output = self.call_find(False, False, False, 'libusb', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('is not possible to be used', output)
self.assertIsNone(method)
method, output = self.call_it(False, False, False, 'hidapi', None)
method, output = self.call_find(False, False, False, 'hidapi', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('is not possible to be used', output)
self.assertIsNone(method)
@patch('sys.platform', new='windows')
def test_windows_negative(self):
method, output = self.call_it(True, False, True, 'hidapi', None)
method, output = self.call_find(True, False, True, 'hidapi', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('please use method', output)
self.assertIsNone(method)
@patch('sys.platform', new='darwin')
def test_macos_negative(self):
method, output = self.call_it(False, True, True, 'libusb', None)
method, output = self.call_find(False, True, True, 'libusb', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('please use method', output)
self.assertIsNone(method)
@patch('sys.version_info', new=[2])
def test_python2_negative(self):
method, output = self.call_find(True, True, True, 'hidapi', 'auto')
self.assertNotIn('device initialized', output)
self.assertIn('Please use method', output)
self.assertIsNone(method)
#--------------------------------------------------------------------------
# -------------------------------------------------------------------------
def call_it_neg(self, pyusb_available, pyhidapi_available, device_available, method, endpoint):
method_obj = None
output = None
with self.assertRaises(SystemExit):
method_obj, output = self.call_it(pyusb_available, pyhidapi_available, device_available, method, endpoint)
def test_get_methods(self):
methods, output = self.call_info_methods()
self.assertDictEqual({
'hidapi': True,
'libusb': True}, methods)
def test_get_device_ids(self):
device_ids, output = self.call_info_ids('libusb')
self.assertDictEqual({
'3:4:2': 'LibUsb Test Manufacturer - LibUsb Test Product (bus=3 dev=4 endpoint=2)'},
device_ids)
device_ids, output = self.call_info_ids('hidapi')
self.assertDictEqual({
'3-4:5-6': 'HidApi Test Manufacturer - HidApi Test Product (if=0)'},
device_ids)
# -------------------------------------------------------------------------
def call_find(self, pyusb_available, pyhidapi_available, device_available, method, device_id):
self.print_test_conditions(pyusb_available, pyhidapi_available, device_available, method, device_id)
method_obj, output = self.prepare_modules(pyusb_available, pyhidapi_available, device_available,
lambda m: m._find_write_method(method, device_id))
self.assertEqual(pyusb_available, 'usb.core detected' in output)
self.assertEqual(pyhidapi_available, 'pyhidapi detected' in output)
return method_obj, output
def call_it(self, pyusb_available, pyhidapi_available, device_available, method, endpoint):
method_obj = None
def call_info_methods(self):
self.print_test_conditions(True, True, True, '-', '-')
return self.prepare_modules(True, True, True,
lambda m: m.get_available_methods())
def call_info_ids(self, method):
self.print_test_conditions(True, True, True, '-', '-')
return self.prepare_modules(True, True, True,
lambda m: m.get_available_device_ids(method))
def prepare_modules(self, pyusb_available, pyhidapi_available, device_available, func):
result = None
output = None
with self.do_import_patch(pyusb_available, pyhidapi_available, device_available) as mock:
with patch('sys.stdout', new_callable=StringIO) as stdio_mock:
import lednamebadge
try:
method_obj = lednamebadge.LedNameBadge._find_write_method(method, endpoint)
result = func(lednamebadge.LedNameBadge)
except(SystemExit):
pass
output = stdio_mock.getvalue()
print(output)
self.assertEqual(pyusb_available, 'usb.core detected' in output)
self.assertEqual(pyhidapi_available, 'pyhidapi detected' in output)
return method_obj, output
return result, output
def print_test_conditions(self, pyusb_available, pyhidapi_available, device_available, method, device_id):
print("Test condition: os=%s pyusb=%s pyhidapi=%s device=%s method=%s device_id=%s" % (
sys.platform,
'yes' if pyusb_available else 'no',
'yes' if pyhidapi_available else 'no',
'yes' if device_available else 'no',
method,
device_id))
def do_import_patch(self, pyusb_available, pyhidapi_available, device_available):
return patch.dict('sys.modules',
{
'pyhidapi': self.create_hid_mock(device_available) if pyhidapi_available else None,
'usb': self.create_usb_mock(device_available) if pyusb_available else None,
'usb.core': MagicMock() if pyusb_available else None})
patch_obj = patch.dict('sys.modules', {
'pyhidapi': self.create_hid_mock(device_available) if pyhidapi_available else None,
'usb': self.create_usb_mock(device_available) if pyusb_available else None,
'usb.core': MagicMock() if pyusb_available else None,
'usb.core.USBError': USBError if pyusb_available else None,
'usb.util': MagicMock() if pyusb_available else None})
# Assure fresh reimport of lednamebadge with current mocks
if 'lednamebadge' in sys.modules:
del sys.modules['lednamebadge']
return patch_obj
def create_hid_mock(self, device_available):
device = MagicMock()
device.path = 'devicepath'
device.path = b'3-4:5-6'
device.manufacturer_string = 'HidApi Test Manufacturer'
device.product_string = 'HidApi Test Product'
device.interface_number = 0
mock = MagicMock()
mock.hid_enumerate.return_value = [device] if device_available else None
mock.hid_open_path.return_value = 'device' if device_available else None
mock.hid_enumerate.return_value = [device] if device_available else []
mock.hid_open_path.return_value = 123456 if device_available else []
return mock
def create_usb_mock(self, device_available):
device = MagicMock()
device.manufacturer = 'LibUsb Test Manufacturer'
device.product = 'LibUsb Test Product'
device.bus = 3
device.address = 4
ep = MagicMock()
ep.bEndpointAddress = 2
mock = MagicMock()
mock.core = MagicMock()
mock.core.find.return_value = 'device' if device_available else None
mock.core.find.return_value = [device] if device_available else []
mock.core.USBError = USBError
mock.util.find_descriptor.return_value = [ep] if device_available else []
return mock

Loading…
Cancel
Save