Merge branch 'bensartori-modularization'

rebase020124
Juergen Weigert 11 months ago
commit 5f5340fb29
  1. 111
      README.md
  2. 582
      led-badge-11x44.py
  3. 669
      lednamebadge.py
  4. BIN
      photos/bitmap_data_all.png
  5. 35
      photos/bitmap_data_descr.puml
  6. BIN
      photos/bitmap_data_onebyte.png
  7. BIN
      photos/bitmap_data_onescene.png
  8. BIN
      tests/resources/bitpatterns.png
  9. 8
      tests/run_tests.py
  10. 34
      tests/test_lednamebadge_create.py
  11. 79
      tests/test_lednamebadge_write.py

@ -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,117 @@ Example combining image and text:
sudo ./led-badge-11x44.py "I:HEART2:you"
</pre>
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/

@ -1,581 +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,
# "àäòöùüèéêëôöûîïÿç"
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_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 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(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] << i
h[7] += a[i] << i
for i in range(8):
h[8 + i] = 16 * s[i] + m[i]
for i in range(len(lengths)):
h[17 + (2 * i) - 1] = lengths[i] // 256
h[17 + (2 * i)] = lengths[i] % 256
cdate = datetime.now()
h[38 + 0] = cdate.year % 100
h[38 + 1] = cdate.month
h[38 + 2] = cdate.day
h[38 + 3] = cdate.hour
h[38 + 4] = cdate.minute
h[38 + 5] = cdate.second
return h
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(bitmap_named.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()
if have_pyhidapi:
devinfo = pyhidapi.hid_enumerate(0x0416, 0x5020)
# dev = pyhidapi.hid_open(0x0416, 0x5020)
else:
if args.hid != "0":
sys.exit("HID API access is needed but not initialized. Fix your setup")
dev = usb.core.find(idVendor=0x0416, idProduct=0x5020)
if have_pyhidapi:
if devinfo:
dev = pyhidapi.hid_open_path(devinfo[0].path)
print("using [%s %s] int=%d page=%s via pyHIDAPI" % (
devinfo[0].manufacturer_string, devinfo[0].product_string, devinfo[0].interface_number, devinfo[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)
else:
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))
if args.preload:
for file in args.preload:
bitmap_preloaded.append(bitmap_img(file))
bitmaps_preloaded_unused = True
msgs = []
for arg in args.message:
msgs.append(bitmap(arg))
if bitmaps_preloaded_unused == True:
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 in msgs:
# trivial hack to support 12x48 badges:
# patch extra empty lines into the message stream.
for o in reversed(range(1, int(len(msg[0]) / 11) + 1)):
msg[0][o * 11:o * 11] = array('B', [0])
else:
print("Type: 11x44")
buf = array('B')
buf.extend(header(list(map(lambda x: x[1], msgs)), args.speed, args.mode, args.blink, args.ants, int(args.brightness)))
for msg in msgs:
buf.extend(msg[0])
needpadding = len(buf) % 64
if needpadding:
buf.extend((0,) * (64 - needpadding))
# print(buf) # array('B', [119, 97, 110, 103, 0, 0, 0, 0, 48, 48, 48, 48, 48, 48, 48, 48, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 126, 255, 255, 255, 255, 126, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
if len(buf) > 8192:
print("Writing more than 8192 bytes damages the display!")
sys.exit(1)
if have_pyhidapi:
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])
pyhidapi.hid_write(dev,sendbuf)
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()

@ -0,0 +1,669 @@
#! /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,
# "àäòöùüèéêëôöûîïÿç"
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:
if args.hid != "0":
sys.exit("HID API access is needed but not initialized. Fix your setup")
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])
LedNameBadge.write(buf)
if __name__ == '__main__':
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

@ -0,0 +1,8 @@
import sys
import unittest
if __name__ == '__main__':
sys.path.append("..")
suite = unittest.TestLoader().discover(".")
unittest.TextTestRunner().run(suite)

@ -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)

@ -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)
Loading…
Cancel
Save