From 012d4415bdc0aed4b9ff73493afab6ebfd6d7233 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 18 Nov 2023 17:46:49 +0100 Subject: [PATCH] Rearranging code: cleanup, documentation, version number increment. --- README.md | 110 +++++++++++++++++++++++++++- lednamebadge.py | 117 +++++++++++++++++++++--------- photos/bitmap_data_all.png | Bin 0 -> 2620 bytes photos/bitmap_data_descr.puml | 35 +++++++++ photos/bitmap_data_onebyte.png | Bin 0 -> 3286 bytes photos/bitmap_data_onescene.png | Bin 0 -> 5932 bytes tests/run_tests.py | 8 ++ tests/test_lednamebadge_create.py | 1 - tests/test_lednamebadge_write.py | 28 ++++++- 9 files changed, 260 insertions(+), 39 deletions(-) create mode 100644 photos/bitmap_data_all.png create mode 100644 photos/bitmap_data_descr.puml create mode 100644 photos/bitmap_data_onebyte.png create mode 100644 photos/bitmap_data_onescene.png create mode 100644 tests/run_tests.py diff --git a/README.md b/README.md index 815c22d..d68cba7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Led-Badge-44x11 -Upload tool for an led name tag with USB-HID interface +Upload tool for a LED name tag with USB-HID interface ![LED Mini Board](photos/blueBadge.jpg) @@ -25,7 +25,7 @@ In both configurations, the badge identifies itself on the USB as ## Warning There are many different versions of LED Badges on the market. -This one uses an USB-HID interface, while others use USB-Serial (see references below). +This one uses a USB-HID interface, while others use USB-Serial (see references below). ## Command Line Installation and Usage @@ -173,10 +173,116 @@ Example combining image and text: sudo ./led-badge-11x44.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 ### 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. +## 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. + +- create the header +- append your own content +- write to device + +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 + 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. + +Your own content has to be a byte array with the bitmap data for all scenes. Of course, it has to fit the given lengths. + +See the following graphic for better understanding: +![bitmap_data_onebyte.png](photos%2Fbitmap_data_onebyte.png) + +![bitmap_data_onescene.png](photos%2Fbitmap_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) + +Example: + +Let's say, you have 2 scenes, one is 11x32 pixels, one is 11x60 pixels. So, the first have 4 byte-columns and 44 bytes, +the second has to be padded with 4 empty bit-columns in the last byte-column to 11x64 pixels and has therefore +8 byte-columns and 88 bytes. + +We like to display both in mode 4, the first one with speed 3 and the second one with speed 2 and the second one shall +be displayed with ants. And we like to set the initial brightness to 50%. + +This would be achieved by these calls: + +```python +from lednamebadge import LedNameBadge + +buf = array('B') +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) +``` + +### Using the text generation + +You can also use the text/icon/graphic generation of this module to get the corresponding byte buffers. + +This is quite simple and just like with the command line usage. There is the additional option to create a bitmap just +and only from an image file by giving the filename instead of a message. + +```python +from lednamebadge import SimpleTextAndIcons + +creator = SimpleTextAndIcons() +scene_a_bitmap = creator.bitmap("Hello :HEART2: World!") +scene_b_bitmap = creator.bitmap("As you :gfx/bicycle3.png: like...") +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: + +```python +from lednamebadge import * + +creator = SimpleTextAndIcons() +scene_x_bitmap = creator.bitmap("Hello :HEART2: World!") +scene_y_bitmap = creator.bitmap("Complete example ahead.") +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(scene_x_bitmap[0]) +buf.extend(scene_y_bitmap[0]) +buf.extend(your_own_stuff.bytes) +LedNameBadge.write(buf) +``` + +## Development + +### Generating Plantuml graphics + +You will need PlantUML and potentially GraphViz dot to generate the diagrams from the *.puml files. + +Just run `plantuml "*.puml"` from the `photos` directory to regenerate all diagrams. + +### Running the unit tests + +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/ diff --git a/lednamebadge.py b/lednamebadge.py index c176410..dd0dc4a 100755 --- a/lednamebadge.py +++ b/lednamebadge.py @@ -48,6 +48,19 @@ # v0.10, 2019-09-09, jw Support for loading monochrome images. Typos fixed. # v0.11, 2019-09-29, jw New option --brightness added. # v0.12, 2019-12-27, jw hint at pip3 -- as discussed in https://github.com/jnweiger/led-name-badge-ls32/issues/19 +# v0.13, 2023-11-14, bs modularization. +# Some comments about this big change: +# * I wanted to keep this one-python-file-for-complete-command-line-usage, but also needed to introduce importable +# classes for writing own content to the device (my upcoming GUI editor). Therefore, the file was renamed to an +# importable python file, and forwarding python files are introduced with the old file names for full +# compatibility. +# * A bit of code rearranging and cleanup was necessary for that goal, but I hope the original parts are still +# recognizable, as I tried to keep all this refactoring as less, as possible and sense-making, but did not do +# the full clean-codish refactoring. Keeping the classes in one file is part of that refactoring-omittance. +# * 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! + import argparse import os @@ -58,7 +71,7 @@ from array import array from datetime import datetime -__version = "0.12" +__version = "0.13" class SimpleTextAndIcons: @@ -225,7 +238,6 @@ class SimpleTextAndIcons: u"ÀÅÄÉÈÊËÖÔÜÛÙŸ" char_offsets = {} - pass for i in range(len(charmap)): char_offsets[charmap[i]] = 11 * i # print(i, charmap[i], char_offset[charmap[i]]) @@ -288,23 +300,25 @@ class SimpleTextAndIcons: self.bitmaps_preloaded_unused = False def add_preload_img(self, filename): + """Still used by main, but deprecated. PLease use ":"-notation for bitmap() / bitmap_text()""" 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 @staticmethod - def get_named_bitmaps_keys(): + def _get_named_bitmaps_keys(): return SimpleTextAndIcons.bitmap_named.keys() def bitmap_char(self, ch): - """ Returns a tuple of 11 bytes, - ch = '_' returns (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255) - The bits in each byte are horizontal, highest bit is left. + """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). + The bits in each byte are horizontal, highest bit is left. """ if ord(ch) < 32: if ch in SimpleTextAndIcons.bitmap_builtin: @@ -318,7 +332,7 @@ class SimpleTextAndIcons: def bitmap_text(self, text): - """ returns a tuple of (buffer, length_in_byte_columns_aka_chars) + """Returns a tuple of (buffer, length_in_byte_columns_aka_chars) We preprocess the text string for substitution patterns "::" is replaced with a single ":" ":1: is replaced with CTRL-A referencing the first preloaded or loaded image. @@ -350,7 +364,10 @@ class SimpleTextAndIcons: @staticmethod def bitmap_img(file): - """ returns a tuple of (buffer, length_in_byte_columns) + """Returns a tuple of (buffer, length_in_byte_columns) representing the given image file. + It has to be an 8-bit grayscale image or a color image with 8 bit per channel. Color pixels are converted to + 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 @@ -383,8 +400,8 @@ class SimpleTextAndIcons: 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. + """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()). """ if os.path.exists(arg): return SimpleTextAndIcons.bitmap_img(arg) @@ -408,7 +425,7 @@ class LedNameBadge: pyhidapi.hid_init() _have_pyhidapi = True print("Pyhidapi detected") - except Exception: + except: try: import usb.core print("Pyusb detected") @@ -429,27 +446,47 @@ class LedNameBadge: or sudo apt-get install python3-usb """) - else: # windows? + else: # windows? print("""Please with Linux or MacOS or help us implement support for """ + sys.platform) sys.exit(1) @staticmethod - def _expand_tuple(l): - l = l + (l[-1],) * (8 - len(l)) # repeat last element - return l + 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 header(lengths, speeds, modes, blinks, ants, brightness=100, date=datetime.now()): - """ lengths[0] is the number of chars of the first text - - Speeds come in as 1..8, but are needed 0..7 here. + """Create a protocol header + * length, speeds, modes, blinks, ants are iterables with at least one element + * lengths[0] is the number of chars/byte-columns of the first text/bitmap, lengths[1] of the second, + and so on... + * 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, + * 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. """ - ants = LedNameBadge._expand_tuple(ants) - blinks = LedNameBadge._expand_tuple(blinks) - speeds = LedNameBadge._expand_tuple(speeds) - modes = LedNameBadge._expand_tuple(modes) + try: + lengths_sum = sum(lengths) + except: + raise TypeError("Please give a list or tuple with at least one number: " + str(lengths)) + 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) + modes = LedNameBadge._prepare_iterable(modes, 0, 8) speeds = [x - 1 for x in speeds] @@ -461,6 +498,7 @@ or h[5] = 0x20 elif brightness <= 75: h[5] = 0x10 + # else default 100% == 0x00 for i in range(8): h[6] += blinks[i] << i @@ -473,18 +511,31 @@ or h[17 + (2 * i) - 1] = lengths[i] // 256 h[17 + (2 * i)] = lengths[i] % 256 - h[38 + 0] = date.year % 100 - h[38 + 1] = date.month - h[38 + 2] = date.day - h[38 + 3] = date.hour - h[38 + 4] = date.minute - h[38 + 5] = date.second + try: + h[38 + 0] = date.year % 100 + h[38 + 1] = date.month + h[38 + 2] = date.day + h[38 + 3] = date.hour + h[38 + 4] = date.minute + h[38 + 5] = date.second + except: + raise TypeError("Please give a datetime object: " + str(date)) return h @staticmethod def write(buf): + """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)) + if len(buf) > 8192: print("Writing more than 8192 bytes damages the display!") sys.exit(1) @@ -522,7 +573,7 @@ or def split_to_ints(list_str): - return tuple([int(x) for x in re.split(r'[\s,]+', list_str)]) + return [int(x) for x in re.split(r'[\s,]+', list_str)] def main(): parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, @@ -540,7 +591,7 @@ def main(): 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", - version=':' + ': :'.join(SimpleTextAndIcons.get_named_bitmaps_keys()) + ': :: or e.g. :path/to/some_icon.png:') + 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") parser.add_argument('--mode-help', action='version', help=argparse.SUPPRESS, version=""" @@ -590,7 +641,7 @@ def main(): else: print("Type: 11x44") - lengths = tuple([b[1] for b in msg_bitmaps]) + lengths = [b[1] for b in msg_bitmaps] speeds = split_to_ints(args.speed) modes = split_to_ints(args.mode) blinks = split_to_ints(args.blink) @@ -603,10 +654,6 @@ def main(): for msg_bitmap in msg_bitmaps: buf.extend(msg_bitmap[0]) - need_padding = len(buf) % 64 - if need_padding: - buf.extend((0,) * (64 - need_padding)) - LedNameBadge.write(buf) diff --git a/photos/bitmap_data_all.png b/photos/bitmap_data_all.png new file mode 100644 index 0000000000000000000000000000000000000000..fbcee1d9cc8602c8a2bbbe2d7de421571276d1a8 GIT binary patch literal 2620 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 literal 0 HcmV?d00001 diff --git a/photos/bitmap_data_descr.puml b/photos/bitmap_data_descr.puml new file mode 100644 index 0000000..7268993 --- /dev/null +++ b/photos/bitmap_data_descr.puml @@ -0,0 +1,35 @@ +@startuml bitmap_data_onebyte +salt +title One byte +{# + { most significant bit 7 + leftmost pixel } | bit 6 | ... | bit 1 | { least significant bit 0 + rightmost pixel } +} +@enduml + +@startuml bitmap_data_onescene +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 ... | ... +} +@enduml + +@startuml bitmap_data_all +salt +title Complete bitmap data +{# + scene 0 == x bytes | ... | scene n == z bytes +} +@enduml diff --git a/photos/bitmap_data_onebyte.png b/photos/bitmap_data_onebyte.png new file mode 100644 index 0000000000000000000000000000000000000000..6aca0be614eb72b6f20075103abf3907483a6f8a GIT binary patch literal 3286 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 literal 0 HcmV?d00001 diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..7d6c8c7 --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,8 @@ +import sys +import unittest + +if __name__ == '__main__': + sys.path.append("..") + suite = unittest.TestLoader().discover(".") + unittest.TextTestRunner().run(suite) + diff --git a/tests/test_lednamebadge_create.py b/tests/test_lednamebadge_create.py index 12de16a..cff662e 100644 --- a/tests/test_lednamebadge_create.py +++ b/tests/test_lednamebadge_create.py @@ -1,4 +1,3 @@ -import datetime from array import array from unittest import TestCase diff --git a/tests/test_lednamebadge_write.py b/tests/test_lednamebadge_write.py index af62a59..ac5f664 100644 --- a/tests/test_lednamebadge_write.py +++ b/tests/test_lednamebadge_write.py @@ -1,5 +1,4 @@ import datetime -from array import array from unittest import TestCase from lednamebadge import LedNameBadge as testee @@ -9,6 +8,27 @@ class Test(TestCase): def setUp(self): self.test_date = datetime.datetime(2022, 11, 13, 17, 38, 24) + def test_prepare_collection_expand(self): + self.assertEqual((1, 1, 1, 1, 1, 1, 1, 1), testee._prepare_iterable((1,), 1, 8)) + self.assertEqual((1, 2, 3, 4, 4, 4, 4, 4), testee._prepare_iterable([1, 2, 3, 4], 1, 8)) + self.assertEqual((1, 2, 3, 4, 5, 6, 7, 8), testee._prepare_iterable((1, 2, 3, 4, 5, 6, 7, 8), 1, 8)) + # Weired, but possible: + self.assertEqual(('A', 'B', 'C', 'D', 'D', 'D', 'D', 'D'), testee._prepare_iterable("ABCD", 'A', 'Z')) + self.assertEqual((True, False, True, True, True, True, True, True), testee._prepare_iterable((True, False, True), False, True)) + + def test_prepare_collection_limit(self): + self.assertEqual((1, 8, 8, 8, 8, 8, 8, 8), testee._prepare_iterable([-1, 9], 1, 8)) + # Weired, but possible: + self.assertEqual(('C', 'C', 'C', 'D', 'E', 'F', 'F', 'F'), testee._prepare_iterable("ABCDEFGH", 'C', 'F')) + self.assertEqual((True, 1, True, True, True, True, True, True), testee._prepare_iterable((True, False, True), 1, 8)) + self.assertEqual((0, False, 0, 0, 0, 0, 0, 0), testee._prepare_iterable((True, False, True), -2, 0)) + + def test_prepare_collection_type(self): + with self.assertRaises(TypeError): + testee._prepare_iterable(4, 1, 8) + with self.assertRaises(TypeError): + testee._prepare_iterable([], 1, 8) + def test_header_2msgs(self): buf = testee.header((6, 7), (5, 3), (6, 2), (0, 1), (1, 0), 75, self.test_date) self.assertEqual([119, 97, 110, 103, 0, 16, 254, 1, 70, 34, 34, 34, 34, 34, 34, 34, 0, 6, 0, 7, 0, 0, 0, 0, 0, @@ -51,3 +71,9 @@ class Test(TestCase): self.assertEqual(buf1[0:38], buf2[0:38]) self.assertEqual(buf1[38 + 6:], buf2[38 + 6:]) self.assertNotEqual(buf1[38:38 + 6], buf2[38:38 + 6]) + + def test_header_type(self): + with self.assertRaises(TypeError): + testee.header(("nan",), (4,), (4,), (0,), (0,), 80, self.test_date) + with self.assertRaises(ValueError): + testee.header((370,380), (4,), (4,), (0,), (0,), 80, self.test_date)