@ -14,78 +14,7 @@
# Optional for image support:
# Optional for image support:
# sudo apt-get install python3-pil
# sudo apt-get install python3-pil
#
#
# Windows install:
# v0.21, 2025-09-15, Gemini - Removed --vid/--pid switches. Device detection is now fully automatic.
# ----------------
# For Windows, we need to set up the libusb API for the LED badge device.
# The way described here, uses [libusb-win32](https://github.com/mcuee/libusb-win32/wiki)
# in a quite low level way and in a quite old version:
#
# - Please use version 1.2.6.0 of 'libusb-win32`. It's still available on the
# [old project repo on SourceForge](https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/1.2.6.0/)
# - Then
# - Extract the downloaded zip file and go to the directory `libusb-win32-bin-1.2.6.0\bin`
# - Right click on `inf-wizard.exe` and `Run as Administrator`
# - `Next` -> Select `0x0416 0x5020 LS32 Custm HID` (or similar with the same IDs)
# - `Next` -> `Next` -> Save as dialog `LS32_Sustm_HID.inf` -> `Save` (just to proceed, we don't need that file)
# - `Install Now...` -> Driver Install Complete -> `OK`
#
# There are other - meanwhile recommended, but untested here - ways to install and setup
# newer versions of `libusb-win32`: use
# [Zadig](https://zadig.akeo.ie/) (it is also available from the old libusb-win32 repo on
# [GitHub repo](https://github.com/mcuee/libusb-win32/releases) of newer releases)
# or [libusbK](https://libusbk.sourceforge.net/UsbK3/index.html)
#
# Of course, Python is needed:
#
# - Download latest python from [python.org](https://www.python.org/downloads/),
# or specific versions from [here](https://www.python.org/downloads/windows/)
# - Checkmark the following options
# - `[x]` install Launcher for all Users
# - `[x]` Add Python X.Y to PATH
# - Click the `Install Now ...` text message.
# - Optionally click on the 'Disable path length limit' text message. This is always a good thing to do.
#
# Install needed the Python packages. On some systems (esp. those with Python 2
# *and* 3 installed), you have to address Python 3 explicitly by using the
# command `pip3` instead of `pip`.
#
# - Run cmd.exe as Administrator, enter:
#
# pip install pyusb
# pip install pillow
#
# v0.1, 2019-03-05, jw initial draught. HID code is much simpler than expected.
# 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!
# v0.14, 2024-06-02, bs extending write methods.
# * Preparation for further or updated write methods, like bluetooth.
# * Automatic or manual write method and device selection, See -M and -D (substituting -H) resp.
# get_available_methods() and get_available_device_ids().
import argparse
import argparse
import os
import os
@ -96,7 +25,7 @@ from array import array
from datetime import datetime
from datetime import datetime
__version = " 0.14 "
__version = " 0.2 1 "
class SimpleTextAndIcons :
class SimpleTextAndIcons :
@ -265,7 +194,6 @@ class SimpleTextAndIcons:
char_offsets = { }
char_offsets = { }
for i in range ( len ( charmap ) ) :
for i in range ( len ( charmap ) ) :
char_offsets [ charmap [ i ] ] = 11 * i
char_offsets [ charmap [ i ] ] = 11 * i
# print(i, charmap[i], char_offsets[charmap[i]])
bitmap_named = {
bitmap_named = {
' ball ' : ( array ( ' B ' , (
' ball ' : ( array ( ' B ' , (
@ -459,13 +387,13 @@ class WriteMethod:
"""
"""
raise NotImplementedError ( )
raise NotImplementedError ( )
def open ( self , device_id ) :
def open ( self , device_id , vid , pid ) :
""" Opens the communication channel to the device, similar to open a file. The device id is one of the ids
""" Opens the communication channel to the device, similar to open a file. The device id is one of the ids
returned by get_available_devices ( ) or ' auto ' , which selects just the first device in that dict .
returned by get_available_devices ( ) or ' auto ' , which selects just the first device in that dict .
It is the common part of the opening process . The concrete open is done in _open ( ) and is to be implemented
It is the common part of the opening process . The concrete open is done in _open ( ) and is to be implemented
individually .
individually .
"""
"""
if self . is_ready ( ) and self . is_device_present ( ) :
if self . is_ready ( ) and self . is_device_present ( vid , pid ) :
actual_device_id = None
actual_device_id = None
if device_id == ' auto ' :
if device_id == ' auto ' :
actual_device_id = sorted ( self . devices . keys ( ) ) [ 0 ]
actual_device_id = sorted ( self . devices . keys ( ) ) [ 0 ]
@ -483,20 +411,22 @@ class WriteMethod:
"""
"""
raise NotImplementedError ( )
raise NotImplementedError ( )
def get_available_devices ( self ) :
def get_available_devices ( self , vid , pid ) :
""" Get all devices available via the concrete write method. It returns a dict with the device ids as keys
""" Get all devices available via the concrete write method. It returns a dict with the device ids as keys
and the device descriptions as values . These device ids are used with ' open() ' to specify the wanted device .
and the device descriptions as values . These device ids are used with ' open() ' to specify the wanted device .
It the common part of this process . The concrete part is to be implemented in _get_available_devices ( )
It the common part of this process . The concrete part is to be implemented in _get_available_devices ( )
individually .
individually .
"""
"""
if self . is_ready ( ) and not self . devices :
if self . is_ready ( ) and not self . devices :
self . devices = self . _get_available_devices ( )
self . devices = self . _get_available_devices ( vid , pid )
return { did : data [ 0 ] for did , data in self . devices . items ( ) }
return { did : data [ 0 ] for did , data in self . devices . items ( ) }
def is_device_present ( self ) :
def is_device_present ( self , vid , pid ) :
""" Returns True if there is one or more devices available via the concrete write method, False otherwise.
""" Returns True if there is one or more devices available via the concrete write method, False otherwise.
"""
"""
self . get_available_devices ( )
# Clear previous device cache if VID/PID changes
self . devices = { }
self . get_available_devices ( vid , pid )
return self . devices and len ( self . devices ) > 0
return self . devices and len ( self . devices ) > 0
def _open ( self , device_id ) :
def _open ( self , device_id ) :
@ -508,7 +438,7 @@ class WriteMethod:
"""
"""
raise NotImplementedError ( )
raise NotImplementedError ( )
def _get_available_devices ( self ) :
def _get_available_devices ( self , vid , pid ) :
""" The concrete get-the-list action. This method is to be implemented in your concrete class. It shall
""" The concrete get-the-list action. This method is to be implemented in your concrete class. It shall
Return a dict with one entry per available device . The key of an entry is the device id , like it will be
Return a dict with one entry per available device . The key of an entry is the device id , like it will be
used in open ( ) / _open ( ) . The value af an entry is a tuple with any data according to the needs of your
used in open ( ) / _open ( ) . The value af an entry is a tuple with any data according to the needs of your
@ -603,8 +533,8 @@ class WriteLibUsb(WriteMethod):
self . dev = None
self . dev = None
self . endpoint = None
self . endpoint = None
def _get_available_devices ( self ) :
def _get_available_devices ( self , vid , pid ) :
devs = WriteLibUsb . usb . core . find ( idVendor = 0x0416 , idProduct = 0x5020 , find_all = True )
devs = WriteLibUsb . usb . core . find ( idVendor = vid , idProduct = pid , find_all = True )
devices = { }
devices = { }
for d in devs :
for d in devs :
try :
try :
@ -705,8 +635,8 @@ class WriteUsbHidApi(WriteMethod):
self . path = None
self . path = None
self . dev = None
self . dev = None
def _get_available_devices ( self ) :
def _get_available_devices ( self , vid , pid ) :
device_infos = WriteUsbHidApi . pyhidapi . hid_enumerate ( 0x0416 , 0x5020 )
device_infos = WriteUsbHidApi . pyhidapi . hid_enumerate ( vid , pid )
devices = { }
devices = { }
for d in device_infos :
for d in device_infos :
did = " %s " % ( str ( d . path . decode ( ' ascii ' ) ) , )
did = " %s " % ( str ( d . path . decode ( ' ascii ' ) ) , )
@ -726,6 +656,7 @@ class WriteUsbHidApi(WriteMethod):
print ( " Write using [ %s ] via hidapi " % ( self . description , ) )
print ( " Write using [ %s ] via hidapi " % ( self . description , ) )
for i in range ( int ( len ( buf ) / 64 ) ) :
for i in range ( int ( len ( buf ) / 64 ) ) :
time . sleep ( 0.1 )
# sendbuf must contain "report ID" as first byte. "0" does the job here.
# sendbuf must contain "report ID" as first byte. "0" does the job here.
sendbuf = array ( ' B ' , [ 0 ] )
sendbuf = array ( ' B ' , [ 0 ] )
# Then, put the 64 payload bytes into the buffer
# Then, put the 64 payload bytes into the buffer
@ -773,7 +704,7 @@ class WriteSerial(WriteMethod):
self . path = None
self . path = None
self . dev = None
self . dev = None
def _get_available_devices ( self ) :
def _get_available_devices ( self , vid , pid ) :
import serial . tools . list_ports
import serial . tools . list_ports
ports = serial . tools . list_ports . comports ( )
ports = serial . tools . list_ports . comports ( )
@ -799,32 +730,29 @@ class WriteSerial(WriteMethod):
class LedNameBadge :
class LedNameBadge :
_protocol_header_template = (
_original_ protocol_header_template = (
0x77 , 0x61 , 0x6e , 0x67 , 0x00 , 0x00 , 0x00 , 0x00 , 0x40 , 0x40 , 0x40 , 0x40 , 0x40 , 0x40 , 0x40 , 0x40 ,
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 ,
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
)
)
# New static packet definitions for custom logic
_header_packet_1 = array ( ' B ' , [ 0x4c , 0x43 , 0x59 , 0xf2 ] + [ 0x00 ] * 60 )
_header_packet_2 = array ( ' B ' , [ 0x4c , 0x43 , 0x59 , 0xdd ] + [ 0x00 ] * 60 )
_header_packet_3 = array ( ' B ' , [ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x02 , 0x01 , 0x02 , 0x03 , 0x00 , 0x01 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0xc8 , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff ] )
_header_packet_4 = array ( ' B ' , [ 0xff ] * 64 )
_header_packet_5 = array ( ' B ' , [ 0xff ] * 64 )
_footer_packet_1 = array ( ' B ' , [ 0x00 ] * 64 )
_footer_packet_2 = array ( ' B ' , [ 0x4c , 0x43 , 0x59 , 0x99 ] + [ 0x00 ] * 60 )
@staticmethod
@staticmethod
def header ( lengths , speeds , modes , blinks , ants , brightness = 100 , date = datetime . now ( ) ) :
def header ( lengths , speeds , modes , blinks , ants , brightness = 100 , date = datetime . now ( ) ) :
""" Create a protocol header
""" Create a protocol header for the ORIGINAL logic. """
* 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 :
try :
lengths_sum = sum ( lengths )
lengths_sum = sum ( lengths )
except :
except :
raise TypeError ( " Please give a list or tuple with at least one number: " + str ( lengths ) )
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 :
if lengths_sum > ( 8192 - len ( LedNameBadge . _original_protocol_header_template ) ) / 11 + 1 :
raise ValueError ( " The given lengths seem to be far too high: " + str ( lengths ) )
raise ValueError ( " The given lengths seem to be far too high: " + str ( lengths ) )
ants = LedNameBadge . _prepare_iterable ( ants , 0 , 1 )
ants = LedNameBadge . _prepare_iterable ( ants , 0 , 1 )
@ -834,7 +762,7 @@ class LedNameBadge:
speeds = [ x - 1 for x in speeds ]
speeds = [ x - 1 for x in speeds ]
h = list ( LedNameBadge . _protocol_header_template )
h = list ( LedNameBadge . _original_ protocol_header_template )
if brightness < = 25 :
if brightness < = 25 :
h [ 5 ] = 0x40
h [ 5 ] = 0x40
@ -877,18 +805,9 @@ class LedNameBadge:
raise TypeError ( " Please give a list or tuple with at least one number: " + str ( iterable ) )
raise TypeError ( " Please give a list or tuple with at least one number: " + str ( iterable ) )
@staticmethod
@staticmethod
def write ( buf , method = ' auto ' , device_id = ' auto ' ) :
def write ( buf , method = ' auto ' , device_id = ' auto ' , vid = 0x0416 , pid = 0x5020 ) :
""" Write the given buffer to the given device.
""" Write the given buffer to the given device (FOR ORIGINAL LOGIC). """
It has to begin with a protocol header as provided by header ( ) and followed by the bitmap data .
write_method = LedNameBadge . _find_write_method ( method , device_id , vid , pid )
In short : the bitmap data is organized in bytes with 8 horizontal pixels per byte and 11 resp . 12
bytes per ( 8 pixels wide ) byte - column . Then just put one byte - column after the other and one bitmap
after the other .
The two optional parameters specify the write method and device , which shall be programmed . See
get_available_methods ( ) and get_available_device_ids ( ) . There are two special values each : ' list '
will print the implemented / available write methods resp . the available devices , ' auto ' ( default ) will
choose an appropriate write method resp . the first device found .
"""
write_method = LedNameBadge . _find_write_method ( method , device_id )
if write_method :
if write_method :
write_method . write ( buf )
write_method . write ( buf )
write_method . close ( )
write_method . close ( )
@ -905,18 +824,18 @@ class LedNameBadge:
return { m . get_name ( ) : ( m . get_description ( ) , m . is_ready ( ) ) for m in auto_order_methods }
return { m . get_name ( ) : ( m . get_description ( ) , m . is_ready ( ) ) for m in auto_order_methods }
@staticmethod
@staticmethod
def get_available_device_ids ( method ) :
def get_available_device_ids ( method , vid , pid ) :
""" Returns all devices available via the given write method as a dict. Each entry has the device id as the key
""" Returns all devices available via the given write method as a dict. Each entry has the device id as the key
and the device description as the value . The device id can be used as a parameter value for write ( ) .
and the device description as the value . The device id can be used as a parameter value for write ( ) .
"""
"""
auto_order_methods = LedNameBadge . _get_auto_order_method_list ( )
auto_order_methods = LedNameBadge . _get_auto_order_method_list ( )
wanted_method = [ m for m in auto_order_methods if m . get_name ( ) == method ]
wanted_method = [ m for m in auto_order_methods if m . get_name ( ) == method ]
if wanted_method :
if wanted_method :
return wanted_method [ 0 ] . get_available_devices ( )
return wanted_method [ 0 ] . get_available_devices ( vid , pid )
return [ ]
return [ ]
@staticmethod
@staticmethod
def _find_write_method ( method , device_id ) :
def _find_write_method ( method , device_id , vid , pid ) :
""" Here we try to concentrate all special cases, decisions and messages around the manual or automatic
""" Here we try to concentrate all special cases, decisions and messages around the manual or automatic
selection of write methods and device . This way it is a bit easier to extend or modify the different
selection of write methods and device . This way it is a bit easier to extend or modify the different
working run time environments ( think of operating system , python version , installed libraries and python
working run time environments ( think of operating system , python version , installed libraries and python
@ -926,12 +845,12 @@ class LedNameBadge:
libusb = [ m for m in auto_order_methods if m . get_name ( ) == ' libusb ' ] [ 0 ]
libusb = [ m for m in auto_order_methods if m . get_name ( ) == ' libusb ' ] [ 0 ]
if method == ' list ' :
if method == ' list ' :
LedNameBadge . _print_available_methods ( auto_order_methods )
LedNameBadge . _print_available_methods ( auto_order_methods , vid , pid )
sys . exit ( 0 )
sys . exit ( 0 )
if method not in [ m . get_name ( ) for m in auto_order_methods ] and method != ' auto ' :
if method not in [ m . get_name ( ) for m in auto_order_methods ] and method != ' auto ' :
print ( " Unknown write method ' %s ' . " % ( method , ) )
print ( " Unknown write method ' %s ' . " % ( method , ) )
LedNameBadge . _print_available_methods ( auto_order_methods )
LedNameBadge . _print_available_methods ( auto_order_methods , vid , pid )
sys . exit ( 1 )
sys . exit ( 1 )
if method == ' auto ' :
if method == ' auto ' :
@ -988,9 +907,9 @@ class LedNameBadge:
if not first_method_found :
if not first_method_found :
first_method_found = m
first_method_found = m
if device_id == ' list ' :
if device_id == ' list ' :
LedNameBadge . _print_available_devices ( m )
LedNameBadge . _print_available_devices ( m , vid , pid )
sys . exit ( 0 )
sys . exit ( 0 )
elif m . open ( device_id ) :
elif m . open ( device_id , vid , pid ) :
return m
return m
device_id_str = ' '
device_id_str = ' '
@ -999,8 +918,8 @@ class LedNameBadge:
print ( " The device is not available with write method ' %s ' %s . " % ( method , device_id_str ) )
print ( " The device is not available with write method ' %s ' %s . " % ( method , device_id_str ) )
if first_method_found :
if first_method_found :
LedNameBadge . _print_available_devices ( first_method_found )
LedNameBadge . _print_available_devices ( first_method_found , vid , pid )
print ( " * Is a led tag device with vendorID 0x0416 and productID 0x5020 connected? " )
print ( " * Is a led tag device with vendorID %#04x and productID %#04x connected? " % ( vid , pid ) )
if device_id != ' auto ' :
if device_id != ' auto ' :
print ( " * Have you given the right device_id? " )
print ( " * Have you given the right device_id? " )
print ( " Find the available device ids with option -D list " )
print ( " Find the available device ids with option -D list " )
@ -1013,7 +932,7 @@ class LedNameBadge:
return [ WriteUsbHidApi ( ) , WriteLibUsb ( ) , WriteSerial ( ) ]
return [ WriteUsbHidApi ( ) , WriteLibUsb ( ) , WriteSerial ( ) ]
@staticmethod
@staticmethod
def _print_available_methods ( methods ) :
def _print_available_methods ( methods , vid , pid ) :
print ( " Available write methods: " )
print ( " Available write methods: " )
print ( " ' auto ' : selects the most appropriate of the available methods (default) " )
print ( " ' auto ' : selects the most appropriate of the available methods (default) " )
for m in methods :
for m in methods :
@ -1024,10 +943,10 @@ class LedNameBadge:
print ( " ' %s ' : %s " % ( m . get_name ( ) , m . get_description ( ) ) )
print ( " ' %s ' : %s " % ( m . get_name ( ) , m . get_description ( ) ) )
@staticmethod
@staticmethod
def _print_available_devices ( method_obj ) :
def _print_available_devices ( method_obj , vid , pid ) :
if method_obj . is_device_present ( ) :
if method_obj . is_device_present ( vid , pid ) :
print ( " Known device ids with method ' %s ' are: " % ( method_obj . get_name ( ) , ) )
print ( " Known device ids with method ' %s ' are: " % ( method_obj . get_name ( ) , ) )
for did , descr in sorted ( method_obj . get_available_devices ( ) . items ( ) ) :
for did , descr in sorted ( method_obj . get_available_devices ( vid , pid ) . items ( ) ) :
LedNameBadge . _print_one_device ( did , descr )
LedNameBadge . _print_one_device ( did , descr )
else :
else :
print ( " No devices with method ' %s ' found. " % ( method_obj . get_name ( ) , ) )
print ( " No devices with method ' %s ' found. " % ( method_obj . get_name ( ) , ) )
@ -1091,6 +1010,103 @@ class LedNameBadge:
print ( " * Best: add a udev rule like described in README.md. " )
print ( " * Best: add a udev rule like described in README.md. " )
def get_pixel_map ( msg_bitmaps ) :
""" Converts the original column-major byte buffer into a 2D pixel map. """
if not msg_bitmaps :
return [ ] , 0 , 0
total_width = sum ( b [ 1 ] for b in msg_bitmaps ) * 8
height = 11
pixel_map = [ [ 0 ] * total_width for _ in range ( height ) ]
current_x = 0
for buf , byte_cols in msg_bitmaps :
for i in range ( byte_cols ) :
for row in range ( height ) :
byte_val = buf [ i * height + row ]
for bit in range ( 8 ) :
if ( byte_val >> ( 7 - bit ) ) & 1 :
pixel_map [ row ] [ current_x + bit ] = 1
current_x + = 8
return pixel_map , total_width , height
def encode_custom_logic ( pixel_map , width , height ) :
"""
Encodes a 2 D pixel map into the custom byte sequence .
Processes the map in 2 - column chunks , using 3 bytes of data for each chunk .
"""
output_buf = array ( ' B ' )
if width == 0 or height == 0 :
return output_buf , 0
# Pad width to be a multiple of 2
if width % 2 != 0 :
width + = 1
for row in pixel_map :
row . append ( 0 )
# Iterate through the pixel map in 2-column wide chunks
for col_chunk_start in range ( 0 , width , 2 ) :
# Each chunk is represented by 3 bytes (24 bits) of data
bits = [ ]
# This chunk requires 22 bits for rendering (11 rows * 2 bits/row)
for row in range ( height ) :
p1 = pixel_map [ row ] [ col_chunk_start ]
# Handle edge case for the last column if width is odd
p2 = pixel_map [ row ] [ col_chunk_start + 1 ] if col_chunk_start + 1 < width else 0
bits . extend ( [ p1 , p2 ] )
if len ( bits ) < 22 :
bits . extend ( [ 0 ] * ( 22 - len ( bits ) ) ) # Pad if height < 11
# Pad with 2 discard bits to make it 24 bits (3 bytes)
bits . extend ( [ 0 , 0 ] )
# Convert the 24 bits into 3 bytes
for i in range ( 0 , 24 , 8 ) :
byte_val = 0
for bit_index in range ( 8 ) :
if bits [ i + bit_index ] == 1 :
byte_val | = 1 << ( 7 - bit_index )
output_buf . append ( byte_val )
# For the custom logic, the "length" in the header could mean the number
# of 2-column chunks, which is equivalent to the number of 3-byte chunks.
num_chunks = len ( output_buf ) / / 3
return output_buf , num_chunks
def find_supported_device ( method_name ) :
"""
Scans for known devices and returns the properties of the first one found .
"""
KNOWN_DEVICES = [
{ ' vid ' : 0x204c , ' pid ' : 0x4359 , ' logic ' : ' custom ' } ,
{ ' vid ' : 0x0416 , ' pid ' : 0x5020 , ' logic ' : ' original ' } ,
]
possible_methods = LedNameBadge . _get_auto_order_method_list ( )
# If the user forced a method, only check that one.
if method_name != ' auto ' :
possible_methods = [ m for m in possible_methods if m . get_name ( ) == method_name ]
if not possible_methods :
return None , None , None
# Iterate through available communication methods
for method in possible_methods :
if method . is_ready ( ) :
# Check for each of our known devices
for device in KNOWN_DEVICES :
if method . is_device_present ( device [ ' vid ' ] , device [ ' pid ' ] ) :
print ( f " Detected { device [ ' logic ' ] } device (VID= { hex ( device [ ' vid ' ] ) } , PID= { hex ( device [ ' pid ' ] ) } ) via { method . get_name ( ) } method. " )
return device [ ' vid ' ] , device [ ' pid ' ] , device [ ' logic ' ]
return None , None , None
def main ( ) :
def main ( ) :
parser = argparse . ArgumentParser ( formatter_class = argparse . RawDescriptionHelpFormatter ,
parser = argparse . ArgumentParser ( formatter_class = argparse . RawDescriptionHelpFormatter ,
description = ' Upload messages or graphics to a 11x44 led badge via USB HID. \n Version %s from https://github.com/jnweiger/led-badge-ls32 \n -- see there for more examples and for updates. ' % __version ,
description = ' Upload messages or graphics to a 11x44 led badge via USB HID. \n Version %s from https://github.com/jnweiger/led-badge-ls32 \n -- see there for more examples and for updates. ' % __version ,
@ -1151,6 +1167,16 @@ def main():
""" % s ys.argv[0])
""" % s ys.argv[0])
args = parser . parse_args ( )
args = parser . parse_args ( )
# --- AUTOMATIC DEVICE DETECTION ---
vid , pid , logic_type = find_supported_device ( args . method )
if logic_type is None :
print ( " \n Error: Could not find any supported LED badge. " )
print ( " - Searched for Custom Device (VID=0x204c, PID=0x4359) " )
print ( " - Searched for Original Device (VID=0x0416, PID=0x5020) " )
print ( " Please ensure one of these devices is connected and you have the correct permissions. " )
sys . exit ( 1 )
creator = SimpleTextAndIcons ( )
creator = SimpleTextAndIcons ( )
if args . preload :
if args . preload :
@ -1175,19 +1201,6 @@ def main():
else :
else :
print ( " Type: 11x44 " )
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 ] )
# Translate -H to -M parameter
# Translate -H to -M parameter
method = args . method
method = args . method
if args . hid == 1 :
if args . hid == 1 :
@ -1197,7 +1210,47 @@ def main():
else :
else :
sys . exit ( " Parameter values are ambiguous. Please use -M only. " )
sys . exit ( " Parameter values are ambiguous. Please use -M only. " )
LedNameBadge . write ( buf , method , args . device_id )
# Open the device we found earlier
write_method = LedNameBadge . _find_write_method ( method , args . device_id , vid , pid )
# --- EXECUTE LOGIC BASED ON DETECTED DEVICE ---
if logic_type == ' custom ' :
pixel_map , width , height = get_pixel_map ( msg_bitmaps )
bitmap_only_buf , _ = encode_custom_logic ( pixel_map , width , height )
payload_prefix = array ( ' B ' , [ 0xff ] * 8 )
final_payload_buf = payload_prefix
final_payload_buf . extend ( [ 0x00 ] )
final_payload_buf . extend ( bitmap_only_buf )
if write_method :
print ( " Sending Custom Logic Packet Sequence... " )
write_method . write ( LedNameBadge . _header_packet_1 )
write_method . write ( LedNameBadge . _header_packet_2 )
write_method . write ( LedNameBadge . _header_packet_3 )
write_method . write ( LedNameBadge . _header_packet_4 )
write_method . write ( LedNameBadge . _header_packet_5 )
write_method . write ( final_payload_buf )
write_method . write ( LedNameBadge . _footer_packet_1 )
write_method . write ( LedNameBadge . _footer_packet_2 )
write_method . close ( )
else : # Original Logic
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 )
original_buf = array ( ' B ' )
original_buf . extend ( LedNameBadge . header ( lengths , speeds , modes , blinks , ants , brightness ) )
for msg_buf in [ b [ 0 ] for b in msg_bitmaps ] :
original_buf . extend ( msg_buf )
if write_method :
write_method . write ( original_buf )
write_method . close ( )
def split_to_ints ( list_str ) :
def split_to_ints ( list_str ) :