diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..932b3ff --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [jnweiger] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/README.md b/README.md index 4486b43..696518e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # 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/green_badge.jpg) +![LED Mini Board](photos/blueBadge.jpg) -## Warning +Added Accentuated french Characters -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). +![French LED Mini Board](photos/accentuated.gif) + +## Hardware The types supported by this project have an array of @@ -21,21 +22,54 @@ In both configurations, the badge identifies itself on the USB as Mfr=1, Product=2, SerialNumber=0 LSicroelectronics LS32 Custm HID +## Warning + +There are many different versions of LED Badges on the market. +This one uses a USB-HID interface, while others use USB-Serial (see references below). + ## Command Line Installation and Usage +### User access to badge on all Linuxes + +The following will install a udev rule allowing anybody read/write +access to the badge via USB. + + sudo cp 99-led-badge-44x11.rules /etc/udev/rules.d/ + sudo udevadm control --reload-rules && sudo udevadm trigger + ### Required dependencies on Arch Linux sudo pacman -Syu python-pyusb python-hidapi ### Required dependencies on Debian/Ubuntu Systems - sudo pip install pyhidapi - sudo pip install pillow - sudo apt-get install libhidapi-hidraw0 - sudo ln -s /usr/lib/x86_64-linux-gnu/libhidapi-hidraw.so.0 /usr/local/lib/ - or +#### Using Debian/Ubuntu packages (recommended): + 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. + + sudo apt install python3-venv + python3 -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!" + +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 … + +### Required dependencies on Fedora Systems + + sudo dnf install hidapi python3-hidapi python3-pillow python3-pyusb + ### Required dependencies on Mac Systems sudo easy_install pip @@ -85,7 +119,7 @@ Per default you will only see 'Hello'. To see all messages, press the small but sudo python3 ./led-badge-11x44.py -m 5 :gfx/fablabnbg_logo_44x11.png: -loads a fullscreen still image. Avoid whitespace between colons and name. +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:" @@ -103,6 +137,10 @@ shows a bicycle crossing the display in left-to-right and right-to-left (as a se shows a simple animation of a slowly beating heart on the first message, and a blinking heart on the second message. + ./led-badge-11x44.py -B 50 -m 0 -s 8 "Bonjour à toutes et à tous" "Bienvenu(e)s en Master 2 EEA ISHM" "Ingénierie des systèmes Humains Machines" "Bonne réussite à votre promotion 2023-2024" + +![M2 ishm](photos/m2ishm.gif) + python3 ./led-badge-11x44.py --list-names prints the list of builtin icon names, including :happy: :happy2: :heart: :HEART: :heart2: :HEART2: :fablab: :bicycle: :bicycle_r: :owncloud: :: @@ -112,43 +150,143 @@ 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] [-m MODE] [-b BLINK]
-			  [-a ANTS] [-p FILE] [-l]
-                          MESSAGE [MESSAGE ...]
+usage: led-badge-11x44.py [-h] [-t TYPE] [-s SPEED] [-B BRIGHTNESS] [-m MODE] [-b BLINK] [-a ANTS] [-l] MESSAGE [MESSAGE ...]
 
-Upload messages or graphics to a 44x11 led badge via USB HID.
-Version 0.6 from https://github.com/jnweiger/led-badge-44x11
+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
 
-optional arguments:
+options:
   -h, --help            show this help message and exit
-  -t TYPE, --type TYPE
-			Type of display: supported values are 12x48 or
-			(default) 11x44. Rename the program to led-badge-12x48,
-			to switch the default.
+  -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.
   -s SPEED, --speed SPEED
-                        Scroll speed (Range 1..8). Up to 8 comma-seperated
-                        values
-  -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.
+                        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.
   -b BLINK, --blink BLINK
