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] 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