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 # Led-Badge-44x11
Upload tool for a LED name tag with USB-HID interface Upload tool for a LED name tag with USB-HID interface
![LED Mini Board](photos/blueBadge.jpg) ![LED Mini Board](photos/blueBadge.jpg)
@ -44,7 +45,7 @@ access to the badge via USB.
sudo apt install python3-usb python3-pil sudo apt install python3-usb python3-pil
#### manually using a python virtual environment #### manually using a python virtual environment
Using a venv will allow to use pip to install dependencies without the danger 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. 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' Find the inf-wizard.exe in the bin folder. Right click 'Run as Administrator'
Then continue as with windows 10 above. Then continue as with windows 10 above.
#### Examples: #### Examples:
Sudo may or may not be needed for accessing the USB device, depending on your system. Sudo may or may not be needed for accessing the USB device, depending on your system.
sudo python3 ./led-badge-11x44.py "Hello World!" 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!" 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: 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:" 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) ![LED Mini Board](photos/love_my_fablab.jpg)
sudo python3 ./led-badge-11x44.py -s7 -m0,1 :bicycle: :bicycle_r: 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) ![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 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 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: prints some condensed help:
python3 ./led-badge-11x44.py -h
<pre> <pre>
usage: lednamebadge.py [-h] [-t TYPE] [-H HID] [-M METHOD] [-E ENDPOINT] usage: lednamebadge.py [-h] [-t TYPE] [-H HID] [-M METHOD] [-D DEVICE_ID]
[-s SPEED] [-B BRIGHTNESS] [-m MODE] [-b BLINK] [-a ANTS] [-s SPEED] [-B BRIGHTNESS] [-m MODE] [-b BLINK]
[-l] [-a ANTS] [-l]
MESSAGE [MESSAGE ...] MESSAGE [MESSAGE ...]
Upload messages or graphics to a 11x44 led badge via USB HID. 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. -- see there for more examples and for updates.
positional arguments: positional arguments:
MESSAGE Up to 8 message texts with embedded builtin icons or MESSAGE Up to 8 message texts with embedded builtin icons or
loaded images within colons(:) -- See -l for a list of loaded images within colons(:) -- See -l for a list of
builtins builtins.
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-t TYPE, --type TYPE Type of display: supported values are 12x48 or (default) -t TYPE, --type TYPE Type of display: supported values are 12x48 or
11x44. Rename the program to led-badge-12x48, to switch (default) 11x44. Rename the program to led-
the default. badge-12x48, to switch the default.
-H HID, --hid HID Deprecated, only for backwards compatibility, please use -H HID, --hid HID Deprecated, only for backwards compatibility, please
-M! Set to 1 to ensure connect via HID API, program will use -M! Set to 1 to ensure connect via HID API,
then not fallback to usb.core library program will then not fallback to usb.core library.
-M METHOD, --method METHOD -M METHOD, --method METHOD
Force using the given write method ('hidapi' or 'libusb') Force using the given write method. Use one of 'auto',
-E ENDPOINT, --endpoint ENDPOINT 'list' or whatever list is printing.
Force using the given device endpoint -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 -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 -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) -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); -down(3); still-centered(4); animation(5); drop-
curtain(7); laser(8); See '--mode-help' for more details. down(6); curtain(7); laser(8); See '--mode-help' for
more details.
-b BLINK, --blink BLINK -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 -a ANTS, --ants ANTS 1: animated border, 0: normal. Up to 8 comma-separated
values values.
-l, --list-names list named icons to be embedded in messages and exit -l, --list-names list named icons to be embedded in messages and exit.
Example combining image and text: Example combining image and text:
sudo ./led-badge-11x44.py "I:HEART2:you" sudo lednamebadge.py "I:HEART2:you"
</pre> </pre>
There are some options defining the default type: 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 `lednamebadge.py` directly: default type is 11x44
- use led-badge-11x44.py: default type is 11x44 - rename `lednamebadge.py` to something with `12` and use that: default type is 12x48
- use led-badge-12x48.py: default type is 12x48 - use `led-badge-11x44.py`: default type is 11x44
For all these options you can override the default type with -t - 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 ### 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 ## Usage as module
### Writing to the device ### 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 - create the header
- append your own content - 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: The method `header()` takes a number of parameters:
- up to 8 lengths as a tuple of numbers - 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 - 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 corresponding bitmap data divided by 11 (for the 11x44 devices) respective 12 (for the 12x48-devices), where one
byte is 8 pixels wide. 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 - 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. 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 - 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. at the devices display.
@ -246,12 +295,52 @@ This would be achieved by these calls:
from lednamebadge import LedNameBadge from lednamebadge import LedNameBadge
buf = array('B') 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_one_bytes)
buf.extend(scene_two_bytes) buf.extend(scene_two_bytes)
LedNameBadge.write(buf) 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 ### Using the text generation
You can also use the text/icon/graphic generation of this module to get the corresponding byte buffers. 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 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 ```python
from lednamebadge import * from lednamebadge import *
creator = SimpleTextAndIcons() creator = SimpleTextAndIcons()
scene_x_bitmap = creator.bitmap("Hello :HEART2: World!") 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) lengths = (scene_x_bitmap[1], scene_y_bitmap[1], your_own_stuff.len)
buf = array('B') 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_x_bitmap[0])
buf.extend(scene_y_bitmap[0]) buf.extend(scene_y_bitmap[0])
buf.extend(your_own_stuff.bytes) 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. Run `python run_tests.py` from the `tests` directory.
## Related References (for USB-Serial devices) ## Related References (for USB-Serial devices)
* https://github.com/Caerbannog/led-mini-board
* http://zunkworks.com/projects/programmablelednamebadges/ * https://github.com/Caerbannog/led-mini-board
* https://github.com/DirkReiners/LEDBadgeProgrammer * http://zunkworks.com/projects/programmablelednamebadges/
* https://bitbucket.org/bartj/led/src * https://github.com/DirkReiners/LEDBadgeProgrammer
* http://www.daveakerman.com/?p=1440 * https://bitbucket.org/bartj/led/src
* https://github.com/stoggi/ledbadge * 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 # * 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 :) # somehow, but it is acceptable, as we do not need to save every processor cycle, here :)
# * Have fun! # * 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 import argparse
@ -369,7 +371,7 @@ class SimpleTextAndIcons:
except: except:
print("If you like to use images, the module pillow is needed. Try:") print("If you like to use images, the module pillow is needed. Try:")
print("$ pip install pillow") print("$ pip install pillow")
LedNameBadge._print_common_install_hints() LedNameBadge._print_common_install_hints('pillow', 'python3-pillow')
sys.exit(1) sys.exit(1)
im = Image.open(file) im = Image.open(file)
@ -410,35 +412,41 @@ class SimpleTextAndIcons:
class WriteMethod: class WriteMethod:
def __init__(self): def __init__(self):
self.devices = None self.devices = {}
def __del__(self): def __del__(self):
self.close() self.close()
def get_name(self):
raise NotImplementedError()
def get_description(self):
raise NotImplementedError()
def open(self, device_id): def open(self, device_id):
if self.is_ready(): if self.is_ready() and self.is_device_present():
self.devices = self._get_available_devices() actual_device_id = None
if self.devices and len(self.devices) > 0: if device_id == 'auto':
if device_id == 'auto' and len(self.devices) > 0: actual_device_id = sorted(self.devices.keys())[0]
# /***/ was, wenn kein gerät angeschlossen? else:
device_id = sorted(self.devices.keys())[0] if device_id in self.devices.keys():
elif device_id == 'list': actual_device_id = device_id
self.print_devices()
sys.exit(0) if actual_device_id:
elif device_id not in self.devices.keys(): return self._open(actual_device_id)
print("Device ID '%s' is unknown. Please choose from:" % (device_id,)) return False
self.print_devices()
sys.exit(1)
return self._open(device_id)
# /***/ was wenn kein Device? -> Alle Meldungen hier raus in die vorbereitung
return None
def close(self): def close(self):
raise NotImplementedError() raise NotImplementedError()
def print_devices(self): def get_available_devices(self):
for k, d in self.devices.items(): if self.is_ready() and not self.devices:
print("'%s': %s" % (k, d[0])) 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): def _open(self, device_id):
raise NotImplementedError() raise NotImplementedError()
@ -446,18 +454,6 @@ class WriteMethod:
def _get_available_devices(self): def _get_available_devices(self):
raise NotImplementedError() 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): def is_ready(self):
raise NotImplementedError() raise NotImplementedError()
@ -469,6 +465,18 @@ class WriteMethod:
self.check_length(buf, 8192) self.check_length(buf, 8192)
self._write(buf) 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): def _write(self, buf):
raise NotImplementedError() raise NotImplementedError()
@ -488,6 +496,12 @@ class WriteLibUsb(WriteMethod):
self.dev = None self.dev = None
self.endpoint = 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): def _open(self, device_id):
self.description = self.devices[device_id][0] self.description = self.devices[device_id][0]
self.dev = self.devices[device_id][1] self.dev = self.devices[device_id][1]
@ -496,19 +510,14 @@ class WriteLibUsb(WriteMethod):
return True return True
def close(self): def close(self):
if self.devices: for k, d in self.devices.items():
for k, d in self.devices.items(): d[1].reset()
d[1].reset() WriteLibUsb.usb.util.dispose_resources(d[1])
WriteLibUsb.usb.util.dispose_resources(d[1])
self.description = None self.description = None
self.dev = None self.dev = None
self.endpoint = None self.endpoint = None
print("Libusb: device resources freed")
def _get_available_devices(self): def _get_available_devices(self):
if not self.is_ready():
return {}
devs = WriteLibUsb.usb.core.find(idVendor=0x0416, idProduct=0x5020, find_all=True) devs = WriteLibUsb.usb.core.find(idVendor=0x0416, idProduct=0x5020, find_all=True)
devices = {} devices = {}
for d in devs: for d in devs:
@ -521,8 +530,8 @@ class WriteLibUsb(WriteMethod):
try: try:
d.set_configuration() d.set_configuration()
except(WriteLibUsb.usb.core.USBError): except(WriteLibUsb.usb.core.USBError):
# TODO: use all the nice output in _find_write_method(), somehow.
print("No write access to device!") print("No write access to device!")
# /***/
print("Maybe, you have to run this program with administrator rights.") print("Maybe, you have to run this program with administrator rights.")
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
print("* Try with sudo or add a udev rule like described in README.md.") 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) WriteLibUsb.usb.util.endpoint_direction(e.bEndpointAddress) == WriteLibUsb.usb.util.ENDPOINT_OUT)
for ep in eps: for ep in eps:
id = "%d:%d:%d" % (d.bus, d.address, ep.bEndpointAddress) 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) devices[id] = (descr, d, ep)
return devices return devices
@ -557,6 +566,7 @@ class WriteLibUsb(WriteMethod):
try: try:
self.dev.set_configuration() self.dev.set_configuration()
except(WriteLibUsb.usb.core.USBError): except(WriteLibUsb.usb.core.USBError):
# TODO: use all the nice output in _find_write_method(), somehow.
print("No write access to device!") print("No write access to device!")
print("Maybe, you have to run this program with administrator rights.") print("Maybe, you have to run this program with administrator rights.")
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
@ -585,6 +595,12 @@ class WriteUsbHidApi(WriteMethod):
self.path = None self.path = None
self.dev = 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): def _open(self, device_id):
self.description = self.devices[device_id][0] self.description = self.devices[device_id][0]
self.path = self.devices[device_id][1] self.path = self.devices[device_id][1]
@ -600,14 +616,13 @@ class WriteUsbHidApi(WriteMethod):
self.description = None self.description = None
self.path = None self.path = None
self.dev = None self.dev = None
print("Hidapi: device resources freed")
def _get_available_devices(self): def _get_available_devices(self):
device_infos = WriteUsbHidApi.pyhidapi.hid_enumerate(0x0416, 0x5020) device_infos = WriteUsbHidApi.pyhidapi.hid_enumerate(0x0416, 0x5020)
devices = {} devices = {}
for d in device_infos: for d in device_infos:
id = "%s" % (str(d.path.decode('ascii')),) 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) devices[id] = (descr, d.path)
return devices return devices
@ -720,126 +735,178 @@ class LedNameBadge:
write_method = LedNameBadge._find_write_method(method, device_id) write_method = LedNameBadge._find_write_method(method, device_id)
if write_method: if write_method:
write_method.write(buf) 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 @staticmethod
def _find_write_method(method, device_id): 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("Unknown write method '%s'." % (method,))
print("Available options: 'libusb', 'hidapi' and 'auto' (default)") LedNameBadge._print_available_methods(auto_order_methods)
sys.exit(1) sys.exit(1)
libusb = WriteLibUsb()
hidapi = WriteUsbHidApi()
# Python2 only with libusb
if method == 'auto': if method == 'auto':
if sys.version_info[0] < 3: if sys.version_info[0] < 3:
method = 'libusb' method = libusb.get_name()
print("Preferring method 'libusb' over 'hidapi' with Python 2.x because of https://github.com/jnweiger/led-badge-ls32/issues/9") 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'): elif sys.platform.startswith('darwin'):
method = 'hidapi' method = hidapi.get_name()
print("Selected method 'hidapi' with MacOs") print("Selected method %s with MacOs" % (hidapi.get_name(),))
elif sys.platform.startswith('win'): elif sys.platform.startswith('win'):
method = 'libusb' method = libusb.get_name()
print("Selected method 'libusb' with Windows") print("Selected method %s with Windows" % (libusb.get_name(),))
elif not libusb.is_ready() and not hidapi.is_ready(): elif not libusb.is_ready() and not hidapi.is_ready():
if sys.version_info[0] < 3 or sys.platform.startswith('win'): if sys.version_info[0] < 3 or sys.platform.startswith('win'):
print("You need the usb.core module.") LedNameBadge._print_libusb_install_hints(libusb.get_name())
LedNameBadge._print_libusb_install_hints()
sys.exit(1) sys.exit(1)
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
print("You need the pyhidapi module.") LedNameBadge._print_hidapi_install_hints(hidapi.get_name())
LedNameBadge._print_hidapi_install_hints()
sys.exit(1) sys.exit(1)
else: else:
print("You need the pyhidapi or usb.core module.") print("One of the python packages 'pyhidapi' or 'pyusb' is needed to run this program (or both).")
LedNameBadge._print_libusb_install_hints() LedNameBadge._print_libusb_install_hints(libusb.get_name())
LedNameBadge._print_hidapi_install_hints() LedNameBadge._print_hidapi_install_hints(hidapi.get_name())
sys.exit(1) sys.exit(1)
if method == 'libusb': if method == libusb.get_name():
if sys.platform.startswith('darwin'): 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.") print("Or help us implementing support for MacOs.")
sys.exit(1) sys.exit(1)
elif not libusb.is_ready(): elif not libusb.is_ready():
LedNameBadge._print_libusb_install_hints() LedNameBadge._print_libusb_install_hints(libusb.get_name())
sys.exit(1) sys.exit(1)
if method == 'hidapi': if method == hidapi.get_name():
if sys.platform.startswith('win'): 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.") print("Or help us implementing support for Windows.")
sys.exit(1) sys.exit(1)
elif sys.version_info[0] < 3: 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) sys.exit(1)
elif not hidapi.is_ready(): elif not hidapi.is_ready():
LedNameBadge._print_hidapi_install_hints() LedNameBadge._print_hidapi_install_hints(hidapi.get_name())
sys.exit(1) sys.exit(1)
if (method == 'auto' or method == 'hidapi'): first_method_found = None
method_obj = hidapi for m in auto_order_methods:
if method_obj.open(device_id): if method == 'auto' or method == m.get_name():
return method_obj if not first_method_found:
first_method_found = m
if (method == 'auto' or method == 'libusb'): if device_id == 'list':
method_obj = libusb LedNameBadge._print_available_devices(m)
if method_obj.open(device_id): sys.exit(0)
return method_obj elif m.open(device_id):
return m
device_id_str = '' device_id_str = ''
if device_id != 'auto': if device_id != 'auto':
device_id_str = ' with device_id %s' % (device_id,) device_id_str = ' with device_id %s' % (device_id,)
print("The device is not available with write method '%s'%s." % (method, device_id_str)) 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?") print("* Is a led tag device with vendorID 0x0416 and productID 0x5020 connected?")
if device_id != 'auto': if device_id != 'auto':
print("* Have you given the right device_id?") print("* Have you given the right device_id?")
if sys.platform.startswith('linux'): print(" Find the available device ids with option -D list")
# /***/
print(" Try this to find the available endpoint addresses:")
print(' $ lsusb -d 0416:5020 -v | grep -i "endpoint.*out"')
print("* If it is connected and still do not work, maybe you have to run") print("* If it is connected and still do not work, maybe you have to run")
print(" this program as root.") print(" this program as root.")
sys.exit(1) sys.exit(1)
@staticmethod @staticmethod
def _print_libusb_install_hints(): def get_auto_order_method_list():
print("The method 'libusb' is not possible to be used: The module usb.core could not be loaded.") return [WriteUsbHidApi(), WriteLibUsb()]
print("* Have you installed the Module? Try:")
@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") print(" $ pip install pyusb")
LedNameBadge._print_common_install_hints() LedNameBadge._print_common_install_hints('pyusb', 'python3-usb')
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
print("* Have you installed the libusb driver or libusb-filter for the device?") print("* Have you installed the libusb driver or libusb-filter for the device?")
elif sys.platform.startswith('linux'): 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") print(" $ sudo apt-get install libusb-1.0-0")
@staticmethod @staticmethod
def _print_hidapi_install_hints(): def _print_hidapi_install_hints(name):
print("The method 'hidapi' is not possible to be used: The module pyhidapi could not be loaded.") print("The method %s is not possible to be used:" % (name,))
print("* Have you installed the Module? Try:") print("The module 'pyhidapi' could not be loaded.")
print("* Have you installed the corresponding python package 'pyhidapi'? Try:")
print(" $ pip install pyhidapi") print(" $ pip install pyhidapi")
LedNameBadge._print_common_install_hints() LedNameBadge._print_common_install_hints('pyhidapi', 'python3-hidapi')
if sys.platform.startswith('darwin'): if sys.platform.startswith('darwin'):
print("* Have you installed the library itself? Try:") print("* Have you installed the library itself? Try:")
print(" $ brew install hidapi") print(" $ brew install hidapi")
elif sys.platform.startswith('linux'): 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(" $ 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/") print(" $ sudo ln -s /usr/lib/x86_64-linux-gnu/libhidapi-hidraw.so.0 /usr/local/lib/")
@staticmethod @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.)") print(" (You may need to use pip3 or pip2 instead of pip depending on your python version.)")
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
print(" (You may need to run cmd.exe as Administrator for system wide module installation.)") print(" (You may need to run cmd.exe as Administrator for system wide module installation.)")
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
print(" (You may need prepend 'sudo' for system wide module installation.)") 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(" (You may also use your package manager. Try the following, e.g for %s)" % (pip_package,))
print(" E.g. 'sudo apt install python3-usb' for pyusb)") 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(): def main():
@ -849,7 +916,7 @@ def main():
parser.add_argument('-t', '--type', default='11x44', 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.") 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('-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('-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('-s', '--speed', default='4', help="Scroll speed (Range 1..8). Up to 8 comma-separated values.")
parser.add_argument('-B', '--brightness', default='100', parser.add_argument('-B', '--brightness', default='100',
@ -927,10 +994,11 @@ def main():
# Translate -H to -M parameter # Translate -H to -M parameter
method = args.method method = args.method
if args.hid == 1: if args.hid == 1:
print("Option -H is deprecated, please use -M!")
if not method or method == 'auto': if not method or method == 'auto':
method = 'hidapi' method = 'hidapi'
else: 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) LedNameBadge.write(buf, method, args.device_id)

@ -4,159 +4,259 @@ from unittest.mock import patch, MagicMock
from io import StringIO from io import StringIO
class USBError(BaseException):
pass
class Test(TestCase): class Test(TestCase):
def setUp(self): def setUp(self):
print("Real platform: " + sys.platform) 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') @patch('sys.platform', new='linux')
def test_all_in_linux_positive(self): 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.assertIn('device initialized', output)
self.assertIsNotNone(method) 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.assertIn('device initialized', output)
self.assertIsNotNone(method) 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.assertIn('device initialized', output)
self.assertIsNotNone(method) self.assertIsNotNone(method)
@patch('sys.platform', new='linux') @patch('sys.platform', new='linux')
def test_only_one_lib_linux_positive(self): 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.assertIn('device initialized', output)
self.assertIsNotNone(method) 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.assertIn('device initialized', output)
self.assertIsNotNone(method) 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.assertIn('device initialized', output)
self.assertIsNotNone(method) 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.assertIn('device initialized', output)
self.assertIsNotNone(method) self.assertIsNotNone(method)
@patch('sys.platform', new='windows') @patch('sys.platform', new='windows')
def test_windows_positive(self): 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.assertIn('device initialized', output)
self.assertIsNotNone(method) 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.assertIn('device initialized', output)
self.assertIsNotNone(method) self.assertIsNotNone(method)
@patch('sys.platform', new='darwin') @patch('sys.platform', new='darwin')
def test_macos_positive(self): 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.assertIn('device initialized', output)
self.assertIsNotNone(method) 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.assertIn('device initialized', output)
self.assertIsNotNone(method) 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') @patch('sys.platform', new='linux')
def test_all_in_linux_negative(self): 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.assertNotIn('device initialized', output)
self.assertIn('device is not available', output) self.assertIn('device is not available', output)
self.assertIsNone(method) 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.assertNotIn('device initialized', output)
self.assertIn('device is not available', output) self.assertIn('device is not available', output)
self.assertIsNone(method) 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.assertNotIn('device initialized', output)
self.assertIn('device is not available', output) self.assertIn('device is not available', output)
self.assertIsNone(method) self.assertIsNone(method)
@patch('sys.platform', new='linux') @patch('sys.platform', new='linux')
def test_all_out_linux_negative(self): 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.assertNotIn('device initialized', output)
self.assertIn('You need', output) self.assertIn('One of the python packages', output)
self.assertIsNone(method) 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.assertNotIn('device initialized', output)
self.assertIn('is not possible to be used', output) self.assertIn('is not possible to be used', output)
self.assertIsNone(method) 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.assertNotIn('device initialized', output)
self.assertIn('is not possible to be used', output) self.assertIn('is not possible to be used', output)
self.assertIsNone(method) self.assertIsNone(method)
@patch('sys.platform', new='windows') @patch('sys.platform', new='windows')
def test_windows_negative(self): 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.assertNotIn('device initialized', output)
self.assertIn('please use method', output) self.assertIn('please use method', output)
self.assertIsNone(method) self.assertIsNone(method)
@patch('sys.platform', new='darwin') @patch('sys.platform', new='darwin')
def test_macos_negative(self): 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.assertNotIn('device initialized', output)
self.assertIn('please use method', output) self.assertIn('please use method', output)
self.assertIsNone(method) 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): def test_get_methods(self):
method_obj = None methods, output = self.call_info_methods()
output = None self.assertDictEqual({
with self.assertRaises(SystemExit): 'hidapi': True,
method_obj, output = self.call_it(pyusb_available, pyhidapi_available, device_available, method, endpoint) '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 return method_obj, output
def call_it(self, pyusb_available, pyhidapi_available, device_available, method, endpoint): def call_info_methods(self):
method_obj = None 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 output = None
with self.do_import_patch(pyusb_available, pyhidapi_available, device_available) as mock: with self.do_import_patch(pyusb_available, pyhidapi_available, device_available) as mock:
with patch('sys.stdout', new_callable=StringIO) as stdio_mock: with patch('sys.stdout', new_callable=StringIO) as stdio_mock:
import lednamebadge import lednamebadge
try: try:
method_obj = lednamebadge.LedNameBadge._find_write_method(method, endpoint) result = func(lednamebadge.LedNameBadge)
except(SystemExit): except(SystemExit):
pass pass
output = stdio_mock.getvalue() output = stdio_mock.getvalue()
print(output) print(output)
self.assertEqual(pyusb_available, 'usb.core detected' in output) return result, output
self.assertEqual(pyhidapi_available, 'pyhidapi detected' in output)
return method_obj, 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): def do_import_patch(self, pyusb_available, pyhidapi_available, device_available):
return patch.dict('sys.modules', patch_obj = patch.dict('sys.modules', {
{ 'pyhidapi': self.create_hid_mock(device_available) if pyhidapi_available else None,
'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': self.create_usb_mock(device_available) if pyusb_available else None, 'usb.core': MagicMock() 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): def create_hid_mock(self, device_available):
device = MagicMock() 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 = MagicMock()
mock.hid_enumerate.return_value = [device] if device_available else None mock.hid_enumerate.return_value = [device] if device_available else []
mock.hid_open_path.return_value = 'device' if device_available else None mock.hid_open_path.return_value = 123456 if device_available else []
return mock return mock
def create_usb_mock(self, device_available): 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 = MagicMock()
mock.core = 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 return mock

Loading…
Cancel
Save