diff --git a/README.md b/README.md index c8310a3..c5c9c07 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) @@ -48,92 +49,139 @@ 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. +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 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 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/ + ### 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 10 +### Required dependencies on Windows 7/10 + +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: - 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 +- 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` + - 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` - Download python from python.org - [x] install Launcher for all Users - [x] Add Python 3.7 to PATH - -> Click the 'Install Now ...' text message. +There are other - meanwhile recommended, but untested here - ways to install and setup +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) - Run cmd.exe as Administrator, enter: - pip install pyusb - pip install pillow +Of course, Python is needed: -### Required dependencies on Windows 7 +- 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. - 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. +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 #### 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 or install a udev rule as stated above. On Windows, maybe you +have to run the `cmd.exe`, where you enter the commands, with `Run as administrator`, +which is similar to the `sudo`on linux. + +On some systems (esp. those with Python 2 *and* 3 installed), you have to +address Python 3 explicitly by using `python3` instead of `python`. Run +`python -V` to see which version of python is configured as default. - 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. +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. +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` +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). +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. +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. @@ -159,56 +207,110 @@ plays a doom game video. ![LED Mini Board](photos/doom.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: :: +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 + +lists all write methods. Does not write anything to the device. + + python ./led-badge-11x44.py -M list "dummy message" + +lists all devices available with write method 'hidapi'. Does not write anything to the device. + + python ./led-badge-11x44.py -M hidapi -D list "dummy message" + +programs a specific device with a specific write method. + + python ./led-badge-11x44.py -M hidapi -D "3-1:1.0" "Hello World!" prints some condensed help: + python ./led-badge-11x44.py -h +
-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] [-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
+  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. 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. Use
+                        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
-  -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.
+                        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.
   -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
-  -l, --list-names      list named icons to be embedded in messages and exit
+                        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.
 
 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` (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 command line option `-t` + +There are two options to control 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 @@ -217,10 +319,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. @@ -229,13 +331,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: @@ -252,12 +354,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) +2. `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. @@ -275,10 +417,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!") @@ -287,7 +429,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) @@ -307,9 +449,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/ (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 +* https://github.com/stoggi/ledbadge diff --git a/lednamebadge.py b/lednamebadge.py index 0b9aa6d..345d069 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 '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` +# - 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 `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) +# +# 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. @@ -60,6 +81,10 @@ # * 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 extending write methods. +# * 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 @@ -71,7 +96,7 @@ from array import array from datetime import datetime -__version = "0.13" +__version = "0.14" class SimpleTextAndIcons: @@ -185,7 +210,7 @@ class SimpleTextAndIcons: 0x00, 0x00, 0x00, 0x66, 0x3c, 0xff, 0x3c, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - # "äöüÄÖÜߊ" + # "äöüÄÖÜß" 0x00, 0xcc, 0xcc, 0x00, 0x78, 0x0c, 0x7c, 0xcc, 0xcc, 0x76, 0x00, 0x00, 0xc6, 0xc6, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0xcc, 0xcc, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x76, 0x00, @@ -193,7 +218,6 @@ class SimpleTextAndIcons: 0xc6, 0xc6, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0xc6, 0xc6, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, 0x00, 0x3c, 0x66, 0x66, 0x66, 0x7c, 0x66, 0x66, 0x66, 0x6c, 0x60, - 0x28, 0x10, 0x7c, 0xc6, 0xe0, 0x38, 0x0c, 0xc6, 0xc6, 0x7c, 0x00, # "àäòöùüèéêëôöûîïÿç" 0x00, 0x60, 0x18, 0x00, 0x78, 0x0c, 0x7c, 0xcc, 0xcc, 0x76, 0x00, @@ -234,7 +258,7 @@ class SimpleTextAndIcons: u'abcdefghijklmnopqrstuvwxyz' + \ u'0987654321^ !"\0$%&/()=?` °\\}][{' + \ u"@ ~ |<>,;.:-_#'+* " + \ - u"äöüÄÖÜߊ" + \ + u"äöüÄÖÜß" + \ u"àäòöùüèéêëôöûîïÿç" + \ u"ÀÅÄÉÈÊËÖÔÜÛÙŸ" @@ -244,7 +268,7 @@ class SimpleTextAndIcons: # print(i, charmap[i], char_offsets[charmap[i]]) bitmap_named = { - 'ball': (array('B', ( + 'ball': (array('B', ( 0b00000000, 0b00000000, 0b00111100, @@ -257,7 +281,7 @@ class SimpleTextAndIcons: 0b00111100, 0b00000000 )), 1, '\x1e'), - 'happy': (array('B', ( + 'happy': (array('B', ( 0b00000000, # 0x00 0b00000000, # 0x00 0b00111100, # 0x3c @@ -270,32 +294,31 @@ 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 = {} 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 @@ -305,17 +328,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). @@ -329,8 +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) @@ -360,8 +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): @@ -370,7 +388,13 @@ 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") + LedNameBadge._print_common_install_hints('pillow', 'python3-pillow') + sys.exit(1) im = Image.open(file) print("fetching bitmap from file %s -> (%d x %d)" % (file, im.width, im.height)) @@ -397,8 +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. @@ -409,58 +432,314 @@ 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: + """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': + 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): + """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 {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. + """ + 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, 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,) * (block_size - need_padding)) + + @staticmethod + 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: - 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 + import usb.util + _module_loaded = True + print("Module usb.core detected") except: + pass + + def __init__(self): + WriteMethod.__init__(self) + self.description = None + 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] + self.endpoint = self.devices[device_id][2] + print("Libusb device initialized") + return True + + def close(self): + if self.dev: + self.dev.reset() + WriteLibUsb.usb.util.dispose_resources(self.dev) + self.description = None + self.dev = None + self.endpoint = None + + def _get_available_devices(self): + 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: + # TODO: use all the nice output in _find_write_method(), somehow. + print("No read access to device list!") + LedNameBadge._print_sudo_hints() + 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) + for ep in eps: + 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): + return WriteLibUsb._module_loaded + + def has_device(self): + return self.dev is not None + + def _write(self, buf): + if not self.dev: + return + try: - import usb.core - print("Pyusb detected") + # win32: NotImplementedError: is_kernel_driver_active + if self.dev.is_kernel_driver_active(0): + self.dev.detach_kernel_driver(0) 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) + pass + + 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!") + LedNameBadge._print_sudo_hints() sys.exit(1) + 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]) - @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)) +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 + pyhidapi.hid_init() + _module_loaded = True + print("Module pyhidapi detected") + except: + pass + + def __init__(self): + WriteMethod.__init__(self) + self.description = None + 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] + self.dev = WriteUsbHidApi.pyhidapi.hid_open_path(self.path) + if self.dev: + print("Hidapi device initialized") + + 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 + + def _get_available_devices(self): + device_infos = WriteUsbHidApi.pyhidapi.hid_enumerate(0x0416, 0x5020) + devices = {} + for d in device_infos: + did = "%s" % (str(d.path.decode('ascii')),) + descr = "%s - %s (if=%d)" % (d.manufacturer_string, d.product_string, d.interface_number) + devices[did] = (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: + return + + 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) + + +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()): @@ -471,7 +750,7 @@ or * 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. @@ -483,7 +762,6 @@ or 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) @@ -524,69 +802,229 @@ 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): - """Write the given buffer to the device. + def write(buf, method='auto', device_id='auto'): + """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. """ - need_padding = len(buf) % 64 - if need_padding: - buf.extend((0,) * (64 - need_padding)) + write_method = LedNameBadge._find_write_method(method, device_id) + if write_method: + write_method.write(buf) + write_method.close() + + @staticmethod + def get_available_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): + """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() + return [] - if len(buf) > 8192: - print("Writing more than 8192 bytes damages the display!") + @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] + + 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,)) + LedNameBadge._print_available_methods(auto_order_methods) 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 == 'auto': + if sys.version_info[0] < 3: + 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.get_name() + print("Selected method %s with MacOs" % (hidapi.get_name(),)) + elif sys.platform.startswith('win'): + 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'): + LedNameBadge._print_libusb_install_hints(libusb.get_name()) + sys.exit(1) + elif sys.platform.startswith('darwin'): + LedNameBadge._print_hidapi_install_hints(hidapi.get_name()) + sys.exit(1) + else: + 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.get_name(): + if sys.platform.startswith('darwin'): + print("For MacOs, please use method '%s' or 'auto'." % (hidapi.get_name(),)) + 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 libusb.is_ready(): + LedNameBadge._print_libusb_install_hints(libusb.get_name()) 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)) - endpoint = 1 - i = 0 - while i < int(len(buf) / 64): - time.sleep(0.1) - try: - dev.write(endpoint, buf[i * 64:i * 64 + 64]) - i += 1 - except ValueError: - if endpoint == 1: - endpoint = 2 - i = 0 -def split_to_ints(list_str): - return [int(x) for x in re.split(r'[\s,]+', list_str)] + if method == hidapi.get_name(): + 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) + elif not hidapi.is_ready(): + 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(): + 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?") + print(" Find the available device ids with option -D list") + print("* If it is connected and still do not work:") + LedNameBadge._print_sudo_hints() + sys.exit(1) + + @staticmethod + 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 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(did, descr): + print(" '%s': %s" % (did, 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('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 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(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('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 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 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(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. 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,)) + + @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, @@ -594,20 +1032,37 @@ 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('-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. 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") + 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" @@ -668,11 +1123,20 @@ 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: + 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 -M only.") - LedNameBadge.write(buf) + LedNameBadge.write(buf, method, args.device_id) + + +def split_to_ints(list_str): + return [int(x) for x in re.split(r'[\s,]+', list_str)] if __name__ == '__main__': 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 diff --git a/tests/abstract_write_method_test.py b/tests/abstract_write_method_test.py new file mode 100644 index 0000000..9504e33 --- /dev/null +++ b/tests/abstract_write_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..9f7d9c4 --- /dev/null +++ b/tests/test_lednamebadge_api.py @@ -0,0 +1,57 @@ +import sys +from array import array + +import abstract_write_method_test + + +class Test(abstract_write_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 new file mode 100644 index 0000000..98de979 --- /dev/null +++ b/tests/test_lednamebadge_select_method.py @@ -0,0 +1,163 @@ +from unittest.mock import patch + +import abstract_write_method_test + + +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') + 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_find(True, True, True, 'auto', 'auto') + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_find(True, True, True, 'libusb', 'auto') + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + 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_find(False, True, True, 'auto', 'auto') + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_find(False, True, True, 'hidapi', 'auto') + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + method, output = self.call_find(True, False, True, 'auto', 'auto') + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + 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_find(True, False, True, 'auto', 'auto') + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + 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_find(False, True, True, 'auto', 'auto') + self.assertIn('device initialized', output) + self.assertIsNotNone(method) + + 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_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_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_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_find(False, False, False, 'auto', 'auto') + self.assertNotIn('device initialized', output) + self.assertIn('One of the python packages', output) + self.assertIsNone(method) + + 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_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_find(True, False, True, 'hidapi', 'auto') + self.assertNotIn('device initialized', 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') + def test_macos_negative(self): + 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_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 +