From 83fefd2404fb2ae81bded05030fb75269334113f Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Sat, 11 May 2024 14:10:16 +0200 Subject: [PATCH 01/19] Some documentation improvements --- README.md | 13 +++++++++++-- lednamebadge.py | 1 + photos/bitmap_data_descr.puml | 28 +++++++++++++++------------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 38eaf7a..7f1acfc 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,13 @@ To reuse the venv again at a later point: sudo dnf install hidapi python3-hidapi python3-pillow python3-pyusb +### Manual link the HID API lib for some Linuxes + +For some distributions there is a discrepancy between the where pyhidapi searches for the lib and where the package +hidapi places it. A simple solution is to link the libraray to the needed place, e.g. + + ln -s /usr/lib/libhidapi-hidraw.so.0 /usr/local/lib/ + ### Required dependencies on Mac Systems sudo easy_install pip @@ -146,7 +153,7 @@ prints the list of builtin icon names, including :happy: :happy2: :heart: :HEART prints some condensed help:
-usage: led-badge-11x44.py [-h] [-t TYPE] [-s SPEED] [-B BRIGHTNESS] [-m MODE] [-b BLINK] [-a ANTS] [-l] MESSAGE [MESSAGE ...] +usage: led-badge-11x44.py [-h] [-t TYPE] [-H HID] [-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 @@ -158,6 +165,7 @@ positional arguments: 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' Set to 1 to ensure connect via HID API, program will then not fallback to usb.core library -s SPEED, --speed SPEED Scroll speed (Range 1..8). Up to 8 comma-separated values -B BRIGHTNESS, --brightness BRIGHTNESS @@ -175,9 +183,10 @@ Example combining image and text: 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 +- rename lednamebadge.py to something with '12' (e.g. badge12.py) 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 ### Animations diff --git a/lednamebadge.py b/lednamebadge.py index f65c71e..81fd593 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -551,6 +551,7 @@ or print("No led tag with vendorID 0x0416 and productID 0x5020 found.") print("Connect the led tag and run this tool as root.") sys.exit(1) + for i in range(int(len(buf)/64)): # sendbuf must contain "report ID" as first byte. "0" does the job here. sendbuf=array('B',[0]) diff --git a/photos/bitmap_data_descr.puml b/photos/bitmap_data_descr.puml index 7268993..ccd55c6 100644 --- a/photos/bitmap_data_descr.puml +++ b/photos/bitmap_data_descr.puml @@ -1,9 +1,11 @@ +' Using .. for three dots / ellipsis, els it would be a dotted line + @startuml bitmap_data_onebyte salt title One byte {# { most significant bit 7 - leftmost pixel } | bit 6 | ... | bit 1 | { least significant bit 0 + leftmost pixel } | bit 6 | .. | bit 1 | { least significant bit 0 rightmost pixel } } @enduml @@ -12,17 +14,17 @@ title One byte salt title One scene {# - byte 0 == 8 pixel | byte 11 == 8 pixel | byte 22 == 8 pixel | ... - byte 1 ... | byte 12 ... | byte 23 ... | ... - byte 2 ... | byte 13 ... | byte 24 ... | ... - byte 3 ... | byte 14 ... | byte 25 ... | ... - byte 4 ... | byte 15 ... | byte 26 ... | ... - byte 5 ... | byte 16 ... | byte 27 ... | ... - byte 6 ... | byte 17 ... | byte 28 ... | ... - byte 7 ... | byte 18 ... | byte 29 ... | ... - byte 8 ... | byte 19 ... | byte 30 ... | ... - byte 9 ... | byte 20 ... | byte 31 ... | ... - byte 10 ... | byte 21 ... | byte 32 ... | ... + byte 0 == 8 pixel | byte 11 == 8 pixel | byte 22 == 8 pixel | .. + byte 1 ... | byte 12 ... | byte 23 ... | .. + byte 2 ... | byte 13 ... | byte 24 ... | .. + byte 3 ... | byte 14 ... | byte 25 ... | .. + byte 4 ... | byte 15 ... | byte 26 ... | .. + byte 5 ... | byte 16 ... | byte 27 ... | .. + byte 6 ... | byte 17 ... | byte 28 ... | .. + byte 7 ... | byte 18 ... | byte 29 ... | .. + byte 8 ... | byte 19 ... | byte 30 ... | .. + byte 9 ... | byte 20 ... | byte 31 ... | .. + byte 10 ... | byte 21 ... | byte 32 ... | .. } @enduml @@ -30,6 +32,6 @@ title One scene salt title Complete bitmap data {# - scene 0 == x bytes | ... | scene n == z bytes + scene 0 == x bytes | .. | scene n == z bytes } @enduml From 21d5665a42429b105dbd089500ec31cec36fcb12 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Sun, 26 May 2024 15:37:40 +0200 Subject: [PATCH 02/19] Some documentation improvements regarding issue #29 --- README.md | 89 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7f1acfc..330c60c 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,9 @@ To reuse the venv again at a later point: ### Manual link the HID API lib for some Linuxes -For some distributions there is a discrepancy between the where pyhidapi searches for the lib and where the package -hidapi places it. A simple solution is to link the libraray to the needed place, e.g. +For some distributions there is a discrepancy in where pyhidapi searches for the +lib and where the package hidapi places it. A simple solution is to link the +library to the needed place, e.g. ln -s /usr/lib/libhidapi-hidraw.so.0 /usr/local/lib/ @@ -80,63 +81,83 @@ hidapi places it. A simple solution is to link the libraray to the needed place, pip install pillow ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null brew install hidapi - or - Try with `pip3 install pyhidapi` -### Required dependencies on Windows 10 +or Try with - Download win/inf-wizard.exe to your desktop. Right click 'Run as Administrator' - -> Click 0x0416 0x5020 LS32 Custm HID - -> Next -> Next -> Documents LS32_Sustm_HID.inf -> Save (we don't need that file) - -> Install Now... -> Driver Install Complete -> OK + pip3 install pyhidapi - Download python from python.org - [x] install Launcher for all Users - [x] Add Python 3.7 to PATH - -> Click the 'Install Now ...' text message. +### Required dependencies on Windows 7/10 - Run cmd.exe as Administrator, enter: - pip install pyusb - pip install pillow +For Windows, we need to set up the libusb API for the LED badge device. +The way described here, uses [libusb-win32](https://github.com/mcuee/libusb-win32/wiki) +in a quite low level way and in a quite old version: -### Required dependencies on Windows 7 +- Please use version 1.2.6.0 of 'usblib-win32`. It's still available on the + [old project repo on SourceForge](https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/1.2.6.0/) +- Then + - Extract `bin/inf-wizard.exe` from the downloaded zip file. Right click and `Run as Administrator` + - `Next` -> Select `0x0416 0x5020 LS32 Custm HID` (or similar with the same IDs) + - `Next` -> `Next` -> Save as dialog `LS32_Sustm_HID.inf` -> `Save` (just to proceed, we don't need that file) + - `Install Now...` -> Driver Install Complete -> `OK` - Download win/win32-bin-1.2.6.0.zip to your desktop. Extract. - Find the inf-wizard.exe in the bin folder. Right click 'Run as Administrator' - Then continue as with windows 10 above. +There are other - meanwhile recommended, but untested here - ways to install and setup +newer versions of `libus-win32`: use +[Zadig](https://zadig.akeo.ie/) (it is also available from the old libusb-win32 repo on +[GitHub repo](https://github.com/mcuee/libusb-win32/releases) of newer releases) +or [libusbK](https://libusbk.sourceforge.net/UsbK3/index.html) +Of course, Python is needed: + +- Download latest python from [python.org](https://www.python.org/downloads/), +or specific versions from [here](https://www.python.org/downloads/windows/) + - Checkmark the following options + - `[x]` install Launcher for all Users + - `[x]` Add Python X.Y to PATH + - Click the `Install Now ...` text message. + +Install needed the Python packages. + +- Run cmd.exe as Administrator, enter: + + pip install pyusb + pip install pillow #### Examples: -Sudo may or may not be needed for accessing the USB device, depending on your system. +To run these examples on linux, you might have to prepend `sudo` for accessing +the USB device. On Windows, maybe you have to run the `cmd`, where you enter the +commands, with `Run as administrator`, which is similar to the `sudo`on linux. + +On systems with Python 2 *and* 3 installed, you might want to use Python 3 +explicitely by using `python3` instead of `python`. - sudo python3 ./led-badge-11x44.py "Hello World!" + python ./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. - sudo python3 ./led-badge-11x44.py -m 6 -s 8 "Hello" "World!" + python ./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. -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: + python ./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` - sudo python3 ./led-badge-11x44.py "I:HEART2:my:gfx/fablab_logo_16x11.png:fablab:1:" + python ./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). ![LED Mini Board](photos/love_my_fablab.jpg) - sudo python3 ./led-badge-11x44.py -s7 -m0,1 :bicycle: :bicycle_r: + python ./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. ![LED Mini Board](photos/bicycle.gif) - sudo python3 ./led-badge-11-x44.py -b0,1 -s1 -m5 " :heart2: :HEART2:" " :HEART2:" + python ./led-badge-11-x44.py -b0,1 -s1 -m5 " :heart2: :HEART2:" " :HEART2:" shows a simple animation of a slowly beating heart on the first message, and a blinking heart on the second message. @@ -144,11 +165,11 @@ shows a simple animation of a slowly beating heart on the first message, and a b ![M2 ishm](photos/m2ishm.gif) - python3 ./led-badge-11x44.py --list-names + python ./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: :: - python3 ./led-badge-11x44.py --help + python ./led-badge-11x44.py --help prints some condensed help: @@ -216,13 +237,13 @@ Your own content has to be a byte array with the bitmap data for all scenes. Of See the following graphic for better understanding: -![bitmap_data_onebyte.png](photos%2Fbitmap_data_onebyte.png) +![bitmap_data_onebyte.png](photos/bitmap_data_onebyte.png) -![bitmap_data_onescene.png](photos%2Fbitmap_data_onescene.png) +![bitmap_data_onescene.png](photos/bitmap_data_onescene.png) For a 12x48 device there have to be 12 bytes for each byte-column instead of 11, of course. -![bitmap_data_all.png](photos%2Fbitmap_data_all.png) +![bitmap_data_all.png](photos/bitmap_data_all.png) Example: @@ -262,7 +283,7 @@ 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 b9a7466208f3a3939700846265e8584fc33bdbf3 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Sun, 2 Jun 2024 07:25:10 +0200 Subject: [PATCH 03/19] Refactoring: modularity of write methods, improved error messages --- README.md | 28 +- lednamebadge.py | 317 ++++++++++++++++------- tests/test_lednamebadge_select_method.py | 162 ++++++++++++ 3 files changed, 402 insertions(+), 105 deletions(-) create mode 100644 tests/test_lednamebadge_select_method.py diff --git a/README.md b/README.md index 38eaf7a..814826b 100644 --- a/README.md +++ b/README.md @@ -146,27 +146,43 @@ prints the list of builtin icon names, including :happy: :happy2: :heart: :HEART prints some condensed help:-usage: led-badge-11x44.py [-h] [-t TYPE] [-s SPEED] [-B BRIGHTNESS] [-m MODE] [-b BLINK] [-a ANTS] [-l] MESSAGE [MESSAGE ...] +usage: lednamebadge.py [-h] [-t TYPE] [-H HID] [-M METHOD] [-E ENDPOINT] + [-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 -- 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 + MESSAGE Up to 8 message texts with embedded builtin icons or + loaded images within colons(:) -- See -l for a list of + 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. + -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 -s SPEED, --speed SPEED 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 - -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. + -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. -b BLINK, --blink BLINK 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 + -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 Example combining image and text: diff --git a/lednamebadge.py b/lednamebadge.py index f65c71e..dd8c48c 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -60,6 +60,7 @@ # * 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. import argparse @@ -71,7 +72,7 @@ from array import array from datetime import datetime -__version = "0.13" +__version = "0.14" class SimpleTextAndIcons: @@ -294,7 +295,6 @@ class SimpleTextAndIcons: for i in bitmap_named: bitmap_builtin[bitmap_named[i][2]] = bitmap_named[i] - def __init__(self): self.bitmap_preloaded = [([], 0)] self.bitmaps_preloaded_unused = False @@ -304,17 +304,14 @@ class SimpleTextAndIcons: self.bitmap_preloaded.append(SimpleTextAndIcons.bitmap_img(filename)) self.bitmaps_preloaded_unused = True - def are_preloaded_unused(self): """Still used by main, but deprecated. PLease use ":"-notation for bitmap() / bitmap_text()""" - return self.bitmaps_preloaded_unused == True - + return self.bitmaps_preloaded_unused is True @staticmethod def _get_named_bitmaps_keys(): return SimpleTextAndIcons.bitmap_named.keys() - def bitmap_char(self, ch): """Returns a tuple of 11 bytes, it is the bitmap data of given character. Example: ch = '_' returns (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255). @@ -330,7 +327,6 @@ class SimpleTextAndIcons: o = SimpleTextAndIcons.char_offsets[ch] return (SimpleTextAndIcons.font_11x44[o:o + 11], 1) - def bitmap_text(self, text): """Returns a tuple of (buffer, length_in_byte_columns_aka_chars) We preprocess the text string for substitution patterns @@ -361,7 +357,6 @@ class SimpleTextAndIcons: cols += n return (buf, cols) - @staticmethod def bitmap_img(file): """Returns a tuple of (buffer, length_in_byte_columns) representing the given image file. @@ -369,7 +364,12 @@ class SimpleTextAndIcons: grayscale by arithmetic mean. Threshold for an active led is then > 127. If the width is not a multiple on 8 it will be padded with empty pixel-columns. """ - from PIL import Image + try: + from PIL import Image + except: + print("If you like to use images, the module pillow is needed. Try:") + print("$ pip install pillow") + sys.exit(1) im = Image.open(file) print("fetching bitmap from file %s -> (%d x %d)" % (file, im.width, im.height)) @@ -398,7 +398,6 @@ class SimpleTextAndIcons: im.close() return (buf, cols) - def bitmap(self, arg): """If arg is a valid and existing path name, we load it as an image. Otherwise, we take it as a string (with ":"-notation, see bitmap_text()). @@ -408,58 +407,124 @@ class SimpleTextAndIcons: return self.bitmap_text(arg) -class LedNameBadge: - _protocol_header_template = ( - 0x77, 0x61, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - ) - _have_pyhidapi = False +class WriteMethod: + @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 has_device(self): + raise NotImplementedError() + + def write(self, buf): + self.add_padding(buf, 64) + self.check_length(buf, 8192) + self._write(buf) + + def _write(self, buf): + raise NotImplementedError() + + +class WriteLibUsb(WriteMethod): + _module_loaded = False try: - if sys.version_info[0] < 3: - print("Preferring Pyusb over Pyhidapi with Python 2.x") - raise Exception("Prefer usb.core with python-2.x because of https://github.com/jnweiger/led-badge-ls32/issues/9") - import pyhidapi - pyhidapi.hid_init() - _have_pyhidapi = True - print("Pyhidapi detected") + import usb.core + _module_loaded = True + print("Module pyusb detected") except: - try: - import usb.core - print("Pyusb detected") - except: - print("ERROR: Need the pyhidapi or usb.core module.") - if sys.platform == "darwin": - print("""Please try - pip3 install pyhidapi - pip install pyhidapi - brew install hidapi -""") - elif sys.platform == "linux": - print("""Please try - sudo pip3 install pyhidapi - sudo pip install pyhidapi - sudo apt-get install libhidapi-hidraw0 - sudo ln -s /usr/lib/x86_64-linux-gnu/libhidapi-hidraw.so.0 /usr/local/lib/ -or - sudo apt-get install python3-usb -""") - else: # windows? - print("""Please try with Linux or MacOS or help us implement support for """ + sys.platform) - sys.exit(1) + pass + def __init__(self, endpoint): + self.dev = None + if WriteLibUsb._module_loaded: + self.dev = WriteLibUsb.usb.core.find(idVendor=0x0416, idProduct=0x5020) + if self.dev: + print("Libusb device initialized") @staticmethod - def _prepare_iterable(iterable, min_, max_): + def is_ready(): + return WriteLibUsb._module_loaded + + def has_device(self): + return self.dev is not None + + def _write(self, buf): + if not self.dev: + return + try: - iterable = [min(max(x, min_), max_) for x in iterable] - iterable = tuple(iterable) + (iterable[-1],) * (8 - len(iterable)) # repeat last element - return iterable + # win32: NotImplementedError: is_kernel_driver_active + if self.dev.is_kernel_driver_active(0): + self.dev.detach_kernel_driver(0) except: - raise TypeError("Please give a list or tuple with at least one number: " + str(iterable)) + pass + self.dev.set_configuration() + print("Write using [%s %s] bus=%d dev=%d via libusb" % + (self.dev.manufacturer, self.dev.product, self.dev.bus, self.dev.address)) + for i in range(int(len(buf) / 64)): + time.sleep(0.1) + self.dev.write(1, buf[i * 64:i * 64 + 64]) + + +class WriteUsbHidApi(WriteMethod): + _module_loaded = False + try: + import pyhidapi + pyhidapi.hid_init() + _module_loaded = True + print("Module pyhidapi detected") + except: + pass + + def __init__(self, endpoint): + self.dev = None + self.dev_info = None + if WriteUsbHidApi._module_loaded: + self.dev_info = WriteUsbHidApi.pyhidapi.hid_enumerate(0x0416, 0x5020) + if self.dev_info: + self.dev = WriteUsbHidApi.pyhidapi.hid_open_path(self.dev_info[0].path) + if self.dev: + print("Hidapi device initialized") + + # alternative: self.dev = WriteUsbHidApi.pyhidapi.hid_open(0x0416, 0x5020) + + @staticmethod + def is_ready(): + return WriteUsbHidApi._module_loaded + + def has_device(self): + return self.dev is not None + + def _write(self, buf): + if not self.dev or not self.dev_info: + return + + print("Write using [%s %s] int=%d page=%s via hidapi" % ( + self.dev_info[0].manufacturer_string, self.dev_info[0].product_string, + self.dev_info[0].interface_number, self.dev_info[0].usage_page)) + for i in range(int(len(buf)/64)): + # sendbuf must contain "report ID" as first byte. "0" does the job here. + sendbuf = array('B', [0]) + # Then, put the 64 payload bytes into the buffer + sendbuf.extend(buf[i*64:i*64+64]) + WriteUsbHidApi.pyhidapi.hid_write(self.dev, sendbuf) + WriteUsbHidApi.pyhidapi.hid_close(self.dev) + +class LedNameBadge: + _protocol_header_template = ( + 0x77, 0x61, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) @staticmethod def header(lengths, speeds, modes, blinks, ants, brightness=100, date=datetime.now()): @@ -523,62 +588,106 @@ or return h + @staticmethod + def _prepare_iterable(iterable, min_, max_): + try: + iterable = [min(max(x, min_), max_) for x in iterable] + iterable = tuple(iterable) + (iterable[-1],) * (8 - len(iterable)) # repeat last element + return iterable + except: + raise TypeError("Please give a list or tuple with at least one number: " + str(iterable)) @staticmethod - def write(buf): + def write(buf, method = 'auto', endpoint = 'auto'): """Write the given buffer to the device. It has to begin with a protocol header as provided by header() and followed by the bitmap data. In short: the bitmap data is organized in bytes with 8 horizontal pixels per byte and 11 resp. 12 bytes per (8 pixels wide) byte-column. Then just put one byte-column after the other and one bitmap after the other. """ - need_padding = len(buf) % 64 - if need_padding: - buf.extend((0,) * (64 - need_padding)) + write_method = LedNameBadge._find_write_method(method, endpoint) + if write_method: + write_method.write(buf) - if len(buf) > 8192: - print("Writing more than 8192 bytes damages the display!") + @staticmethod + def _find_write_method(method, endpoint): + if method is None: + method = 'auto' + + if endpoint is None: + endpoint = 'auto' + + if method not in ('libusb', 'hidapi', 'auto'): + print("Unknown write method '%s'." % (method,)) sys.exit(1) - if LedNameBadge._have_pyhidapi: - dev_info = LedNameBadge.pyhidapi.hid_enumerate(0x0416, 0x5020) - # dev = pyhidapi.hid_open(0x0416, 0x5020) - if dev_info: - dev = LedNameBadge.pyhidapi.hid_open_path(dev_info[0].path) - print("using [%s %s] int=%d page=%s via pyHIDAPI" % ( - dev_info[0].manufacturer_string, dev_info[0].product_string, dev_info[0].interface_number, dev_info[0].usage_page)) - else: - print("No led tag with vendorID 0x0416 and productID 0x5020 found.") - print("Connect the led tag and run this tool as root.") + if method == 'libusb': + if sys.platform == "darwin": + print("For MacOs, please use method 'hidapi' or 'auto'.") + print("Or help us implementing support for MacOs.") sys.exit(1) - for i in range(int(len(buf)/64)): - # sendbuf must contain "report ID" as first byte. "0" does the job here. - sendbuf=array('B',[0]) - # Then, put the 64 payload bytes into the buffer - sendbuf.extend(buf[i*64:i*64+64]) - LedNameBadge.pyhidapi.hid_write(dev, sendbuf) - LedNameBadge.pyhidapi.hid_close(dev) - else: - dev = LedNameBadge.usb.core.find(idVendor=0x0416, idProduct=0x5020) - if dev is None: - print("No led tag with vendorID 0x0416 and productID 0x5020 found.") - print("Connect the led tag and run this tool as root.") + elif not WriteLibUsb.is_ready(): + print("The method 'libusb' is not possible to be used: The module could not be loaded.") + print("* Have you installed the Module? Try:") + print(" $ pip install pyusb") + if sys.platform == 'windows': + print("""* Have you installed the libusb driver or libusb-filter for the device?""") sys.exit(1) - try: - # win32: NotImplementedError: is_kernel_driver_active - if dev.is_kernel_driver_active(0): - dev.detach_kernel_driver(0) - except: - pass - dev.set_configuration() - print("using [%s %s] bus=%d dev=%d" % (dev.manufacturer, dev.product, dev.bus, dev.address)) - for i in range(int(len(buf) / 64)): - time.sleep(0.1) - dev.write(1, buf[i * 64:i * 64 + 64]) + if method == 'hidapi': + if sys.platform == "windows": + print("For Windows, please use method 'libusb' or 'auto'.") + 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") + sys.exit(1) + elif not WriteUsbHidApi.is_ready(): + print("The method 'hidapi' is not possible to be used: The module could not be loaded.") + print("* Have you installed the Module? Try:") + print(" $ pip install pyhidapi") + if sys.platform == 'darwin': + print("* Have you installed the library itself? Try:") + print(" $ brew install hidapi") + elif sys.platform == 'linux': + print(" or") + print(" $ sudo apt-get install python3-usb") + print("* Is the library itself installed? Try (or similar, suitable for your distro):") + 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(" $ sudo ln -s /usr/lib/x86_64-linux-gnu/libhidapi-hidraw.so.0 /usr/local/lib/") + sys.exit(1) + + # Python2 only with libusb + if method == 'auto' and 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") + + if (method == 'auto' or method == 'hidapi') and WriteUsbHidApi.is_ready(): + method_obj = WriteUsbHidApi(endpoint) + if method_obj.has_device(): + return method_obj + + if (method == 'auto' or method == 'libusb') and WriteLibUsb.is_ready(): + method_obj = WriteLibUsb(endpoint) + if method_obj.has_device(): + return method_obj + + endpoint_str = '' + if endpoint != 'auto': + endpoint = int(endpoint) + endpoint_str = ' on endpoint %d' % (endpoint,) + + print("The device is not available with write method '%s'%s." % (method, endpoint_str)) + print("* Is a led tag device with vendorID 0x0416 and productID 0x5020 connected?") + if endpoint != 'auto': + print("* Have you given the right endpoint?") + if sys.platform == "linux": + 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 this program as root.") + sys.exit(1) -def split_to_ints(list_str): - return [int(x) for x in re.split(r'[\s,]+', list_str)] def main(): parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, @@ -586,7 +695,9 @@ def main(): epilog='Example combining image and text:\n sudo %s "I:HEART2:you"' % sys.argv[0]) 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="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('-E', '--endpoint', default='auto', help="Force using the given device endpoint") 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', help="Brightness for the display in percent: 25, 50, 75, or 100") @@ -660,11 +771,19 @@ def main(): for msg_bitmap in msg_bitmaps: buf.extend(msg_bitmap[0]) - if not LedNameBadge._have_pyhidapi: - if args.hid != "0": - sys.exit("HID API access is needed but not initialized. Fix your setup") + # Translate -H to -M parameter + method = args.method + if args.hid == 1: + if not method or method == 'auto': + method = 'hidapi' + else: + sys.exit("Parameter values are ambiguous. Please use either -H or -M.") + + LedNameBadge.write(buf, method, args.endpoint) + - LedNameBadge.write(buf) +def split_to_ints(list_str): + return [int(x) for x in re.split(r'[\s,]+', list_str)] if __name__ == '__main__': diff --git a/tests/test_lednamebadge_select_method.py b/tests/test_lednamebadge_select_method.py new file mode 100644 index 0000000..a9a67f3 --- /dev/null +++ b/tests/test_lednamebadge_select_method.py @@ -0,0 +1,162 @@ +import sys +from unittest import TestCase +from unittest.mock import patch, MagicMock +from io import StringIO + + +class Test(TestCase): + def setUp(self): + print("Real platform: " + sys.platform) + + @patch('sys.platform', new='linux') + def test_all_in_linux_positive(self): + method, output = self.call_it(True, True, True, None, None) + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_it(True, True, True, 'libusb', None) + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_it(True, True, True, 'hidapi', None) + 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) + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_it(False, True, True, 'hidapi', None) + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_it(True, False, True, None, None) + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_it(True, False, True, 'libusb', None) + 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) + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_it(True, False, True, 'libusb', None) + 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) + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_it(False, True, True, 'hidapi', None) + self.assertIn('device initialized', 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) + 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) + 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) + 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) + self.assertNotIn('device initialized', output) + self.assertIn('device is not available', output) + self.assertIsNone(method) + + method, output = self.call_it(False, False, False, 'libusb', None) + 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) + 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) + 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) + 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) + return method_obj, output + + def call_it(self, pyusb_available, pyhidapi_available, device_available, method, endpoint): + method_obj = 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) + except(SystemExit): + pass + output = stdio_mock.getvalue() + print(output) + self.assertEqual(pyusb_available, 'pyusb detected' in output) + self.assertEqual(pyhidapi_available, 'pyhidapi detected' in output) + return method_obj, output + + 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}) + + def create_hid_mock(self, device_available): + device = MagicMock() + device.path = 'devicepath' + + 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 + return mock + + def create_usb_mock(self, device_available): + mock = MagicMock() + mock.core = MagicMock() + mock.core.find.return_value = 'device' if device_available else None + return mock From 52c335ffa9e04d6955d58d4ba175659035ada243 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Sun, 2 Jun 2024 13:33:48 +0200 Subject: [PATCH 04/19] Tested with python2, error handling and messages improved. --- lednamebadge.py | 58 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/lednamebadge.py b/lednamebadge.py index dd8c48c..8091d48 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -369,6 +369,7 @@ class SimpleTextAndIcons: except: print("If you like to use images, the module pillow is needed. Try:") print("$ pip install pillow") + LedNameBadge.print_install_message() sys.exit(1) im = Image.open(file) @@ -431,7 +432,6 @@ class WriteMethod: def _write(self, buf): raise NotImplementedError() - class WriteLibUsb(WriteMethod): _module_loaded = False try: @@ -465,7 +465,16 @@ class WriteLibUsb(WriteMethod): self.dev.detach_kernel_driver(0) except: pass - self.dev.set_configuration() + + try: + self.dev.set_configuration() + except(WriteLibUsb.usb.core.USBError): + print("No write access to device!") + print("Maybe, you have to run this program with administrator rights.") + if 'linux' in sys.platform: + print("* Try with sudo or add a udev rule like described in README.md.") + sys.exit(1) + print("Write using [%s %s] bus=%d dev=%d via libusb" % (self.dev.manufacturer, self.dev.product, self.dev.bus, self.dev.address)) for i in range(int(len(buf) / 64)): @@ -621,8 +630,20 @@ class LedNameBadge: print("Unknown write method '%s'." % (method,)) sys.exit(1) + # 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") + elif sys.platform == 'darwin': + method = 'hidapi' + print("Selected method 'hidapi' with MacOs") + elif sys.platform == 'windows': + method = 'libusb' + print("Selected method 'libusb' with Windows") + if method == 'libusb': - if sys.platform == "darwin": + if sys.platform == 'darwin': print("For MacOs, please use method 'hidapi' or 'auto'.") print("Or help us implementing support for MacOs.") sys.exit(1) @@ -630,12 +651,16 @@ class LedNameBadge: print("The method 'libusb' is not possible to be used: The module could not be loaded.") print("* Have you installed the Module? Try:") print(" $ pip install pyusb") + LedNameBadge._print_install_hints() if sys.platform == 'windows': - 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 'linux' in sys.platform: + print("* Is the library itself installed? Try (or similar, suitable for your distro):") + print(" $ sudo apt-get install libusb-1.0-0") sys.exit(1) if method == 'hidapi': - if sys.platform == "windows": + if sys.platform == 'windows': print("For Windows, please use method 'libusb' or 'auto'.") print("Or help us implementing support for Windows.") sys.exit(1) @@ -646,23 +671,17 @@ class LedNameBadge: print("The method 'hidapi' is not possible to be used: The module could not be loaded.") print("* Have you installed the Module? Try:") print(" $ pip install pyhidapi") + LedNameBadge._print_install_hints() if sys.platform == 'darwin': print("* Have you installed the library itself? Try:") print(" $ brew install hidapi") - elif sys.platform == 'linux': - print(" or") - print(" $ sudo apt-get install python3-usb") + elif 'linux' in sys.platform: print("* Is the library itself installed? Try (or similar, suitable for your distro):") 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(" $ sudo ln -s /usr/lib/x86_64-linux-gnu/libhidapi-hidraw.so.0 /usr/local/lib/") sys.exit(1) - # Python2 only with libusb - if method == 'auto' and 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") - if (method == 'auto' or method == 'hidapi') and WriteUsbHidApi.is_ready(): method_obj = WriteUsbHidApi(endpoint) if method_obj.has_device(): @@ -682,12 +701,21 @@ class LedNameBadge: print("* Is a led tag device with vendorID 0x0416 and productID 0x5020 connected?") if endpoint != 'auto': print("* Have you given the right endpoint?") - if sys.platform == "linux": + if 'linux' in sys.platform: 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 this program as root.") + 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_install_hints(): + print(" (You may need to use pip3 or pip2 instead of pip depending on your python version.)") + print(" (You may need prepend 'sudo' for system wide module installation.)") + if 'linux' in sys.platform: + print(" (You may also use your package manager, but the exact package might be different.") + print(" E.g. 'sudo apt install python3-usb' for pyusb)") + def main(): parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, From 5597db11e6c3fa847da4ffbab683d21f72392f0c Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Mon, 3 Jun 2024 08:22:23 +0200 Subject: [PATCH 05/19] Tested with windows, improved error handling. --- lednamebadge.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lednamebadge.py b/lednamebadge.py index 8091d48..13a4c2c 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -471,7 +471,7 @@ class WriteLibUsb(WriteMethod): except(WriteLibUsb.usb.core.USBError): print("No write access to device!") print("Maybe, you have to run this program with administrator rights.") - if 'linux' in sys.platform: + if sys.platform.startswith('linux'): print("* Try with sudo or add a udev rule like described in README.md.") sys.exit(1) @@ -628,6 +628,7 @@ class LedNameBadge: if method not in ('libusb', 'hidapi', 'auto'): print("Unknown write method '%s'." % (method,)) + print("Available options: 'libusb', 'hidapi' and 'auto' (default)") sys.exit(1) # Python2 only with libusb @@ -635,15 +636,15 @@ class LedNameBadge: 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") - elif sys.platform == 'darwin': + elif sys.platform.startswith('darwin'): method = 'hidapi' print("Selected method 'hidapi' with MacOs") - elif sys.platform == 'windows': + elif sys.platform.startswith('win'): method = 'libusb' print("Selected method 'libusb' with Windows") if method == 'libusb': - if sys.platform == 'darwin': + if sys.platform.startswith('darwin'): print("For MacOs, please use method 'hidapi' or 'auto'.") print("Or help us implementing support for MacOs.") sys.exit(1) @@ -652,15 +653,15 @@ class LedNameBadge: print("* Have you installed the Module? Try:") print(" $ pip install pyusb") LedNameBadge._print_install_hints() - if sys.platform == 'windows': + if sys.platform.startswith('win'): print("* Have you installed the libusb driver or libusb-filter for the device?") - elif 'linux' in sys.platform: + elif sys.platform.startswith('linux'): print("* Is the library itself installed? Try (or similar, suitable for your distro):") print(" $ sudo apt-get install libusb-1.0-0") sys.exit(1) if method == 'hidapi': - if sys.platform == 'windows': + if sys.platform.startswith('win'): print("For Windows, please use method 'libusb' or 'auto'.") print("Or help us implementing support for Windows.") sys.exit(1) @@ -672,10 +673,10 @@ class LedNameBadge: print("* Have you installed the Module? Try:") print(" $ pip install pyhidapi") LedNameBadge._print_install_hints() - if sys.platform == 'darwin': + if sys.platform.startswith('darwin'): print("* Have you installed the library itself? Try:") print(" $ brew install hidapi") - elif 'linux' in sys.platform: + elif sys.platform.startswith('linux'): print("* Is the library itself installed? Try (or similar, suitable for your distro):") 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):") @@ -701,7 +702,7 @@ class LedNameBadge: print("* Is a led tag device with vendorID 0x0416 and productID 0x5020 connected?") if endpoint != 'auto': print("* Have you given the right endpoint?") - if 'linux' in sys.platform: + 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("* If it is connected and still do not work, maybe you have to run") @@ -712,7 +713,7 @@ class LedNameBadge: def _print_install_hints(): print(" (You may need to use pip3 or pip2 instead of pip depending on your python version.)") print(" (You may need prepend 'sudo' for system wide module installation.)") - if 'linux' in sys.platform: + if sys.platform.startswith('linux'): print(" (You may also use your package manager, but the exact package might be different.") print(" E.g. 'sudo apt install python3-usb' for pyusb)") From 5c93752fd3dbe7bbc17b2b55a45809536edc91d0 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:01:36 +0200 Subject: [PATCH 06/19] Improved error handling and messages. --- lednamebadge.py | 70 ++++++++++++++++-------- tests/test_lednamebadge_select_method.py | 4 +- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/lednamebadge.py b/lednamebadge.py index 13a4c2c..ae3ee42 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -369,7 +369,7 @@ class SimpleTextAndIcons: except: print("If you like to use images, the module pillow is needed. Try:") print("$ pip install pillow") - LedNameBadge.print_install_message() + LedNameBadge._print_common_install_hints() sys.exit(1) im = Image.open(file) @@ -437,7 +437,7 @@ class WriteLibUsb(WriteMethod): try: import usb.core _module_loaded = True - print("Module pyusb detected") + print("Module usb.core detected") except: pass @@ -642,6 +642,20 @@ class LedNameBadge: elif sys.platform.startswith('win'): method = 'libusb' print("Selected method 'libusb' with Windows") + elif not WriteLibUsb.is_ready() and not WriteUsbHidApi.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() + sys.exit(1) + elif sys.platform.startswith('darwin'): + print("You need the pyhidapi module.") + LedNameBadge._print_hidapi_install_hints() + sys.exit(1) + else: + print("You need the pyhidapi or usb.core module.") + LedNameBadge._print_libusb_install_hints() + LedNameBadge._print_hidapi_install_hints() + sys.exit(1) if method == 'libusb': if sys.platform.startswith('darwin'): @@ -649,15 +663,7 @@ class LedNameBadge: print("Or help us implementing support for MacOs.") sys.exit(1) elif not WriteLibUsb.is_ready(): - print("The method 'libusb' is not possible to be used: The module could not be loaded.") - print("* Have you installed the Module? Try:") - print(" $ pip install pyusb") - LedNameBadge._print_install_hints() - 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(" $ sudo apt-get install libusb-1.0-0") + LedNameBadge._print_libusb_install_hints() sys.exit(1) if method == 'hidapi': @@ -669,18 +675,7 @@ class LedNameBadge: print("Please use method 'libusb' or 'auto' with python-2.x because of https://github.com/jnweiger/led-badge-ls32/issues/9") sys.exit(1) elif not WriteUsbHidApi.is_ready(): - print("The method 'hidapi' is not possible to be used: The module could not be loaded.") - print("* Have you installed the Module? Try:") - print(" $ pip install pyhidapi") - LedNameBadge._print_install_hints() - 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(" $ 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(" $ sudo ln -s /usr/lib/x86_64-linux-gnu/libhidapi-hidraw.so.0 /usr/local/lib/") + LedNameBadge._print_hidapi_install_hints() sys.exit(1) if (method == 'auto' or method == 'hidapi') and WriteUsbHidApi.is_ready(): @@ -710,7 +705,34 @@ class LedNameBadge: sys.exit(1) @staticmethod - def _print_install_hints(): + 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:") + print(" $ pip install pyusb") + LedNameBadge._print_common_install_hints() + 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(" $ 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:") + print(" $ pip install pyhidapi") + LedNameBadge._print_common_install_hints() + 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(" $ 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(" $ sudo ln -s /usr/lib/x86_64-linux-gnu/libhidapi-hidraw.so.0 /usr/local/lib/") + + @staticmethod + def _print_common_install_hints(): print(" (You may need to use pip3 or pip2 instead of pip depending on your python version.)") print(" (You may need prepend 'sudo' for system wide module installation.)") if sys.platform.startswith('linux'): diff --git a/tests/test_lednamebadge_select_method.py b/tests/test_lednamebadge_select_method.py index a9a67f3..ff5b3f0 100644 --- a/tests/test_lednamebadge_select_method.py +++ b/tests/test_lednamebadge_select_method.py @@ -85,7 +85,7 @@ class Test(TestCase): def test_all_out_linux_negative(self): method, output = self.call_it(False, False, False, None, None) self.assertNotIn('device initialized', output) - self.assertIn('device is not available', output) + self.assertIn('You need', output) self.assertIsNone(method) method, output = self.call_it(False, False, False, 'libusb', None) @@ -135,7 +135,7 @@ class Test(TestCase): pass output = stdio_mock.getvalue() print(output) - self.assertEqual(pyusb_available, 'pyusb detected' in output) + self.assertEqual(pyusb_available, 'usb.core detected' in output) self.assertEqual(pyhidapi_available, 'pyhidapi detected' in output) return method_obj, output From f43e76e612d7487dd7f075e4806fb1f788e16fc2 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Tue, 4 Jun 2024 07:21:37 +0200 Subject: [PATCH 07/19] Whitespace correction --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 330c60c..581b9e3 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,10 @@ in a quite low level way and in a quite old version: - Please use version 1.2.6.0 of 'usblib-win32`. It's still available on the [old project repo on SourceForge](https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/1.2.6.0/) - Then - - Extract `bin/inf-wizard.exe` from the downloaded zip file. Right click and `Run as Administrator` - - `Next` -> Select `0x0416 0x5020 LS32 Custm HID` (or similar with the same IDs) - - `Next` -> `Next` -> Save as dialog `LS32_Sustm_HID.inf` -> `Save` (just to proceed, we don't need that file) - - `Install Now...` -> Driver Install Complete -> `OK` + - Extract `bin/inf-wizard.exe` from the downloaded zip file. Right click and `Run as Administrator` + - `Next` -> Select `0x0416 0x5020 LS32 Custm HID` (or similar with the same IDs) + - `Next` -> `Next` -> Save as dialog `LS32_Sustm_HID.inf` -> `Save` (just to proceed, we don't need that file) + - `Install Now...` -> Driver Install Complete -> `OK` There are other - meanwhile recommended, but untested here - ways to install and setup newer versions of `libus-win32`: use @@ -110,16 +110,16 @@ Of course, Python is needed: - Download latest python from [python.org](https://www.python.org/downloads/), or specific versions from [here](https://www.python.org/downloads/windows/) - - Checkmark the following options - - `[x]` install Launcher for all Users - - `[x]` Add Python X.Y to PATH - - Click the `Install Now ...` text message. + - Checkmark the following options + - `[x]` install Launcher for all Users + - `[x]` Add Python X.Y to PATH + - Click the `Install Now ...` text message. Install needed the Python packages. - Run cmd.exe as Administrator, enter: - pip install pyusb + pip install pyusb pip install pillow #### Examples: From d5d977bbdd6f654f33f9ff9ce6c175643c4cfeb5 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Tue, 4 Jun 2024 07:49:12 +0200 Subject: [PATCH 08/19] Some documentation improvements regarding common installation hints. --- README.md | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 581b9e3..64cde53 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,23 @@ access to the badge via USB. 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. +On some systems (esp. those with Python 2 *and* 3 installed), you have to +address Python 3 explicitly by using the commands `python3` / `pip3` instead +of `python` / `pip`. sudo apt install python3-venv - python3 -m venv ledtag + python -m venv ledtag source ledtag/bin/activate pip install pyhidapi pyusb pillow # this should now work: - # python3 led-badge-11x44.py -m 6 -s 8 "Hello" "World!" + # python led-badge-11x44.py -m 6 -s 8 "Hello" "World!" if the udev rules are installed, you should be able to access the badge without sudo / root privileges. To reuse the venv again at a later point: source ledtag/bin/activate - python3 led-badge-11x44.py … + python led-badge-11x44.py … ### Required dependencies on Fedora Systems @@ -76,16 +79,15 @@ library to the needed place, e.g. ### Required dependencies on Mac Systems +On some systems (esp. those with Python 2 *and* 3 installed), you have to +address Python 3 explicitly by using the command `pip3` instead of `pip`. + sudo easy_install pip pip install pyhidapi pip install pillow ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null brew install hidapi -or Try with - - pip3 install pyhidapi - ### Required dependencies on Windows 7/10 For Windows, we need to set up the libusb API for the LED badge device. @@ -115,7 +117,9 @@ or specific versions from [here](https://www.python.org/downloads/windows/) - `[x]` Add Python X.Y to PATH - Click the `Install Now ...` text message. -Install needed the Python packages. +Install needed the Python packages. On some systems (esp. those with Python 2 +*and* 3 installed), you have to address Python 3 explicitly by using the +command `pip3` instead of `pip`. - Run cmd.exe as Administrator, enter: @@ -125,11 +129,12 @@ Install needed the Python packages. #### Examples: To run these examples on linux, you might have to prepend `sudo` for accessing -the USB device. On Windows, maybe you have to run the `cmd`, where you enter the -commands, with `Run as administrator`, which is similar to the `sudo`on linux. +the USB device or install a udev rule as stated above. On Windows, maybe you +have to run the `cmd`, where you enter the commands, with `Run as administrator`, +which is similar to the `sudo`on linux. -On systems with Python 2 *and* 3 installed, you might want to use Python 3 -explicitely by using `python3` instead of `python`. +On some systems (esp. those with Python 2 *and* 3 installed), you have to +address Python 3 explicitly by using `python3` instead of `python`. python ./led-badge-11x44.py "Hello World!" From 693cbc503fcc98154f767daa6ae35144df39ef67 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Fri, 7 Jun 2024 06:59:18 +0200 Subject: [PATCH 09/19] Explicitly closing libusb-device. On some systems using libusb it was not possible to call the program (write to the device) a second time without detaching the device. Dispose_resources + reset should solve this problem. --- lednamebadge.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lednamebadge.py b/lednamebadge.py index ae3ee42..35b3f3d 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -436,6 +436,7 @@ class WriteLibUsb(WriteMethod): _module_loaded = False try: import usb.core + import usb.util _module_loaded = True print("Module usb.core detected") except: @@ -480,6 +481,9 @@ class WriteLibUsb(WriteMethod): for i in range(int(len(buf) / 64)): time.sleep(0.1) self.dev.write(1, buf[i * 64:i * 64 + 64]) + WriteLibUsb.usb.util.dispose_resources(self.dev) + self.dev.reset() + self.dev = None class WriteUsbHidApi(WriteMethod): @@ -525,6 +529,7 @@ class WriteUsbHidApi(WriteMethod): sendbuf.extend(buf[i*64:i*64+64]) WriteUsbHidApi.pyhidapi.hid_write(self.dev, sendbuf) WriteUsbHidApi.pyhidapi.hid_close(self.dev) + self.dev = None class LedNameBadge: From 90266f4cc8e3347d7529eb3ce4dfb89b8c760f6e Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Fri, 7 Jun 2024 06:59:46 +0200 Subject: [PATCH 10/19] Error message improvement. --- lednamebadge.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lednamebadge.py b/lednamebadge.py index 35b3f3d..37402b6 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -739,11 +739,13 @@ class LedNameBadge: @staticmethod def _print_common_install_hints(): print(" (You may need to use pip3 or pip2 instead of pip depending on your python version.)") - print(" (You may need prepend 'sudo' for system wide module installation.)") + 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 also use your package manager, but the exact package might be different.") + 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)") - + def main(): parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, From 6de3b1dce8bd0672f4958973fb24c1bba3991ea3 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Wed, 12 Jun 2024 06:13:21 +0200 Subject: [PATCH 11/19] Addressing of specific connected device. So, multiple devices can be connected at the same time and one can get a list and choose via command line parameter. --- lednamebadge.py | 225 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 160 insertions(+), 65 deletions(-) diff --git a/lednamebadge.py b/lednamebadge.py index 37402b6..7bfacb1 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -409,6 +409,43 @@ class SimpleTextAndIcons: class WriteMethod: + def __init__(self): + self.devices = None + + def __del__(self): + self.close() + + 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 + + def close(self): + raise NotImplementedError() + + def print_devices(self): + for k, d in self.devices.items(): + print("'%s': %s" % (k, d[0])) + + def _open(self, device_id): + raise NotImplementedError() + + def _get_available_devices(self): + raise NotImplementedError() + @staticmethod def add_padding(buf, blocksize): need_padding = len(buf) % blocksize @@ -421,6 +458,9 @@ class WriteMethod: print("Writing more than %d bytes damages the display!" % (maxsize,)) sys.exit(1) + def is_ready(self): + raise NotImplementedError() + def has_device(self): raise NotImplementedError() @@ -442,15 +482,62 @@ class WriteLibUsb(WriteMethod): except: pass - def __init__(self, endpoint): + def __init__(self): + WriteMethod.__init__(self) + self.description = None + self.dev = None + self.endpoint = None + + def _open(self, device_id): + self.description = self.devices[device_id][0] + self.dev = self.devices[device_id][1] + self.endpoint = self.devices[device_id][2] + print("Libusb device initialized") + 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]) + self.description = None self.dev = None - if WriteLibUsb._module_loaded: - self.dev = WriteLibUsb.usb.core.find(idVendor=0x0416, idProduct=0x5020) - if self.dev: - print("Libusb device initialized") + 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: + try: + # win32: NotImplementedError: is_kernel_driver_active + if d.is_kernel_driver_active(0): + d.detach_kernel_driver(0) + except: + pass + try: + d.set_configuration() + except(WriteLibUsb.usb.core.USBError): + 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.") + sys.exit(1) - @staticmethod - def is_ready(): + cfg = d.get_active_configuration()[0, 0] + eps = WriteLibUsb.usb.util.find_descriptor(cfg, find_all=True, custom_match = lambda e: \ + 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) + devices[id] = (descr, d, ep) + return devices + + def is_ready(self): return WriteLibUsb._module_loaded def has_device(self): @@ -476,14 +563,10 @@ class WriteLibUsb(WriteMethod): print("* Try with sudo or add a udev rule like described in README.md.") sys.exit(1) - print("Write using [%s %s] bus=%d dev=%d via libusb" % - (self.dev.manufacturer, self.dev.product, self.dev.bus, self.dev.address)) + print("Write using %s via libusb" % (self.description)) for i in range(int(len(buf) / 64)): time.sleep(0.1) - self.dev.write(1, buf[i * 64:i * 64 + 64]) - WriteLibUsb.usb.util.dispose_resources(self.dev) - self.dev.reset() - self.dev = None + self.endpoint.write(buf[i * 64:i * 64 + 64]) class WriteUsbHidApi(WriteMethod): @@ -496,40 +579,55 @@ class WriteUsbHidApi(WriteMethod): except: pass - def __init__(self, endpoint): + def __init__(self): + WriteMethod.__init__(self) + self.description = None + self.path = None self.dev = None - self.dev_info = None - if WriteUsbHidApi._module_loaded: - self.dev_info = WriteUsbHidApi.pyhidapi.hid_enumerate(0x0416, 0x5020) - if self.dev_info: - self.dev = WriteUsbHidApi.pyhidapi.hid_open_path(self.dev_info[0].path) - if self.dev: - print("Hidapi device initialized") - # alternative: self.dev = WriteUsbHidApi.pyhidapi.hid_open(0x0416, 0x5020) + def _open(self, device_id): + self.description = self.devices[device_id][0] + self.path = self.devices[device_id][1] + self.dev = WriteUsbHidApi.pyhidapi.hid_open_path(self.path) + if self.dev: + print("Hidapi device initialized") - @staticmethod - def is_ready(): + return self.dev is not None + + def close(self): + if self.dev is not None: + WriteUsbHidApi.pyhidapi.hid_close(self.dev) + 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) + devices[id] = (descr, d.path) + return devices + + def is_ready(self): return WriteUsbHidApi._module_loaded def has_device(self): return self.dev is not None def _write(self, buf): - if not self.dev or not self.dev_info: + if not self.dev: return - print("Write using [%s %s] int=%d page=%s via hidapi" % ( - self.dev_info[0].manufacturer_string, self.dev_info[0].product_string, - self.dev_info[0].interface_number, self.dev_info[0].usage_page)) + print("Write using [%s] via hidapi" % (self.description,)) for i in range(int(len(buf)/64)): # sendbuf must contain "report ID" as first byte. "0" does the job here. sendbuf = array('B', [0]) # Then, put the 64 payload bytes into the buffer sendbuf.extend(buf[i*64:i*64+64]) WriteUsbHidApi.pyhidapi.hid_write(self.dev, sendbuf) - WriteUsbHidApi.pyhidapi.hid_close(self.dev) - self.dev = None class LedNameBadge: @@ -612,30 +710,27 @@ class LedNameBadge: raise TypeError("Please give a list or tuple with at least one number: " + str(iterable)) @staticmethod - def write(buf, method = 'auto', endpoint = 'auto'): + def write(buf, method = 'auto', device_id = 'auto'): """Write the given buffer to the device. It has to begin with a protocol header as provided by header() and followed by the bitmap data. In short: the bitmap data is organized in bytes with 8 horizontal pixels per byte and 11 resp. 12 bytes per (8 pixels wide) byte-column. Then just put one byte-column after the other and one bitmap after the other. """ - write_method = LedNameBadge._find_write_method(method, endpoint) + write_method = LedNameBadge._find_write_method(method, device_id) if write_method: write_method.write(buf) @staticmethod - def _find_write_method(method, endpoint): - if method is None: - method = 'auto' - - if endpoint is None: - endpoint = 'auto' - + def _find_write_method(method, device_id): if method not in ('libusb', 'hidapi', 'auto'): print("Unknown write method '%s'." % (method,)) print("Available options: 'libusb', 'hidapi' and 'auto' (default)") sys.exit(1) + libusb = WriteLibUsb() + hidapi = WriteUsbHidApi() + # Python2 only with libusb if method == 'auto': if sys.version_info[0] < 3: @@ -647,7 +742,7 @@ class LedNameBadge: elif sys.platform.startswith('win'): method = 'libusb' print("Selected method 'libusb' with Windows") - elif not WriteLibUsb.is_ready() and not WriteUsbHidApi.is_ready(): + 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() @@ -667,7 +762,7 @@ class LedNameBadge: print("For MacOs, please use method 'hidapi' or 'auto'.") print("Or help us implementing support for MacOs.") sys.exit(1) - elif not WriteLibUsb.is_ready(): + elif not libusb.is_ready(): LedNameBadge._print_libusb_install_hints() sys.exit(1) @@ -679,30 +774,30 @@ class LedNameBadge: 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") sys.exit(1) - elif not WriteUsbHidApi.is_ready(): + elif not hidapi.is_ready(): LedNameBadge._print_hidapi_install_hints() sys.exit(1) - if (method == 'auto' or method == 'hidapi') and WriteUsbHidApi.is_ready(): - method_obj = WriteUsbHidApi(endpoint) - if method_obj.has_device(): + if (method == 'auto' or method == 'hidapi'): + method_obj = hidapi + if method_obj.open(device_id): return method_obj - if (method == 'auto' or method == 'libusb') and WriteLibUsb.is_ready(): - method_obj = WriteLibUsb(endpoint) - if method_obj.has_device(): + if (method == 'auto' or method == 'libusb'): + method_obj = libusb + if method_obj.open(device_id): return method_obj - endpoint_str = '' - if endpoint != 'auto': - endpoint = int(endpoint) - endpoint_str = ' on endpoint %d' % (endpoint,) + 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, endpoint_str)) + print("The device is not available with write method '%s'%s." % (method, device_id_str)) print("* Is a led tag device with vendorID 0x0416 and productID 0x5020 connected?") - if endpoint != 'auto': - print("* Have you given the right endpoint?") + 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("* If it is connected and still do not work, maybe you have to run") @@ -753,22 +848,22 @@ def main(): epilog='Example combining image and text:\n sudo %s "I:HEART2:you"' % sys.argv[0]) 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('-E', '--endpoint', default='auto', help="Force using the given device endpoint") - parser.add_argument('-s', '--speed', default='4', help="Scroll speed (Range 1..8). Up to 8 comma-separated values") + 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('-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', - help="Brightness for the display in percent: 25, 50, 75, or 100") + help="Brightness for the display in percent: 25, 50, 75, or 100.") parser.add_argument('-m', '--mode', default='0', help="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.") - parser.add_argument('-b', '--blink', default='0', help="1: blinking, 0: normal. Up to 8 comma-separated values") - parser.add_argument('-a', '--ants', default='0', help="1: animated border, 0: normal. Up to 8 comma-separated values") + parser.add_argument('-b', '--blink', default='0', help="1: blinking, 0: normal. Up to 8 comma-separated values.") + parser.add_argument('-a', '--ants', default='0', help="1: animated border, 0: normal. Up to 8 comma-separated values.") parser.add_argument('-p', '--preload', metavar='FILE', action='append', help=argparse.SUPPRESS) # "Load bitmap images. Use ^A, ^B, ^C, ... in text messages to make them visible. Deprecated, embed within ':' instead") - parser.add_argument('-l', '--list-names', action='version', help="list named icons to be embedded in messages and exit", + parser.add_argument('-l', '--list-names', action='version', help="list named icons to be embedded in messages and exit.", version=':' + ': :'.join(SimpleTextAndIcons._get_named_bitmaps_keys()) + ': :: or e.g. :path/to/some_icon.png:') parser.add_argument('message', metavar='MESSAGE', nargs='+', - help="Up to 8 message texts with embedded builtin icons or loaded images within colons(:) -- See -l for a list of builtins") + help="Up to 8 message texts with embedded builtin icons or loaded images within colons(:) -- See -l for a list of builtins.") parser.add_argument('--mode-help', action='version', help=argparse.SUPPRESS, version=""" -m 5 "Animation" @@ -837,7 +932,7 @@ def main(): else: sys.exit("Parameter values are ambiguous. Please use either -H or -M.") - LedNameBadge.write(buf, method, args.endpoint) + LedNameBadge.write(buf, method, args.device_id) def split_to_ints(list_str): From 94a2559b4a304258a717b083decfb56a77ffe73f Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Sat, 22 Jun 2024 07:28:38 +0200 Subject: [PATCH 12/19] Small refactorings, cleanup, documentation --- README.md | 194 +++++++++++----- lednamebadge.py | 270 ++++++++++++++--------- tests/test_lednamebadge_select_method.py | 184 +++++++++++---- 3 files changed, 453 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index 814826b..3705a37 100644 --- a/README.md +++ b/README.md @@ -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 +-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"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 diff --git a/lednamebadge.py b/lednamebadge.py index 7bfacb1..b5cf567 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -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) diff --git a/tests/test_lednamebadge_select_method.py b/tests/test_lednamebadge_select_method.py index ff5b3f0..0acd963 100644 --- a/tests/test_lednamebadge_select_method.py +++ b/tests/test_lednamebadge_select_method.py @@ -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 From 9b517d122993a043415333c7d3615b946dc78931 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:38:13 +0200 Subject: [PATCH 13/19] Docstrings added --- README.md | 4 +- lednamebadge.py | 137 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 117 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index bad6558..9b1df10 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,8 @@ in a quite low level way and in a quite old version: - Please use version 1.2.6.0 of 'usblib-win32`. It's still available on the [old project repo on SourceForge](https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/1.2.6.0/) - Then - - Extract `bin/inf-wizard.exe` from the downloaded zip file. Right click and `Run as Administrator` + - Extract the downloaded zip file and go to the directory `libusb-win32-bin-1.2.6.0\bin` + - Right click on `inf-wizard.exe` and `Run as Administrator` - `Next` -> Select `0x0416 0x5020 LS32 Custm HID` (or similar with the same IDs) - `Next` -> `Next` -> Save as dialog `LS32_Sustm_HID.inf` -> `Save` (just to proceed, we don't need that file) - `Install Now...` -> Driver Install Complete -> `OK` @@ -117,6 +118,7 @@ or specific versions from [here](https://www.python.org/downloads/windows/) - `[x]` install Launcher for all Users - `[x]` Add Python X.Y to PATH - Click the `Install Now ...` text message. + - Optionally click on the 'Disable path length limit' text message. This is always a good thing to do. Install needed the Python packages. On some systems (esp. those with Python 2 *and* 3 installed), you have to address Python 3 explicitly by using the diff --git a/lednamebadge.py b/lednamebadge.py index b5cf567..a5fc36d 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -16,22 +16,43 @@ # # Windows install: # ---------------- -## https://sourceforge.net/projects/libusb-win32/ -> -## -> https://kent.dl.sourceforge.net/project/libusb-win32/libusb-win32-releases/1.2.6.0/libusb-win32-bin-1.2.6.0.zip -## cd libusb-win32-bin-1.2.6.0\bin -## download inf-wizard.exe to your desktop. Right click 'Run as Administrator' -# -> Click 0x0416 0x5020 LS32 Custm HID -# -> Next -> Next -> Dokumente LS32_Sustm_HID.inf -> Save -# -> Install Now... -> Driver Install Complete -> OK -# download python from python.org -# [x] install Launcher for all Users -# [x] Add Python 3.7 to PATH -# -> Click the 'Install Now ...' text message. -# -> Optionally click on the 'Disable path length limit' text message. This is always a good thing to do. -# run cmd.exe as Administrator, enter: -# pip install pyusb -# pip install pillow +# For Windows, we need to set up the libusb API for the LED badge device. +# The way described here, uses [libusb-win32](https://github.com/mcuee/libusb-win32/wiki) +# in a quite low level way and in a quite old version: # +# - Please use version 1.2.6.0 of 'usblib-win32`. It's still available on the +# [old project repo on SourceForge](https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/1.2.6.0/) +# - Then +# - Extract the downloaded zip file and go to the directory `libusb-win32-bin-1.2.6.0\bin` +# - Right click on `inf-wizard.exe` and `Run as Administrator` +# - `Next` -> Select `0x0416 0x5020 LS32 Custm HID` (or similar with the same IDs) +# - `Next` -> `Next` -> Save as dialog `LS32_Sustm_HID.inf` -> `Save` (just to proceed, we don't need that file) +# - `Install Now...` -> Driver Install Complete -> `OK` +# +# There are other - meanwhile recommended, but untested here - ways to install and setup +# newer versions of `libus-win32`: use +# [Zadig](https://zadig.akeo.ie/) (it is also available from the old libusb-win32 repo on +# [GitHub repo](https://github.com/mcuee/libusb-win32/releases) of newer releases) +# or [libusbK](https://libusbk.sourceforge.net/UsbK3/index.html) +# +# Of course, Python is needed: +# +# - Download latest python from [python.org](https://www.python.org/downloads/), +# or specific versions from [here](https://www.python.org/downloads/windows/) +# - Checkmark the following options +# - `[x]` install Launcher for all Users +# - `[x]` Add Python X.Y to PATH +# - Click the `Install Now ...` text message. +# - Optionally click on the 'Disable path length limit' text message. This is always a good thing to do. +# +# Install needed the Python packages. On some systems (esp. those with Python 2 +# *and* 3 installed), you have to address Python 3 explicitly by using the +# command `pip3` instead of `pip`. +# +# - Run cmd.exe as Administrator, enter: +# +# pip install pyusb +# pip install pillow # # v0.1, 2019-03-05, jw initial draught. HID code is much simpler than expected. @@ -61,8 +82,9 @@ # somehow, but it is acceptable, as we do not need to save every processor cycle, here :) # * Have fun! # 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) +# * Preparation for further or updated write methods, like bluetooth. +# * Automatic or manual write method and device selection, See -M and -D (substituting -H) resp. +# get_available_methods() and get_available_device_ids(). import argparse @@ -411,19 +433,37 @@ class SimpleTextAndIcons: class WriteMethod: + """Base class for a write method. That is a way to communicate with a device. Think of using different access + libraries or interfaces for communication. Basically it implements the common parts of the functionalities + 'device detection' and 'write data' and defines te interfaces to the user and the concrete write method class. + """ def __init__(self): + """Call it from your concrete class in your __init__ method with! + """ self.devices = {} def __del__(self): self.close() def get_name(self): + """Returns the name of the write method. + This method is to be implemented in your concrete class. It should just return a short and unique name. + """ raise NotImplementedError() def get_description(self): + """Returns a description of the write method. + This method is to be implemented in your concrete class. It should just return a short description + of how the write method communicates with the device (think of libraries and interfaces). + """ raise NotImplementedError() def open(self, device_id): + """Opens the communication channel to the device, similar to open a file. The device id is one of the ids + returned by get_available_devices() or 'auto', which selects just the first device in that dict. + It is the common part of the opening process. The concrete open is done in _open() and is to be implemented + individually. + """ if self.is_ready() and self.is_device_present(): actual_device_id = None if device_id == 'auto': @@ -437,50 +477,94 @@ class WriteMethod: return False def close(self): + """Close the communication channel to the device, similar to closing a file. + This method is to be implemented in your concrete class. It should close and free all handles and resources. + """ raise NotImplementedError() def get_available_devices(self): + """Get all devices available via the concrete write method. It returns a dict with the device ids as keys + and the device descriptions as values. These device ids are used with 'open()' to specify the wanted device. + It the common part of this process. The concrete part is to be implemented in _get_available_devices() + individually. + """ 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): + """Returns True if there is one or more devices available via the concrete write method, False otherwise. + """ self.get_available_devices() return self.devices and len(self.devices) > 0 def _open(self, device_id): + """The concrete open action. This method is to be implemented in your concrete class. It shall open + the communication channel to the device with the specified id, which is one of the ids returned by + _get_available_devices(). It shall return True, if successful, otherwise False. The special id 'auto' + is handled in open(). So, this method is called only with device ids from the dict returned by + _get_available_devices(). + """ raise NotImplementedError() def _get_available_devices(self): + """The concrete get-the-list action. This method is to be implemented in your concrete class. It shall + Return a dict with one entry per available device. The key of an entry is the device id, like it will be + used in open() / _open(). The value af an entry is a tuple with any data according to the needs of your + write method. The only defined element is the first one, which shall be a description of the individual + device (e.g. manufacturer or bus number / address). E.g. { '1:5': ('Nametag 5 on bus 1', any, data)} + """ raise NotImplementedError() def is_ready(self): + """Returns True, if the concrete write method is basically ready for operation, otherwise False. + This method is to be implemented in your concrete class. Basically, if the import instruction for the + needed Python modules and potentially a library / module initialization was successful, it shall return True. + This method does not make any statement about concrete devices or device availability. + """ raise NotImplementedError() def has_device(self): + """Returns True, if there is at least one device available with the concrete write method, otherwise False. + This method is to be implemented in your concrete class. + """ raise NotImplementedError() def write(self, buf): + """Call this to write data to the opened device. + The concrete write action is to be implemented in _write().""" self.add_padding(buf, 64) self.check_length(buf, 8192) self._write(buf) @staticmethod - def add_padding(buf, blocksize): - need_padding = len(buf) % blocksize + def add_padding(buf, block_size): + """The given data array will be extended with zeros according to the given block size. SO, afterwards the + length of the array is a multiple of block_size. + """ + need_padding = len(buf) % block_size if need_padding: - buf.extend((0,) * (blocksize - need_padding)) + buf.extend((0,) * (block_size - need_padding)) @staticmethod - def check_length(buf, maxsize): - if len(buf) > maxsize: - print("Writing more than %d bytes damages the display! Nothing written." % (maxsize,)) + def check_length(buf, max_size): + """Just checks the length of the given data array and abort the program execution if it exceeds max_size. + """ + if len(buf) > max_size: + print("Writing more than %d bytes damages the display! Nothing written." % (max_size,)) sys.exit(1) def _write(self, buf): + """Write the given data array to the opened device. + This method is to be implemented in your concrete class. It shall write the given data array to the opened + device. + """ raise NotImplementedError() class WriteLibUsb(WriteMethod): + """Write to a device using pyusb and libusb. The device ids consist of the bus number, the device number on that bus + and the endpoint number. + """ _module_loaded = False try: import usb.core @@ -580,6 +664,9 @@ class WriteLibUsb(WriteMethod): class WriteUsbHidApi(WriteMethod): + """Write to a device connected to USB using pyhidapi and libhidapi. The device ids are simply the device paths as + used by libhidapi. + """ _module_loaded = False try: import pyhidapi @@ -752,6 +839,10 @@ class LedNameBadge: @staticmethod def _find_write_method(method, device_id): + """Here we try to concentrate all special cases, decisions and messages around the manual or automatic + selection of write methods and device. This way it is a bit easier to extend or modify the different + working run time environments (think of operating system, python version, installed libraries and python + modules, ands so on.)""" 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] From db32057c8bd6332873dc324453a274692103ddf6 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:59:20 +0200 Subject: [PATCH 14/19] Code quality improvements --- README.md | 6 +-- lednamebadge.py | 120 ++++++++++++++++++++++++++++-------------------- 2 files changed, 73 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 9b1df10..689a19e 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ For Windows, we need to set up the libusb API for the LED badge device. The way described here, uses [libusb-win32](https://github.com/mcuee/libusb-win32/wiki) in a quite low level way and in a quite old version: -- Please use version 1.2.6.0 of 'usblib-win32`. It's still available on the +- Please use version 1.2.6.0 of 'libusb-win32`. It's still available on the [old project repo on SourceForge](https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/1.2.6.0/) - Then - Extract the downloaded zip file and go to the directory `libusb-win32-bin-1.2.6.0\bin` @@ -105,7 +105,7 @@ in a quite low level way and in a quite old version: - `Install Now...` -> Driver Install Complete -> `OK` There are other - meanwhile recommended, but untested here - ways to install and setup -newer versions of `libus-win32`: use +newer versions of `libusb-win32`: use [Zadig](https://zadig.akeo.ie/) (it is also available from the old libusb-win32 repo on [GitHub repo](https://github.com/mcuee/libusb-win32/releases) of newer releases) or [libusbK](https://libusbk.sourceforge.net/UsbK3/index.html) @@ -235,7 +235,7 @@ options: 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 + Force using the given device id, if ambiguous. Use one of 'auto', 'list' or whatever list is printing. -s SPEED, --speed SPEED Scroll speed (Range 1..8). Up to 8 comma-separated diff --git a/lednamebadge.py b/lednamebadge.py index a5fc36d..8f3f339 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -20,7 +20,7 @@ # The way described here, uses [libusb-win32](https://github.com/mcuee/libusb-win32/wiki) # in a quite low level way and in a quite old version: # -# - Please use version 1.2.6.0 of 'usblib-win32`. It's still available on the +# - Please use version 1.2.6.0 of 'libusb-win32`. It's still available on the # [old project repo on SourceForge](https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/1.2.6.0/) # - Then # - Extract the downloaded zip file and go to the directory `libusb-win32-bin-1.2.6.0\bin` @@ -30,7 +30,7 @@ # - `Install Now...` -> Driver Install Complete -> `OK` # # There are other - meanwhile recommended, but untested here - ways to install and setup -# newer versions of `libus-win32`: use +# newer versions of `libusb-win32`: use # [Zadig](https://zadig.akeo.ie/) (it is also available from the old libusb-win32 repo on # [GitHub repo](https://github.com/mcuee/libusb-win32/releases) of newer releases) # or [libusbK](https://libusbk.sourceforge.net/UsbK3/index.html) @@ -268,7 +268,7 @@ class SimpleTextAndIcons: # print(i, charmap[i], char_offsets[charmap[i]]) bitmap_named = { - 'ball': (array('B', ( + 'ball': (array('B', ( 0b00000000, 0b00000000, 0b00111100, @@ -281,7 +281,7 @@ class SimpleTextAndIcons: 0b00111100, 0b00000000 )), 1, '\x1e'), - 'happy': (array('B', ( + 'happy': (array('B', ( 0b00000000, # 0x00 0b00000000, # 0x00 0b00111100, # 0x3c @@ -294,25 +294,25 @@ class SimpleTextAndIcons: 0b00111100, # 0x3c 0b00000000 # 0x00 )), 1, '\x1d'), - 'happy2': (array('B', (0x00, 0x08, 0x14, 0x08, 0x01, 0x00, 0x00, 0x61, 0x30, 0x1c, 0x07, - 0x00, 0x20, 0x50, 0x20, 0x00, 0x80, 0x80, 0x86, 0x0c, 0x38, 0xe0)), 2, '\x1c'), - 'heart': (array('B', (0x00, 0x00, 0x6c, 0x92, 0x82, 0x82, 0x44, 0x28, 0x10, 0x00, 0x00)), 1, '\x1b'), - 'HEART': (array('B', (0x00, 0x00, 0x6c, 0xfe, 0xfe, 0xfe, 0x7c, 0x38, 0x10, 0x00, 0x00)), 1, '\x1a'), - 'heart2': (array('B', (0x00, 0x0c, 0x12, 0x21, 0x20, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01, - 0x00, 0x60, 0x90, 0x08, 0x08, 0x08, 0x10, 0x20, 0x40, 0x80, 0x00)), 2, '\x19'), - 'HEART2': (array('B', (0x00, 0x0c, 0x1e, 0x3f, 0x3f, 0x3f, 0x1f, 0x0f, 0x07, 0x03, 0x01, - 0x00, 0x60, 0xf0, 0xf8, 0xf8, 0xf8, 0xf0, 0xe0, 0xc0, 0x80, 0x00)), 2, '\x18'), - 'fablab': (array('B', (0x07, 0x0e, 0x1b, 0x03, 0x21, 0x2c, 0x2e, 0x26, 0x14, 0x1c, 0x06, - 0x80, 0x60, 0x30, 0x80, 0x88, 0x38, 0xe8, 0xc8, 0x10, 0x30, 0xc0)), 2, '\x17'), - 'bicycle': (array('B', (0x01, 0x02, 0x00, 0x01, 0x07, 0x09, 0x12, 0x12, 0x10, 0x08, 0x07, - 0x00, 0x87, 0x81, 0x5f, 0x22, 0x94, 0x49, 0x5f, 0x49, 0x80, 0x00, - 0x00, 0x80, 0x00, 0x80, 0x70, 0xc8, 0x24, 0xe4, 0x04, 0x88, 0x70)), 3, '\x16'), + 'happy2': (array('B', (0x00, 0x08, 0x14, 0x08, 0x01, 0x00, 0x00, 0x61, 0x30, 0x1c, 0x07, + 0x00, 0x20, 0x50, 0x20, 0x00, 0x80, 0x80, 0x86, 0x0c, 0x38, 0xe0)), 2, '\x1c'), + 'heart': (array('B', (0x00, 0x00, 0x6c, 0x92, 0x82, 0x82, 0x44, 0x28, 0x10, 0x00, 0x00)), 1, '\x1b'), + 'HEART': (array('B', (0x00, 0x00, 0x6c, 0xfe, 0xfe, 0xfe, 0x7c, 0x38, 0x10, 0x00, 0x00)), 1, '\x1a'), + 'heart2': (array('B', (0x00, 0x0c, 0x12, 0x21, 0x20, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01, + 0x00, 0x60, 0x90, 0x08, 0x08, 0x08, 0x10, 0x20, 0x40, 0x80, 0x00)), 2, '\x19'), + 'HEART2': (array('B', (0x00, 0x0c, 0x1e, 0x3f, 0x3f, 0x3f, 0x1f, 0x0f, 0x07, 0x03, 0x01, + 0x00, 0x60, 0xf0, 0xf8, 0xf8, 0xf8, 0xf0, 0xe0, 0xc0, 0x80, 0x00)), 2, '\x18'), + 'fablab': (array('B', (0x07, 0x0e, 0x1b, 0x03, 0x21, 0x2c, 0x2e, 0x26, 0x14, 0x1c, 0x06, + 0x80, 0x60, 0x30, 0x80, 0x88, 0x38, 0xe8, 0xc8, 0x10, 0x30, 0xc0)), 2, '\x17'), + 'bicycle': (array('B', (0x01, 0x02, 0x00, 0x01, 0x07, 0x09, 0x12, 0x12, 0x10, 0x08, 0x07, + 0x00, 0x87, 0x81, 0x5f, 0x22, 0x94, 0x49, 0x5f, 0x49, 0x80, 0x00, + 0x00, 0x80, 0x00, 0x80, 0x70, 0xc8, 0x24, 0xe4, 0x04, 0x88, 0x70)), 3, '\x16'), 'bicycle_r': (array('B', (0x00, 0x00, 0x00, 0x00, 0x07, 0x09, 0x12, 0x13, 0x10, 0x08, 0x07, 0x00, 0xf0, 0x40, 0xfd, 0x22, 0x94, 0x49, 0xfd, 0x49, 0x80, 0x00, 0x40, 0xa0, 0x80, 0x40, 0x70, 0xc8, 0x24, 0x24, 0x04, 0x88, 0x70)), 3, '\x15'), - 'owncloud': (array('B', (0x00, 0x01, 0x02, 0x03, 0x06, 0x0c, 0x1a, 0x13, 0x11, 0x19, 0x0f, - 0x78, 0xcc, 0x87, 0xfc, 0x42, 0x81, 0x81, 0x81, 0x81, 0x43, 0xbd, - 0x00, 0x00, 0x00, 0x80, 0x80, 0xe0, 0x30, 0x10, 0x28, 0x28, 0xd0)), 3, '\x14'), + 'owncloud': (array('B', (0x00, 0x01, 0x02, 0x03, 0x06, 0x0c, 0x1a, 0x13, 0x11, 0x19, 0x0f, + 0x78, 0xcc, 0x87, 0xfc, 0x42, 0x81, 0x81, 0x81, 0x81, 0x43, 0xbd, + 0x00, 0x00, 0x00, 0x80, 0x80, 0xe0, 0x30, 0x10, 0x28, 0x28, 0xd0)), 3, '\x14'), } bitmap_builtin = {} @@ -349,7 +349,7 @@ class SimpleTextAndIcons: return self.bitmap_preloaded[ord(ch)] o = SimpleTextAndIcons.char_offsets[ch] - return (SimpleTextAndIcons.font_11x44[o:o + 11], 1) + return SimpleTextAndIcons.font_11x44[o:o + 11], 1 def bitmap_text(self, text): """Returns a tuple of (buffer, length_in_byte_columns_aka_chars) @@ -379,7 +379,7 @@ class SimpleTextAndIcons: (b, n) = self.bitmap_char(c) buf.extend(b) cols += n - return (buf, cols) + return buf, cols @staticmethod def bitmap_img(file): @@ -421,7 +421,7 @@ class SimpleTextAndIcons: byte_val += bit_val buf.append(byte_val) im.close() - return (buf, cols) + return buf, cols def bitmap(self, arg): """If arg is a valid and existing path name, we load it as an image. @@ -437,6 +437,7 @@ class WriteMethod: libraries or interfaces for communication. Basically it implements the common parts of the functionalities 'device detection' and 'write data' and defines te interfaces to the user and the concrete write method class. """ + def __init__(self): """Call it from your concrete class in your __init__ method with! """ @@ -490,7 +491,7 @@ class WriteMethod: """ 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()} + return {did: data[0] for did, data in self.devices.items()} def is_device_present(self): """Returns True if there is one or more devices available via the concrete write method, False otherwise. @@ -561,6 +562,7 @@ class WriteMethod: """ raise NotImplementedError() + class WriteLibUsb(WriteMethod): """Write to a device using pyusb and libusb. The device ids consist of the bus number, the device number on that bus and the endpoint number. @@ -613,7 +615,7 @@ class WriteLibUsb(WriteMethod): pass try: 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("Maybe, you have to run this program with administrator rights.") @@ -622,12 +624,15 @@ class WriteLibUsb(WriteMethod): sys.exit(1) cfg = d.get_active_configuration()[0, 0] - eps = WriteLibUsb.usb.util.find_descriptor(cfg, find_all=True, custom_match = lambda e: \ - WriteLibUsb.usb.util.endpoint_direction(e.bEndpointAddress) == WriteLibUsb.usb.util.ENDPOINT_OUT) + eps = WriteLibUsb.usb.util.find_descriptor( + cfg, + find_all=True, + custom_match=lambda e: 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) - devices[id] = (descr, d, ep) + did = "%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)) + devices[did] = (descr, d, ep) return devices def is_ready(self): @@ -649,7 +654,7 @@ class WriteLibUsb(WriteMethod): try: 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("Maybe, you have to run this program with administrator rights.") @@ -657,7 +662,7 @@ class WriteLibUsb(WriteMethod): print("* Try with sudo or add a udev rule like described in README.md.") sys.exit(1) - print("Write using %s via libusb" % (self.description)) + print("Write using %s via libusb" % (self.description,)) for i in range(int(len(buf) / 64)): time.sleep(0.1) self.endpoint.write(buf[i * 64:i * 64 + 64]) @@ -708,9 +713,9 @@ class WriteUsbHidApi(WriteMethod): device_infos = WriteUsbHidApi.pyhidapi.hid_enumerate(0x0416, 0x5020) devices = {} for d in device_infos: - id = "%s" % (str(d.path.decode('ascii')),) + did = "%s" % (str(d.path.decode('ascii')),) descr = "%s - %s (if=%d)" % (d.manufacturer_string, d.product_string, d.interface_number) - devices[id] = (descr, d.path) + devices[did] = (descr, d.path) return devices def is_ready(self): @@ -724,11 +729,11 @@ class WriteUsbHidApi(WriteMethod): return print("Write using [%s] via hidapi" % (self.description,)) - for i in range(int(len(buf)/64)): + for i in range(int(len(buf) / 64)): # sendbuf must contain "report ID" as first byte. "0" does the job here. sendbuf = array('B', [0]) # Then, put the 64 payload bytes into the buffer - sendbuf.extend(buf[i*64:i*64+64]) + sendbuf.extend(buf[i * 64:i * 64 + 64]) WriteUsbHidApi.pyhidapi.hid_write(self.dev, sendbuf) @@ -749,7 +754,7 @@ class LedNameBadge: * len(length) should match the designated bitmap data * speeds come in as 1..8, but will be decremented to 0..7, here. * modes: 0..8 - * blinks and ants: 0..1 or even False..True, + * blinks and ants: 0..1 or even False...True, * brightness, if given, is any number, but it'll be limited to 25, 50, 75, 100 (percent), here * date, if given, is a datetime object. It will be written in the header, but is not to be seen on the devices screen. @@ -761,7 +766,6 @@ class LedNameBadge: if lengths_sum > (8192 - len(LedNameBadge._protocol_header_template)) / 11 + 1: raise ValueError("The given lengths seem to be far too high: " + str(lengths)) - ants = LedNameBadge._prepare_iterable(ants, 0, 1) blinks = LedNameBadge._prepare_iterable(blinks, 0, 1) speeds = LedNameBadge._prepare_iterable(speeds, 1, 8) @@ -812,7 +816,7 @@ class LedNameBadge: raise TypeError("Please give a list or tuple with at least one number: " + str(iterable)) @staticmethod - def write(buf, method = 'auto', device_id = 'auto'): + def write(buf, method='auto', device_id='auto'): """Write the given buffer to the device. It has to begin with a protocol header as provided by header() and followed by the bitmap data. In short: the bitmap data is organized in bytes with 8 horizontal pixels per byte and 11 resp. 12 @@ -947,14 +951,14 @@ class LedNameBadge: 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) + for did, descr in sorted(method_obj.get_available_devices().items()): + LedNameBadge.print_one_device(did, 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)) + def print_one_device(did, descr): + print(" '%s': %s" % (did, descr)) @staticmethod def _print_libusb_install_hints(name): @@ -985,7 +989,8 @@ class LedNameBadge: 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 the following") - print(" (or similar, suitable for your distro; the exact command, library name and paths might be different):") + 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 @@ -997,7 +1002,7 @@ class LedNameBadge: print(" (You may need prepend 'sudo' for system wide module installation.)") 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)) + print(" $ sudo apt install %s" % (pm_package,)) def main(): @@ -1006,19 +1011,34 @@ def main(): epilog='Example combining image and text:\n sudo %s "I:HEART2:you"' % sys.argv[0]) 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. 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('-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. 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. Use 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', help="Brightness for the display in percent: 25, 50, 75, or 100.") parser.add_argument('-m', '--mode', default='0', help="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.") parser.add_argument('-b', '--blink', default='0', help="1: blinking, 0: normal. Up to 8 comma-separated values.") - parser.add_argument('-a', '--ants', default='0', help="1: animated border, 0: normal. Up to 8 comma-separated values.") + parser.add_argument('-a', + '--ants', + default='0', + help="1: animated border, 0: normal. Up to 8 comma-separated values.") parser.add_argument('-p', '--preload', metavar='FILE', action='append', help=argparse.SUPPRESS) # "Load bitmap images. Use ^A, ^B, ^C, ... in text messages to make them visible. Deprecated, embed within ':' instead") - parser.add_argument('-l', '--list-names', action='version', help="list named icons to be embedded in messages and exit.", + parser.add_argument('-l', + '--list-names', + action='version', + help="list named icons to be embedded in messages and exit.", version=':' + ': :'.join(SimpleTextAndIcons._get_named_bitmaps_keys()) + ': :: or e.g. :path/to/some_icon.png:') parser.add_argument('message', metavar='MESSAGE', nargs='+', help="Up to 8 message texts with embedded builtin icons or loaded images within colons(:) -- See -l for a list of builtins.") From 03e57fc57eecc9ad33c5cd5834b37aea9c4c18c3 Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:23:53 +0200 Subject: [PATCH 15/19] Some more docstrings --- lednamebadge.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/lednamebadge.py b/lednamebadge.py index 8f3f339..bf8e134 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -817,11 +817,15 @@ class LedNameBadge: @staticmethod def write(buf, method='auto', device_id='auto'): - """Write the given buffer to the device. + """Write the given buffer to the given device. It has to begin with a protocol header as provided by header() and followed by the bitmap data. In short: the bitmap data is organized in bytes with 8 horizontal pixels per byte and 11 resp. 12 bytes per (8 pixels wide) byte-column. Then just put one byte-column after the other and one bitmap after the other. + The two optional parameters specify the write method and device, which shall be programmed. See + get_available_methods() and get_available_device_ids(). There are two special values each: 'list' + will print the implemented / available write methods resp. the available devices, 'auto' (default) will + choose an appropriate write method resp. the first device found. """ write_method = LedNameBadge._find_write_method(method, device_id) if write_method: @@ -830,12 +834,21 @@ class LedNameBadge: @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} + """ + Returns the implemented / available write methods as a dict. Each entry has the method name as the key and a + tuple as the value with the method description and a boolean indicating the readiness of that write method. + Basically it is ready if all necessary libraries and Python modules could be loaded. The method name can be + used as a parameter value for write(). + """ + auto_order_methods = LedNameBadge._get_auto_order_method_list() + return {m.get_name(): (m.get_description(), 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() + """Returns all devices available via the given write method as a dict. Each entry has the device id as the key + and the device description as the value. The device id can be used as a parameter value for write(). + """ + 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() @@ -847,7 +860,7 @@ class LedNameBadge: selection of write methods and device. This way it is a bit easier to extend or modify the different working run time environments (think of operating system, python version, installed libraries and python modules, ands so on.)""" - auto_order_methods = LedNameBadge.get_auto_order_method_list() + 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] @@ -933,7 +946,7 @@ class LedNameBadge: sys.exit(1) @staticmethod - def get_auto_order_method_list(): + def _get_auto_order_method_list(): return [WriteUsbHidApi(), WriteLibUsb()] @staticmethod @@ -952,12 +965,12 @@ class LedNameBadge: if method_obj.is_device_present(): print("Known device ids with method '%s' are:" % (method_obj.get_name(),)) for did, descr in sorted(method_obj.get_available_devices().items()): - LedNameBadge.print_one_device(did, descr) + LedNameBadge._print_one_device(did, descr) else: print("No devices with method '%s' found." % (method_obj.get_name(),)) @staticmethod - def print_one_device(did, descr): + def _print_one_device(did, descr): print(" '%s': %s" % (did, descr)) @staticmethod From e3a0d805e6e4e54835e0c6587b6a4695b855c0bf Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:25:02 +0200 Subject: [PATCH 16/19] Some more unittests, small restructuring for tests with mocked python modules --- tests/abstract_witre_method_test.py | 85 +++++++++++++++++ tests/test_lednamebadge_api.py | 57 ++++++++++++ tests/test_lednamebadge_select_method.py | 112 +---------------------- 3 files changed, 146 insertions(+), 108 deletions(-) create mode 100644 tests/abstract_witre_method_test.py create mode 100644 tests/test_lednamebadge_api.py diff --git a/tests/abstract_witre_method_test.py b/tests/abstract_witre_method_test.py new file mode 100644 index 0000000..9504e33 --- /dev/null +++ b/tests/abstract_witre_method_test.py @@ -0,0 +1,85 @@ +import sys +from unittest import TestCase +from unittest.mock import patch, MagicMock +from io import StringIO + + +class USBError(BaseException): + pass + + +class AbstractWriteMethodTest(TestCase): + def setUp(self): + print("Real platform: " + sys.platform) + + + # ------------------------------------------------------------------------- + + + 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 prepare_modules(self, pyusb_available, pyhidapi_available, device_available, func): + result = None + output = None + mocks = None + with self.do_import_patch(pyusb_available, pyhidapi_available, device_available) as module_mocks: + with patch('sys.stdout', new_callable=StringIO) as stdio_mock: + import lednamebadge + try: + result = func(lednamebadge.LedNameBadge) + mocks = {'pyhidapi': module_mocks['pyhidapi'], 'usb': module_mocks['usb']} + except(SystemExit): + pass + output = stdio_mock.getvalue() + print(output) + return result, output, mocks + + def do_import_patch(self, pyusb_available, pyhidapi_available, device_available): + 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 = 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 [] + 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 [] + mock.core.USBError = USBError + mock.util.find_descriptor.return_value = [ep] if device_available else [] + return mock diff --git a/tests/test_lednamebadge_api.py b/tests/test_lednamebadge_api.py new file mode 100644 index 0000000..271eed3 --- /dev/null +++ b/tests/test_lednamebadge_api.py @@ -0,0 +1,57 @@ +import sys +from array import array + +import abstract_witre_method_test + + +class Test(abstract_witre_method_test.AbstractWriteMethodTest): + def test_get_methods(self): + methods, output = self.call_info_methods() + self.assertDictEqual({ + 'hidapi': ('Program a device connected via USB using the pyhidapi package and libhidapi.', True), + 'libusb': ('Program a device connected via USB using the pyusb package and 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 test_write(self): + device_ids, output, mocks = self.call_write('auto') + mocks['pyhidapi'].hid_write.assert_called_once() + + device_ids, output, mocks = self.call_write('hidapi') + mocks['pyhidapi'].hid_write.assert_called_once() + + device_ids, output, mocks = self.call_write('libusb') + mocks['usb'].util.find_descriptor.assert_called_once() + mocks['usb'].util.find_descriptor.return_value[0].write.assert_called_once() + + + # ------------------------------------------------------------------------- + + + def call_info_methods(self): + self.print_test_conditions(True, True, True, '-', '-') + method_obj, output, _ = self.prepare_modules(True, True, True, + lambda m: m.get_available_methods()) + return method_obj, output + + def call_info_ids(self, method): + self.print_test_conditions(True, True, True, '-', '-') + method_obj, output, _ = self.prepare_modules(True, True, True, + lambda m: m.get_available_device_ids(method)) + return method_obj, output + + def call_write(self, method): + self.print_test_conditions(True, True, True, 'auto', 'auto') + return self.prepare_modules(True, True, True, + lambda m: m.write(array('B', [1, 2, 3]), method)) diff --git a/tests/test_lednamebadge_select_method.py b/tests/test_lednamebadge_select_method.py index 0acd963..c9bfd1f 100644 --- a/tests/test_lednamebadge_select_method.py +++ b/tests/test_lednamebadge_select_method.py @@ -1,17 +1,9 @@ -import sys -from unittest import TestCase -from unittest.mock import patch, MagicMock -from io import StringIO +from unittest.mock import patch +import abstract_witre_method_test -class USBError(BaseException): - pass - - -class Test(TestCase): - def setUp(self): - print("Real platform: " + sys.platform) +class Test(abstract_witre_method_test.AbstractWriteMethodTest): @patch('sys.platform', new='linux') def test_list(self): method, output = self.call_find(True, True, True, 'list', 'auto') @@ -156,107 +148,11 @@ class Test(TestCase): # ------------------------------------------------------------------------- - 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, + 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_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: - result = func(lednamebadge.LedNameBadge) - except(SystemExit): - pass - output = stdio_mock.getvalue() - print(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): - 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 = 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 [] - 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 [] - mock.core.USBError = USBError - mock.util.find_descriptor.return_value = [ep] if device_available else [] - return mock From aba522f665c668155439e647e0e04de67bdbf08f Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Fri, 28 Jun 2024 07:55:24 +0200 Subject: [PATCH 17/19] Minor fixes for usage/troubleshooting hints and Windows --- lednamebadge.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/lednamebadge.py b/lednamebadge.py index bf8e134..93be065 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -596,9 +596,9 @@ class WriteLibUsb(WriteMethod): return True def close(self): - for k, d in self.devices.items(): - d[1].reset() - WriteLibUsb.usb.util.dispose_resources(d[1]) + if self.dev: + self.dev.reset() + WriteLibUsb.usb.util.dispose_resources(self.dev) self.description = None self.dev = None self.endpoint = None @@ -617,10 +617,8 @@ class WriteLibUsb(WriteMethod): 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.") + print("No read access to device list!") + LedNameBadge._print_sudo_hints() sys.exit(1) cfg = d.get_active_configuration()[0, 0] @@ -657,9 +655,7 @@ class WriteLibUsb(WriteMethod): 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.") + LedNameBadge._print_sudo_hints() sys.exit(1) print("Write using %s via libusb" % (self.description,)) @@ -941,8 +937,8 @@ class LedNameBadge: if device_id != 'auto': print("* Have you given the right device_id?") 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.") + print("* If it is connected and still do not work:") + LedNameBadge._print_sudo_hints() sys.exit(1) @staticmethod @@ -1017,6 +1013,16 @@ class LedNameBadge: print(" (or similar, suitable for your distro; the exact command and package name might be different):") print(" $ sudo apt install %s" % (pm_package,)) + @staticmethod + def _print_sudo_hints(): + print("Maybe, you have to run this program with administrator rights.") + if sys.platform.startswith('win'): + print("* Open start menu, type 'cmd', click 'Run as Administrator'") + if sys.platform.startswith('linux'): + print("* If Try with sudo or") + print("* If you run the program from a virtual env, you may need to open a root shell beforehand.") + print("* Best: add a udev rule like described in README.md.") + def main(): parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, From 312715845b7aa24cd11a68e16b7e947da751a8cf Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Fri, 28 Jun 2024 09:55:03 +0200 Subject: [PATCH 18/19] Minor fixes for Windows. To be backwards compatible, using Windows with hidapi is not forbidden. But now, you have to select it explicitly. --- lednamebadge.py | 12 +++++++----- tests/test_lednamebadge_select_method.py | 7 ++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lednamebadge.py b/lednamebadge.py index 93be065..345d069 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -903,11 +903,7 @@ class LedNameBadge: sys.exit(1) if method == hidapi.get_name(): - if sys.platform.startswith('win'): - 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: + if sys.version_info[0] < 3: 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) @@ -915,6 +911,12 @@ class LedNameBadge: LedNameBadge._print_hidapi_install_hints(hidapi.get_name()) sys.exit(1) + if sys.platform.startswith('win') and hidapi.is_ready(): + print("Method '%s' is not tested under Windows. If not working, please use '%s' or 'auto'" % ( + hidapi.get_name(), libusb.get_name())) + print("Or help us implementing support for Windows.") + # But it is not forbidden + first_method_found = None for m in auto_order_methods: if method == 'auto' or method == m.get_name(): diff --git a/tests/test_lednamebadge_select_method.py b/tests/test_lednamebadge_select_method.py index c9bfd1f..68cfce0 100644 --- a/tests/test_lednamebadge_select_method.py +++ b/tests/test_lednamebadge_select_method.py @@ -127,7 +127,12 @@ class Test(abstract_witre_method_test.AbstractWriteMethodTest): def test_windows_negative(self): method, output = self.call_find(True, False, True, 'hidapi', 'auto') self.assertNotIn('device initialized', output) - self.assertIn('please use method', output) + self.assertIn('is not possible to be used', output) + self.assertIsNone(method) + + method, output = self.call_find(True, True, False, 'hidapi', 'auto') + self.assertNotIn('device initialized', output) + self.assertIn('If not working, please use', output) self.assertIsNone(method) @patch('sys.platform', new='darwin') From 3a110872964daaedf54b4afffa504e509e30f23d Mon Sep 17 00:00:00 2001 From: Ben Sartori <149951068+bensartori@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:13:49 +0200 Subject: [PATCH 19/19] Typo fixed, web link update. --- README.md | 2 +- ...act_witre_method_test.py => abstract_write_method_test.py} | 0 tests/test_lednamebadge_api.py | 4 ++-- tests/test_lednamebadge_select_method.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename tests/{abstract_witre_method_test.py => abstract_write_method_test.py} (100%) diff --git a/README.md b/README.md index 689a19e..bf132c3 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,7 @@ 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/ +* http://zunkworks.com/projects/programmablelednamebadges/ (Offline since 2019. As of 07-2024, it is still available on https://web.archive.org) * https://github.com/DirkReiners/LEDBadgeProgrammer * https://bitbucket.org/bartj/led/src * http://www.daveakerman.com/?p=1440 diff --git a/tests/abstract_witre_method_test.py b/tests/abstract_write_method_test.py similarity index 100% rename from tests/abstract_witre_method_test.py rename to tests/abstract_write_method_test.py diff --git a/tests/test_lednamebadge_api.py b/tests/test_lednamebadge_api.py index 271eed3..9f7d9c4 100644 --- a/tests/test_lednamebadge_api.py +++ b/tests/test_lednamebadge_api.py @@ -1,10 +1,10 @@ import sys from array import array -import abstract_witre_method_test +import abstract_write_method_test -class Test(abstract_witre_method_test.AbstractWriteMethodTest): +class Test(abstract_write_method_test.AbstractWriteMethodTest): def test_get_methods(self): methods, output = self.call_info_methods() self.assertDictEqual({ diff --git a/tests/test_lednamebadge_select_method.py b/tests/test_lednamebadge_select_method.py index 68cfce0..98de979 100644 --- a/tests/test_lednamebadge_select_method.py +++ b/tests/test_lednamebadge_select_method.py @@ -1,9 +1,9 @@ from unittest.mock import patch -import abstract_witre_method_test +import abstract_write_method_test -class Test(abstract_witre_method_test.AbstractWriteMethodTest): +class Test(abstract_write_method_test.AbstractWriteMethodTest): @patch('sys.platform', new='linux') def test_list(self): method, output = self.call_find(True, True, True, 'list', 'auto')