-                        1: blinking, 0: normal. Up to 8 comma-seperated values
-  -a ANTS, --ants ANTS  1: animated border, 0: normal. Up to 8 comma-seperated
-                        values
+                        1: blinking, 0: normal. Up to 8 comma-separated values
+  -a ANTS, --ants ANTS  1: animated border, 0: normal. Up to 8 comma-separated values
   -l, --list-names      list named icons to be embedded in messages and exit
 
 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 diff --git a/led-badge-11x44.py b/led-badge-11x44.py old mode 100755 new mode 100644 index 9f4d467..582349c --- a/led-badge-11x44.py +++ b/led-badge-11x44.py @@ -1,527 +1,3 @@ -#! /usr/bin/python3 -# -*- encoding: utf-8 -*- -# -# (C) 2019 juergen@fabmail.org -# -# This is an upload tool for e.g. -# https://www.sertronics-shop.de/computer/pc-peripheriegeraete/usb-gadgets/led-name-tag-11x44-pixel-usb -# The font_11x44[] data was downloaded from such a device. -# -# Ubuntu install: -# --------------- -# sudo apt-get install python3-usb -# -# Optional for image support: -# sudo apt-get install python3-pil -# -# 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 -# +import lednamebadge -# -# v0.1, 2019-03-05, jw initial draught. HID code is much simpler than expected. -# v0.2, 2019-03-07, jw support for loading bitmaps added. -# v0.3 jw option -p to preload graphics for inline use in text. -# v0.4, 2019-03-08, jw Warning about unused images added. Examples added to the README. -# v0.5, jw Deprecated -p and CTRL-characters. We now use embedding within colons(:) -# Added builtin icons and -l to list them. -# v0.6, 2019-03-14, jw Added --mode-help with hints and example for making animations. -# Options -b --blink, -a --ants added. Removed -p from usage. -# v0.7, 2019-05-20, jw Support pyhidapi, fallback to usb.core. Added python2 compatibility. -# v0.8, 2019-05-23, jw Support usb.core on windows via libusb-win32 -# v0.9, 2019-07-17, jw Support 48x12 configuration too. -# 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 - -import sys, os, re, time, argparse -from datetime import datetime -from array import array -try: - if sys.version_info[0] < 3: 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 -except: - have_pyhidapi = False - try: - import usb.core - 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 with Linux or MacOS or help us implement support for """ + sys.platform) - sys.exit(1) - - -__version = "0.12" - -font_11x44 = ( - # 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - 0x00, 0x38, 0x6c, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, - 0x00, 0xfc, 0x66, 0x66, 0x66, 0x7c, 0x66, 0x66, 0x66, 0xfc, 0x00, - 0x00, 0x7c, 0xc6, 0xc6, 0xc0, 0xc0, 0xc0, 0xc6, 0xc6, 0x7c, 0x00, - 0x00, 0xfc, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0xfc, 0x00, - 0x00, 0xfe, 0x66, 0x62, 0x68, 0x78, 0x68, 0x62, 0x66, 0xfe, 0x00, - 0x00, 0xfe, 0x66, 0x62, 0x68, 0x78, 0x68, 0x60, 0x60, 0xf0, 0x00, - 0x00, 0x7c, 0xc6, 0xc6, 0xc0, 0xc0, 0xce, 0xc6, 0xc6, 0x7e, 0x00, - 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, - 0x00, 0x3c, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, - 0x00, 0x1e, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0xcc, 0xcc, 0x78, 0x00, - 0x00, 0xe6, 0x66, 0x6c, 0x6c, 0x78, 0x6c, 0x6c, 0x66, 0xe6, 0x00, - 0x00, 0xf0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x62, 0x66, 0xfe, 0x00, - 0x00, 0x82, 0xc6, 0xee, 0xfe, 0xd6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, - 0x00, 0x86, 0xc6, 0xe6, 0xf6, 0xde, 0xce, 0xc6, 0xc6, 0xc6, 0x00, - 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, - 0x00, 0xfc, 0x66, 0x66, 0x66, 0x7c, 0x60, 0x60, 0x60, 0xf0, 0x00, - 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xde, 0x7c, 0x06, - 0x00, 0xfc, 0x66, 0x66, 0x66, 0x7c, 0x6c, 0x66, 0x66, 0xe6, 0x00, - 0x00, 0x7c, 0xc6, 0xc6, 0x60, 0x38, 0x0c, 0xc6, 0xc6, 0x7c, 0x00, - 0x00, 0x7e, 0x7e, 0x5a, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, - 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, - 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, - 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0xee, 0xc6, 0x82, 0x00, - 0x00, 0xc6, 0xc6, 0x6c, 0x7c, 0x38, 0x7c, 0x6c, 0xc6, 0xc6, 0x00, - 0x00, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x18, 0x18, 0x3c, 0x00, - 0x00, 0xfe, 0xc6, 0x86, 0x0c, 0x18, 0x30, 0x62, 0xc6, 0xfe, 0x00, - - # 'abcdefghijklmnopqrstuvwxyz' - 0x00, 0x00, 0x00, 0x00, 0x78, 0x0c, 0x7c, 0xcc, 0xcc, 0x76, 0x00, - 0x00, 0xe0, 0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x7c, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xc6, 0x7c, 0x00, - 0x00, 0x1c, 0x0c, 0x0c, 0x7c, 0xcc, 0xcc, 0xcc, 0xcc, 0x76, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0xc6, 0x7c, 0x00, - 0x00, 0x1c, 0x36, 0x30, 0x78, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x76, 0xcc, 0xcc, 0x7c, 0x0c, 0xcc, 0x78, - 0x00, 0xe0, 0x60, 0x60, 0x6c, 0x76, 0x66, 0x66, 0x66, 0xe6, 0x00, - 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, - 0x0c, 0x0c, 0x00, 0x1c, 0x0c, 0x0c, 0x0c, 0x0c, 0xcc, 0xcc, 0x78, - 0x00, 0xe0, 0x60, 0x60, 0x66, 0x6c, 0x78, 0x78, 0x6c, 0xe6, 0x00, - 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xec, 0xfe, 0xd6, 0xd6, 0xd6, 0xc6, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xdc, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xdc, 0x66, 0x66, 0x7c, 0x60, 0x60, 0xf0, - 0x00, 0x00, 0x00, 0x00, 0x7c, 0xcc, 0xcc, 0x7c, 0x0c, 0x0c, 0x1e, - 0x00, 0x00, 0x00, 0x00, 0xde, 0x76, 0x60, 0x60, 0x60, 0xf0, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0x70, 0x1c, 0xc6, 0x7c, 0x00, - 0x00, 0x10, 0x30, 0x30, 0xfc, 0x30, 0x30, 0x30, 0x34, 0x18, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x76, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc6, 0xd6, 0xd6, 0xd6, 0xfe, 0x6c, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc6, 0x6c, 0x38, 0x38, 0x6c, 0xc6, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x0c, 0xf8, - 0x00, 0x00, 0x00, 0x00, 0xfe, 0x8c, 0x18, 0x30, 0x62, 0xfe, 0x00, - - # '0987654321^ !"\0$%&/()=?` °\\}][{' - 0x00, 0x7c, 0xc6, 0xce, 0xde, 0xf6, 0xe6, 0xc6, 0xc6, 0x7c, 0x00, - 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xc6, 0x7c, 0x00, - 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, - 0x00, 0xfe, 0xc6, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, - 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, - 0x00, 0xfe, 0xc0, 0xc0, 0xfc, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, - 0x00, 0x0c, 0x1c, 0x3c, 0x6c, 0xcc, 0xfe, 0x0c, 0x0c, 0x1e, 0x00, - 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x3c, 0x06, 0x06, 0xc6, 0x7c, 0x00, - 0x00, 0x7c, 0xc6, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc6, 0xfe, 0x00, - 0x00, 0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, - 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x3c, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x18, 0x3c, 0x3c, 0x3c, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, - 0x66, 0x66, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x7c, 0x04, 0x14, 0x18, 0x10, 0x10, 0x20, - 0x10, 0x7c, 0xd6, 0xd6, 0x70, 0x1c, 0xd6, 0xd6, 0x7c, 0x10, 0x10, - 0x00, 0x60, 0x92, 0x96, 0x6c, 0x10, 0x6c, 0xd2, 0x92, 0x0c, 0x00, - 0x00, 0x38, 0x6c, 0x6c, 0x38, 0x76, 0xdc, 0xcc, 0xcc, 0x76, 0x00, - 0x00, 0x00, 0x02, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x80, 0x00, - 0x00, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00, - 0x00, 0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x18, 0x30, 0x00, - 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x7c, 0xc6, 0xc6, 0x0c, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, - 0x18, 0x18, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x7c, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x7c, - 0x00, 0x10, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x80, 0xc0, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x02, 0x00, 0x00, - 0x00, 0x70, 0x18, 0x18, 0x18, 0x0e, 0x18, 0x18, 0x18, 0x70, 0x00, - 0x00, 0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00, - 0x00, 0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00, - 0x00, 0x0e, 0x18, 0x18, 0x18, 0x70, 0x18, 0x18, 0x18, 0x0e, 0x00, - - # "@ ~ |<>,;.:-_#'+* " - 0x00, 0x00, 0x3c, 0x42, 0x9d, 0xa5, 0xad, 0xb6, 0x40, 0x3c, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc0, 0x00, 0x00, 0x00, - 0x00, 0x76, 0xdc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x08, 0x08, 0x7c, 0x08, 0x08, 0x18, 0x18, 0x28, 0x28, 0x48, 0x18, - 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, - 0x00, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x00, - 0x00, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x10, 0x20, - 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x08, 0x10, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, - 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, - 0x00, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x00, 0x00, - 0x18, 0x18, 0x08, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00, 0x00, - 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, - 0xc6, 0xc6, 0x38, 0x6c, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, - 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, -) - -charmap = u'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + \ - u'abcdefghijklmnopqrstuvwxyz' + \ - u'0987654321^ !"\0$%&/()=?` °\\}][{' + \ - u"@ ~ |<>,;.:-_#'+* " + \ - u"äöüÄÖÜߊ" - -char_offset = {} -for i in range(len(charmap)): - char_offset[charmap[i]] = 11 * i - # print(i, charmap[i], char_offset[charmap[i]]) - - -bitmap_preloaded = [ ([],0) ] -bitmaps_preloaded_unused = False - -bitmap_named = { - 'ball': (array('B', ( - 0b00000000, - 0b00000000, - 0b00111100, - 0b01111110, - 0b11111111, - 0b11111111, - 0b11111111, - 0b11111111, - 0b01111110, - 0b00111100, - 0b00000000 - )), 1, '\x1e'), - 'happy': (array('B', ( - 0b00000000, # 0x00 - 0b00000000, # 0x00 - 0b00111100, # 0x3c - 0b01000010, # 0x42 - 0b10100101, # 0xa5 - 0b10000001, # 0x81 - 0b10100101, # 0xa5 - 0b10011001, # 0x99 - 0b01000010, # 0x42 - 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'), - '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'), -} -bitmap_builtin = {} -for i in bitmap_named: - bitmap_builtin[bitmap_named[i][2]] = bitmap_named[i] - - -def bitmap_char(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. - """ - if ord(ch) < 32: - if ch in bitmap_builtin: - return bitmap_builtin[ch][:2] - - global bitmaps_preloaded_unused - bitmaps_preloaded_unused = False - return bitmap_preloaded[ord(ch)] - - o = char_offset[ch] - return (font_11x44[o:o+11],1) - - -def bitmap_text(text): - """ 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. - ":happy:" is replaced with a reference to a builtin smiley glyph - ":heart:" is replaced with a reference to a builtin heart glyph - ":gfx/logo.png:" preloads the file gfx/logo.png and is replaced the corresponding control char. - """ - def colonrepl(m): - name = m.group(1) - if name == '': - return ':' - if re.match('^[0-9]*$', name): # py3 name.isdecimal() - return chr(int(name)) - if '.' in name: - bitmap_preloaded.append(bitmap_img(name)) - return chr(len(bitmap_preloaded)-1) - b = bitmap_named[name] - return b[2] - - text = re.sub(r':([^:]*):', colonrepl, text) - buf = array('B') - cols = 0 - for c in text: - (b,n) = bitmap_char(c) - buf.extend(b) - cols += n - return (buf, cols) - - -def bitmap_img(file): - """ returns a tuple of (buffer, length_in_byte_columns) - """ - from PIL import Image - - im = Image.open(file) - print("fetching bitmap from file %s -> (%d x %d)" % (file, im.width, im.height)) - if im.height != 11: - sys.exit("%s: image height must be 11px. Seen %d" % (file, im.height)) - buf = array('B') - cols = int((im.width+7)/8) - for col in range(cols): - for row in range(11): # [0..10] - byte_val = 0 - for bit in range(8): # [0..7] - bit_val = 0 - x = 8*col+bit - if x < im.width: - pixel_color = im.getpixel( (x, row) ) - if isinstance(pixel_color, tuple): - monochrome_color = sum(pixel_color[:3]) / len(pixel_color[:3]) - elif isinstance(pixel_color, int): - monochrome_color = pixel_color - else: - sys.exit("%s: Unknown pixel format detected (%s)!" % (file, pixel_color)) - if monochrome_color > 127: - bit_val = 1 << (7-bit) - byte_val += bit_val - buf.append(byte_val) - im.close() - return (buf, cols) - - -def bitmap(arg): - """ if arg is a valid and existing path name, we load it as an image. - Otherwise we take it as a string. - """ - if os.path.exists(arg): - return bitmap_img(arg) - return bitmap_text(arg) - - -proto_header = ( - 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 -) - - -def header(lengths, speeds, modes, blink, ants, brightness=100): - """ lengths[0] is the number of chars of the first text - - Speeds come in as 1..8, but are needed 0..7 here. - """ - a = [int(x) for x in re.split(r'[\s,]+', ants)] - a = a + [a[-1]]*(8-len(a)) # repeat last element - - b = [int(x) for x in re.split(r'[\s,]+', blink)] - b = b + [b[-1]]*(8-len(b)) # repeat last element - - s = [int(x)-1 for x in re.split(r'[\s,]+', speeds)] - s = s + [s[-1]]*(8-len(s)) # repeat last element - - m = [int(x) for x in re.split(r'[\s,]+', modes)] - m = m + [m[-1]]*(8-len(m)) # repeat last element - - h = list(proto_header) - - if brightness <= 25: - h[5] = 0x40 - elif brightness <= 50: - h[5] = 0x20 - elif brightness <= 75: - h[5] = 0x10 - - for i in range(8): - h[6] += b[i]< 8192: - print ("Writing more than 8192 bytes damages the display!") - sys.exit(1) - -if have_pyhidapi: - pyhidapi.hid_write(dev, buf) -else: - for i in range(int(len(buf)/64)): - time.sleep(0.1) - dev.write(1, buf[i*64:i*64+64]) - -if have_pyhidapi: - pyhidapi.hid_close(dev) +lednamebadge.main() diff --git a/lednamebadge.py b/lednamebadge.py new file mode 100755 index 0000000..a598f9f --- /dev/null +++ b/lednamebadge.py @@ -0,0 +1,672 @@ +#! /usr/bin/python3 +# -*- encoding: utf-8 -*- +# +# (C) 2019 juergen@fabmail.org +# +# This is an upload tool for e.g. +# https://www.sertronics-shop.de/computer/pc-peripheriegeraete/usb-gadgets/led-name-tag-11x44-pixel-usb +# The font_11x44[] data was downloaded from such a device. +# +# Ubuntu install: +# --------------- +# sudo apt-get install python3-usb +# +# Optional for image support: +# sudo apt-get install python3-pil +# +# 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 +# + +# +# v0.1, 2019-03-05, jw initial draught. HID code is much simpler than expected. +# v0.2, 2019-03-07, jw support for loading bitmaps added. +# v0.3 jw option -p to preload graphics for inline use in text. +# v0.4, 2019-03-08, jw Warning about unused images added. Examples added to the README. +# v0.5, jw Deprecated -p and CTRL-characters. We now use embedding within colons(:) +# Added builtin icons and -l to list them. +# v0.6, 2019-03-14, jw Added --mode-help with hints and example for making animations. +# Options -b --blink, -a --ants added. Removed -p from usage. +# v0.7, 2019-05-20, jw Support pyhidapi, fallback to usb.core. Added python2 compatibility. +# v0.8, 2019-05-23, jw Support usb.core on windows via libusb-win32 +# v0.9, 2019-07-17, jw Support 48x12 configuration too. +# 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 +import re +import sys +import time +from array import array +from datetime import datetime + + +__version = "0.13" + + +class SimpleTextAndIcons: + font_11x44 = ( + # 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 0x00, 0x38, 0x6c, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, + 0x00, 0xfc, 0x66, 0x66, 0x66, 0x7c, 0x66, 0x66, 0x66, 0xfc, 0x00, + 0x00, 0x7c, 0xc6, 0xc6, 0xc0, 0xc0, 0xc0, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0xfc, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0xfc, 0x00, + 0x00, 0xfe, 0x66, 0x62, 0x68, 0x78, 0x68, 0x62, 0x66, 0xfe, 0x00, + 0x00, 0xfe, 0x66, 0x62, 0x68, 0x78, 0x68, 0x60, 0x60, 0xf0, 0x00, + 0x00, 0x7c, 0xc6, 0xc6, 0xc0, 0xc0, 0xce, 0xc6, 0xc6, 0x7e, 0x00, + 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, + 0x00, 0x3c, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, + 0x00, 0x1e, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0xcc, 0xcc, 0x78, 0x00, + 0x00, 0xe6, 0x66, 0x6c, 0x6c, 0x78, 0x6c, 0x6c, 0x66, 0xe6, 0x00, + 0x00, 0xf0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x62, 0x66, 0xfe, 0x00, + 0x00, 0x82, 0xc6, 0xee, 0xfe, 0xd6, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, + 0x00, 0x86, 0xc6, 0xe6, 0xf6, 0xde, 0xce, 0xc6, 0xc6, 0xc6, 0x00, + 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0xfc, 0x66, 0x66, 0x66, 0x7c, 0x60, 0x60, 0x60, 0xf0, 0x00, + 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xde, 0x7c, 0x06, + 0x00, 0xfc, 0x66, 0x66, 0x66, 0x7c, 0x6c, 0x66, 0x66, 0xe6, 0x00, + 0x00, 0x7c, 0xc6, 0xc6, 0x60, 0x38, 0x0c, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0x7e, 0x7e, 0x5a, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, + 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, + 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0xee, 0xc6, 0x82, 0x00, + 0x00, 0xc6, 0xc6, 0x6c, 0x7c, 0x38, 0x7c, 0x6c, 0xc6, 0xc6, 0x00, + 0x00, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x18, 0x18, 0x3c, 0x00, + 0x00, 0xfe, 0xc6, 0x86, 0x0c, 0x18, 0x30, 0x62, 0xc6, 0xfe, 0x00, + + # 'abcdefghijklmnopqrstuvwxyz' + 0x00, 0x00, 0x00, 0x00, 0x78, 0x0c, 0x7c, 0xcc, 0xcc, 0x76, 0x00, + 0x00, 0xe0, 0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x7c, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xc6, 0x7c, 0x00, + 0x00, 0x1c, 0x0c, 0x0c, 0x7c, 0xcc, 0xcc, 0xcc, 0xcc, 0x76, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0xc6, 0x7c, 0x00, + 0x00, 0x1c, 0x36, 0x30, 0x78, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x76, 0xcc, 0xcc, 0x7c, 0x0c, 0xcc, 0x78, + 0x00, 0xe0, 0x60, 0x60, 0x6c, 0x76, 0x66, 0x66, 0x66, 0xe6, 0x00, + 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, + 0x0c, 0x0c, 0x00, 0x1c, 0x0c, 0x0c, 0x0c, 0x0c, 0xcc, 0xcc, 0x78, + 0x00, 0xe0, 0x60, 0x60, 0x66, 0x6c, 0x78, 0x78, 0x6c, 0xe6, 0x00, + 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xec, 0xfe, 0xd6, 0xd6, 0xd6, 0xc6, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xdc, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xdc, 0x66, 0x66, 0x7c, 0x60, 0x60, 0xf0, + 0x00, 0x00, 0x00, 0x00, 0x7c, 0xcc, 0xcc, 0x7c, 0x0c, 0x0c, 0x1e, + 0x00, 0x00, 0x00, 0x00, 0xde, 0x76, 0x60, 0x60, 0x60, 0xf0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7c, 0xc6, 0x70, 0x1c, 0xc6, 0x7c, 0x00, + 0x00, 0x10, 0x30, 0x30, 0xfc, 0x30, 0x30, 0x30, 0x34, 0x18, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x76, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x6c, 0x38, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc6, 0xd6, 0xd6, 0xd6, 0xfe, 0x6c, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc6, 0x6c, 0x38, 0x38, 0x6c, 0xc6, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x0c, 0xf8, + 0x00, 0x00, 0x00, 0x00, 0xfe, 0x8c, 0x18, 0x30, 0x62, 0xfe, 0x00, + + # '0987654321^ !"\0$%&/()=?` °\\}][{' + 0x00, 0x7c, 0xc6, 0xce, 0xde, 0xf6, 0xe6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0xc6, 0x7c, 0x00, + 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0xfe, 0xc6, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, + 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0xfe, 0xc0, 0xc0, 0xfc, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00, + 0x00, 0x0c, 0x1c, 0x3c, 0x6c, 0xcc, 0xfe, 0x0c, 0x0c, 0x1e, 0x00, + 0x00, 0x7c, 0xc6, 0x06, 0x06, 0x3c, 0x06, 0x06, 0xc6, 0x7c, 0x00, + 0x00, 0x7c, 0xc6, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc6, 0xfe, 0x00, + 0x00, 0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00, + 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x3c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x3c, 0x3c, 0x3c, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, + 0x66, 0x66, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7c, 0x04, 0x14, 0x18, 0x10, 0x10, 0x20, + 0x10, 0x7c, 0xd6, 0xd6, 0x70, 0x1c, 0xd6, 0xd6, 0x7c, 0x10, 0x10, + 0x00, 0x60, 0x92, 0x96, 0x6c, 0x10, 0x6c, 0xd2, 0x92, 0x0c, 0x00, + 0x00, 0x38, 0x6c, 0x6c, 0x38, 0x76, 0xdc, 0xcc, 0xcc, 0x76, 0x00, + 0x00, 0x00, 0x02, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x80, 0x00, + 0x00, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00, + 0x00, 0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x18, 0x30, 0x00, + 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7c, 0xc6, 0xc6, 0x0c, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, + 0x18, 0x18, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7c, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x7c, + 0x00, 0x10, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0xc0, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x02, 0x00, 0x00, + 0x00, 0x70, 0x18, 0x18, 0x18, 0x0e, 0x18, 0x18, 0x18, 0x70, 0x00, + 0x00, 0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00, + 0x00, 0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00, + 0x00, 0x0e, 0x18, 0x18, 0x18, 0x70, 0x18, 0x18, 0x18, 0x0e, 0x00, + + # "@ ~ |<>,;.:-_#'+* " + 0x00, 0x00, 0x3c, 0x42, 0x9d, 0xa5, 0xad, 0xb6, 0x40, 0x3c, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x76, 0xdc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x08, 0x7c, 0x08, 0x08, 0x18, 0x18, 0x28, 0x28, 0x48, 0x18, + 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, + 0x00, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x00, + 0x00, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x10, 0x20, + 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x08, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0xfe, 0x6c, 0x6c, 0x00, 0x00, + 0x18, 0x18, 0x08, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00, 0x00, + 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, + 0xc6, 0xc6, 0x38, 0x6c, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, + 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, + 0x00, 0x6c, 0x6c, 0x00, 0x78, 0x0c, 0x7c, 0xcc, 0xcc, 0x76, 0x00, + 0x00, 0x60, 0x18, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0x6c, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0x60, 0x18, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x76, 0x00, + 0x00, 0x6c, 0x6c, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x76, 0x00, + 0x00, 0x60, 0x18, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0xc6, 0x7c, 0x00, + 0x00, 0x18, 0x60, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0xc6, 0x7c, 0x00, + 0x00, 0x10, 0x6c, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0xc6, 0x7c, 0x00, + 0x00, 0x6c, 0x6c, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0xc6, 0x7c, 0x00, + 0x00, 0x10, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0x6c, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, + 0x00, 0x10, 0x6c, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x76, 0x00, + 0x00, 0x10, 0x6c, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, + 0x00, 0x6c, 0x6c, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00, + 0x00, 0x6c, 0x6c, 0x00, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0x0c, 0xf8, + 0x00, 0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc0, 0xc6, 0x7c, 0x10, 0x30, + + # "ÀÅÄÉÈÊËÖÔÜÛÙŸ" + 0x60, 0x18, 0x38, 0x6c, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, + 0x10, 0x6c, 0x38, 0x6c, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, + 0x6c, 0x6c, 0x38, 0x6c, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0xc6, 0x00, + 0x18, 0x60, 0xfe, 0x62, 0x68, 0x78, 0x68, 0x62, 0x66, 0xfe, 0x00, + 0x60, 0x18, 0xfe, 0x62, 0x68, 0x78, 0x68, 0x62, 0x66, 0xfe, 0x00, + 0x10, 0x6c, 0xfe, 0x62, 0x68, 0x78, 0x68, 0x62, 0x66, 0xfe, 0x00, + 0x6c, 0x6c, 0xfe, 0x62, 0x68, 0x78, 0x68, 0x62, 0x66, 0xfe, 0x00, + 0x6c, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, # Ö + 0x10, 0x6c, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, # Ô + 0x6c, 0x6c, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, # Ü + 0x10, 0x6c, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, # Û + 0x60, 0x18, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00, # Ù + 0x66, 0x66, 0x00, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x18, 0x3c, 0x00, # Ÿ + ) + + charmap = u'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + \ + u'abcdefghijklmnopqrstuvwxyz' + \ + u'0987654321^ !"\0$%&/()=?` °\\}][{' + \ + u"@ ~ |<>,;.:-_#'+* " + \ + u"äöüÄÖÜߊ" + \ + u"àäòöùüèéêëôöûîïÿç" + \ + u"ÀÅÄÉÈÊËÖÔÜÛÙŸ" + + char_offsets = {} + for i in range(len(charmap)): + char_offsets[charmap[i]] = 11 * i + # print(i, charmap[i], char_offsets[charmap[i]]) + + bitmap_named = { + 'ball': (array('B', ( + 0b00000000, + 0b00000000, + 0b00111100, + 0b01111110, + 0b11111111, + 0b11111111, + 0b11111111, + 0b11111111, + 0b01111110, + 0b00111100, + 0b00000000 + )), 1, '\x1e'), + 'happy': (array('B', ( + 0b00000000, # 0x00 + 0b00000000, # 0x00 + 0b00111100, # 0x3c + 0b01000010, # 0x42 + 0b10100101, # 0xa5 + 0b10000001, # 0x81 + 0b10100101, # 0xa5 + 0b10011001, # 0x99 + 0b01000010, # 0x42 + 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'), + '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'), + } + + 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 + + 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(): + 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). + The bits in each byte are horizontal, highest bit is left. + """ + if ord(ch) < 32: + if ch in SimpleTextAndIcons.bitmap_builtin: + return SimpleTextAndIcons.bitmap_builtin[ch][:2] + + self.bitmaps_preloaded_unused = False + return self.bitmap_preloaded[ord(ch)] + + 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 + "::" is replaced with a single ":" + ":1: is replaced with CTRL-A referencing the first preloaded or loaded image. + ":happy:" is replaced with a reference to a builtin smiley glyph + ":heart:" is replaced with a reference to a builtin heart glyph + ":gfx/logo.png:" preloads the file gfx/logo.png and is replaced the corresponding control char. + """ + + def replace_symbolic(m): + name = m.group(1) + if name == '': + return ':' + if re.match('^[0-9]*$', name): # py3 name.isdecimal() + return chr(int(name)) + if '.' in name: + self.bitmap_preloaded.append(SimpleTextAndIcons.bitmap_img(name)) + return chr(len(self.bitmap_preloaded) - 1) + return SimpleTextAndIcons.bitmap_named[name][2] + + text = re.sub(r':([^:]*):', replace_symbolic, text) + buf = array('B') + cols = 0 + for c in text: + (b, n) = self.bitmap_char(c) + buf.extend(b) + cols += n + return (buf, cols) + + + @staticmethod + def bitmap_img(file): + """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 + + im = Image.open(file) + print("fetching bitmap from file %s -> (%d x %d)" % (file, im.width, im.height)) + if im.height != 11: + sys.exit("%s: image height must be 11px. Seen %d" % (file, im.height)) + buf = array('B') + cols = int((im.width + 7) / 8) + for col in range(cols): + for row in range(11): # [0..10] + byte_val = 0 + for bit in range(8): # [0..7] + bit_val = 0 + x = 8 * col + bit + if x < im.width and row < im.height: + pixel_color = im.getpixel((x, row)) + if isinstance(pixel_color, tuple): + monochrome_color = sum(pixel_color[:3]) / len(pixel_color[:3]) + elif isinstance(pixel_color, int): + monochrome_color = pixel_color + else: + sys.exit("%s: Unknown pixel format detected (%s)!" % (file, pixel_color)) + if monochrome_color > 127: + bit_val = 1 << (7 - bit) + byte_val += bit_val + buf.append(byte_val) + 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()). + """ + if os.path.exists(arg): + return SimpleTextAndIcons.bitmap_img(arg) + 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 + + 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") + 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) + + + @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 header(lengths, speeds, modes, blinks, ants, brightness=100, date=datetime.now()): + """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. + """ + 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] + + h = list(LedNameBadge._protocol_header_template) + + if brightness <= 25: + h[5] = 0x40 + elif brightness <= 50: + h[5] = 0x20 + elif brightness <= 75: + h[5] = 0x10 + # else default 100% == 0x00 + + for i in range(8): + h[6] += blinks[i] << i + h[7] += ants[i] << i + + for i in range(8): + h[8 + i] = 16 * speeds[i] + modes[i] + + for i in range(len(lengths)): + h[17 + (2 * i) - 1] = lengths[i] // 256 + h[17 + (2 * i)] = lengths[i] % 256 + + 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) + + 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.") + 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.") + 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]) + + +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, + description='Upload messages or graphics to a 11x44 led badge via USB HID.\nVersion %s from https://github.com/jnweiger/led-badge-ls32\n -- see there for more examples and for updates.' % __version, + 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('-B', '--brightness', default='100', + help="Brightness for the display in percent: 25, 50, 75, or 100") + parser.add_argument('-m', '--mode', default='0', + help="Up to 8 mode values: Scroll-left(0) -right(1) -up(2) -down(3); still-centered(4); animation(5); drop-down(6); curtain(7); laser(8); See '--mode-help' for more details.") + parser.add_argument('-b', '--blink', default='0', help="1: blinking, 0: normal. Up to 8 comma-separated values") + parser.add_argument('-a', '--ants', default='0', help="1: animated border, 0: normal. Up to 8 comma-separated values") + parser.add_argument('-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:') + 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=""" + + -m 5 "Animation" + + Animation frames are 6 character (or 48px) wide. Upload an animation of + N frames as one image N*48 pixels wide, 11 pixels high. + Frames run from left to right and repeat endless. + Speeds [1..8] result in ca. [1.2 1.3 2.0 2.4 2.8 4.5 7.5 15] fps. + + Example of a slowly beating heart: + sudo %s -s1 -m5 " :heart2: :HEART2:" + + -m 9 "Smooth" + -m 10 "Rotate" + + These modes are mentioned in the BMP Badge software. + Text is shown static, or sometimes (longer texts?) not shown at all. + One significant difference is: The text of the first message stays visible after + upload, even if the USB cable remains connected. + (No "rotation" or "smoothing"(?) effect can be expected, though) + """ % sys.argv[0]) + args = parser.parse_args() + + creator = SimpleTextAndIcons() + + if args.preload: + for filename in args.preload: + creator.add_preload_img(filename) + + msg_bitmaps = [] + for msg_arg in args.message: + msg_bitmaps.append(creator.bitmap(msg_arg)) + + if creator.are_preloaded_unused(): + print( + "\nWARNING:\n Your preloaded images are not used.\n Try without '-p' or embed the control character '^A' in your message.\n") + + if '12' in args.type or '12' in sys.argv[0]: + print("Type: 12x48") + for msg_bitmap in msg_bitmaps: + # trivial hack to support 12x48 badges: + # patch extra empty lines into the message stream. + for i in reversed(range(1, int(len(msg_bitmap[0]) / 11) + 1)): + msg_bitmap[0][i * 11:i * 11] = array('B', [0]) + else: + print("Type: 11x44") + + 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) + ants = split_to_ints(args.ants) + brightness = int(args.brightness) + + buf = array('B') + buf.extend(LedNameBadge.header(lengths, speeds, modes, blinks, ants, brightness)) + + 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") + + LedNameBadge.write(buf) + + +if __name__ == '__main__': + main() diff --git a/photos/accentuated.gif b/photos/accentuated.gif new file mode 100644 index 0000000..2071331 Binary files /dev/null and b/photos/accentuated.gif differ diff --git a/photos/bitmap_data_all.png b/photos/bitmap_data_all.png new file mode 100644 index 0000000..fbcee1d Binary files /dev/null and b/photos/bitmap_data_all.png differ 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 0000000..6aca0be Binary files /dev/null and b/photos/bitmap_data_onebyte.png differ diff --git a/photos/bitmap_data_onescene.png b/photos/bitmap_data_onescene.png new file mode 100644 index 0000000..d845afd Binary files /dev/null and b/photos/bitmap_data_onescene.png differ diff --git a/photos/blueBadge.jpg b/photos/blueBadge.jpg new file mode 100644 index 0000000..9503da3 Binary files /dev/null and b/photos/blueBadge.jpg differ diff --git a/photos/m2ishm.gif b/photos/m2ishm.gif new file mode 100644 index 0000000..70f1d37 Binary files /dev/null and b/photos/m2ishm.gif differ diff --git a/tests/resources/bitpatterns.png b/tests/resources/bitpatterns.png new file mode 100644 index 0000000..03b7a90 Binary files /dev/null and b/tests/resources/bitpatterns.png differ 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 new file mode 100644 index 0000000..cff662e --- /dev/null +++ b/tests/test_lednamebadge_create.py @@ -0,0 +1,34 @@ +from array import array +from unittest import TestCase + +from lednamebadge import SimpleTextAndIcons as testee + + +class Test(TestCase): + def test_bitmap_png(self): + creator = testee() + buf = creator.bitmap("resources/bitpatterns.png") + self.assertEqual((array('B', + [128, 64, 32, 16, 8, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 64, 32, 0, 1, 2, 3, + 4, 5, 15, 31, 63, 127, 255]), + 3), buf) + + def test_bitmap_text(self): + creator = testee() + buf = creator.bitmap("/:HEART2:\\") + self.assertEqual((array('B', + [0, 0, 2, 6, 12, 24, 48, 96, 192, 128, 0, 0, 12, 30, 63, 63, 63, 31, 15, 7, 3, 1, 0, 96, + 240, 248, 248, 248, 240, 224, 192, 128, 0, 0, 128, 192, 96, 48, 24, 12, 6, 2, 0, 0]), + 4), buf) + + def test_preload(self): + creator = testee() + self.assertFalse(creator.are_preloaded_unused()) + creator.add_preload_img("resources/bitpatterns.png") + self.assertTrue(creator.are_preloaded_unused()) + buf = creator.bitmap("\x01") + self.assertFalse(creator.are_preloaded_unused()) + self.assertEqual((array('B', + [128, 64, 32, 16, 8, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 64, 32, 0, 1, 2, 3, + 4, 5, 15, 31, 63, 127, 255]), + 3), buf) diff --git a/tests/test_lednamebadge_write.py b/tests/test_lednamebadge_write.py new file mode 100644 index 0000000..ac5f664 --- /dev/null +++ b/tests/test_lednamebadge_write.py @@ -0,0 +1,79 @@ +import datetime +from unittest import TestCase + +from lednamebadge import LedNameBadge as testee + + +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, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 11, 13, 17, 38, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], buf) + + def test_header_8msgs(self): + buf = testee.header((1, 2, 3, 4, 5, 6, 7, 8), + (1, 2, 3, 4, 5, 6, 7, 8), + (1, 2, 3, 4, 5, 6, 7, 8), + (0, 1, 0, 1, 0, 1, 0, 1), + (1, 0, 1, 0, 1, 0, 1, 0), + 25, + self.test_date) + self.assertEqual([119, 97, 110, 103, 0, 64, 170, 85, 1, 18, 35, 52, 69, 86, 103, 120, 0, 1, 0, 2, 0, 3, 0, 4, 0, + 5, 0, 6, 0, 7, 0, 8, 0, 0, 0, 0, 0, 0, 22, 11, 13, 17, 38, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], buf) + + def test_header_brightness(self): + buf = testee.header((6,), (4,), (4,), (0,), (0,), 25, self.test_date) + self.assertEqual([119, 97, 110, 103, 0, 64, 0, 0, 52, 52, 52, 52, 52, 52, 52, 52, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 11, 13, 17, 38, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0], buf) + buf = testee.header((6,), (4,), (4,), (0,), (0,), 26, self.test_date) + self.assertEqual([119, 97, 110, 103, 0, 32, 0, 0, 52, 52, 52, 52, 52, 52, 52, 52, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 11, 13, 17, 38, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0], buf) + buf = testee.header((6,), (4,), (4,), (0,), (0,), 60, self.test_date) + self.assertEqual([119, 97, 110, 103, 0, 16, 0, 0, 52, 52, 52, 52, 52, 52, 52, 52, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 11, 13, 17, 38, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0], buf) + buf = testee.header((6,), (4,), (4,), (0,), (0,), 80, self.test_date) + self.assertEqual([119, 97, 110, 103, 0, 0, 0, 0, 52, 52, 52, 52, 52, 52, 52, 52, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 11, 13, 17, 38, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0], buf) + + def test_header_date(self): + buf1 = testee.header((6,), (4,), (4,), (0,), (0,), 100, self.test_date) + buf2 = testee.header((6,), (4,), (4,), (0,), (0,), 100) + 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)