diff --git a/README.md b/README.md index ed6d726..dc4428e 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,25 @@ The label dimensions are 12 millimeter in height, however the printable area is +## Compatibility + +* DYMO LetraTag LT-200B + +Seems to be this is the only printer by DYMO that uses Bluetooth. If you know or own a DYMO printer that you believe needs to be supported, don't hesitate to open a new issue. + +This project depends on [`bleak`](https://pypi.org/project/bleak/) for Bluetooth communication, therefore you can only use this project on system where `bleak` is supported. Thus, either; Linux distribution with BlueZ >= 5.43 support, MacOS with at least version 10.11 or Windows build 16299 or greater is required. + ## Installation ``` python -m pip install --upgrade https://github.com/ysfchn/dymo-bluetooth/archive/refs/heads/main.zip ``` -Python 3.10 or up is targeted, but 3.9 should work too. It depends on `bleak` for Bluetooth communication, and `pillow` for importing images from files. If `python-barcode` is installed, it can be used to print barcodes. +Python 3.10 or up is targeted, but 3.9 should work too. It depends on `bleak` for Bluetooth communication, and `pillow` for importing images from files. If `python-barcode` is installed (optional), it can be used to print barcodes. ## Usage -There is a small CLI to print image files in a nearby printer. +There is a small CLI to print image files to a nearby printer. ``` python -m dymo_bluetooth --help @@ -49,7 +57,7 @@ async def main(): # Images needs to be stretched by at least 2 times like # how its mobile app does, otherwise printer will render - # the labels too thin. + # the labels too narrow. canvas = canvas.stretch_image(2) # Get a list of printers. @@ -62,7 +70,7 @@ async def main(): asyncio.run(main()) ``` -## Disclaimer +## License & Disclaimer This program is licensed under [MIT License](./LICENSE). @@ -101,7 +109,7 @@ DIRECTIVE[2] = 1B + COMMAND_TYPE[1] | Command | Character | = Code point (hex) | = Code point (decimal) | Notes | |:------:|:-------------------:|:------------------:|:------------:|:-------:| | [`START`](#start-command) | `s` | `0x73` | `115` | Start command. Each payload starts with this directive. | -| [`MEDIA_TYPE`](#media_type-command) | `M` | `0x4d` | `77` | ? | +| [`MEDIA_TYPE`](#media_type-command) | `M` | `0x4d` | `77` | | | [`PRINT_DENSITY`](#print_density-command) | `C` | `0x43` | `67` | Seems unused. | | [`PRINT_DATA`](#print_data-command) | `D` | `0x44` | `68` | Defines the image that will be printed, see below for byte format. | | [`FORM_FEED`](#form_feed-command) | `E` | `0x45` | `69` | Follows after `PRINT_DATA`. | @@ -125,7 +133,7 @@ This command follows with 1 byte containing a number of some type (?) to be set When this number has set to anything, the printer should advertise this number when searching for Bluetooth devices nearby. (not tested) So I guess it was planned to be used for some sort of casette checking & changing casette type, so printer can hold this value even when its turned off. ``` -MEDIA_TYPE[3] = DIRECTIVE[2] + ?? +MEDIA_TYPE[3] = DIRECTIVE[2] + XX ``` #### `PRINT_DENSITY` command @@ -236,7 +244,7 @@ The ninth byte of the header is the checksum of the header, and it is calculated ``` PARTIAL_HEADER[8] = FF F0 + MAGIC[2] + PAYLOAD_LENGTH[4] -CHECKSUM = SUM(PARTIAL_HEADER) & 0xFF +CHECKSUM[1] = SUM(PARTIAL_HEADER) & 0xFF HEADER[9] = PARTIAL_HEADER[8] + CHECKSUM ``` @@ -256,13 +264,13 @@ Chunking needs to be start right after [`HEADER`](#header), so only the header i Each chunk must contain its index before chunk data itself followed by. The chunk data can be smaller than 500, but it can only contain maximum 500 bytes, inclusive. In the end, you will have the full chunk to be sent over Bluetooth which contains the index and the data. ``` -CHUNK[0...501] = CHUNK_INDEX[1] + CHUNK_DATA[0...500] +CHUNK[1...501] = CHUNK_INDEX[1] + CHUNK_DATA[0...500] ``` The last chunk must contain the [`MAGIC`](#magic-value) value appended end of the chunk. ``` -CHUNK[0...503] = CHUNK_INDEX[1] + CHUNK_DATA[0...500] + MAGIC[2] +CHUNK[1...503] = CHUNK_INDEX[1] + CHUNK_DATA[0...500] + MAGIC[2] ``` #### Set media type diff --git a/dymo_bluetooth/bluetooth.py b/dymo_bluetooth/bluetooth.py index f28235e..d4f2a1e 100644 --- a/dymo_bluetooth/bluetooth.py +++ b/dymo_bluetooth/bluetooth.py @@ -41,20 +41,22 @@ PRINT_REPLY_UUID = "be3dd652-{uuid}-42f1-99c1-f0f749dd0678".format(uuid = "2b3d" UNKNOWN_UUID = "be3dd653-{uuid}-42f1-99c1-f0f749dd0678".format(uuid = "2b3d") -def is_espressif(mac : str): +def is_espressif(input_mac : str): """ - Returns True if given MAC address is in the range of Espressif Inc. + Returns True if given MAC address is from a valid DYMO printer. + The mac address blocks are owned by Espressif Inc. """ - start_block = 0x58_CF_79 << 24 - end_block = start_block + (1 << 24) # Exclusive - mac_value = int(mac.replace(":", ""), base = 16) - if (mac_value >= start_block) and (mac_value < end_block): - return True - start_block = 0xDC_54_75 << 24 - end_block = start_block + (1 << 24) # Exclusive - mac_value = int(mac.replace(":", ""), base = 16) - if (mac_value >= start_block) and (mac_value < end_block): - return True + mac_blocks = [ + "58:CF:79", + # Confirmed in pull request #2 + "DC:54:75" + ] + check_mac = int(input_mac.replace(":", ""), base = 16) + for mac in mac_blocks: + start_block = int(mac.replace(":", ""), base = 16) << 24 + end_block = start_block + (1 << 24) # Exclusive + if (check_mac >= start_block) and (check_mac < end_block): + return True return False diff --git a/dymo_bluetooth/printer.py b/dymo_bluetooth/printer.py index d1f9832..0ba004e 100644 --- a/dymo_bluetooth/printer.py +++ b/dymo_bluetooth/printer.py @@ -121,7 +121,7 @@ class Canvas: # Get the byte containing the pixel value of given coordinates. x_offset = math.ceil((x * Canvas.HEIGHT) / 8) - y_offset = 3 - math.floor(y / 8) + y_offset = ((self.HEIGHT) // 8) - 1 - math.floor(y / 8) self.buffer.seek(x_offset + y_offset) # Check if there is a bit value in given line. @@ -142,7 +142,7 @@ class Canvas: # Get the byte containing the pixel value of given coordinates. x_offset = math.ceil((x * Canvas.HEIGHT) / 8) - y_offset = 3 - math.floor(y / 8) + y_offset = ((self.HEIGHT) // 8) - 1 - math.floor(y / 8) self.buffer.seek(x_offset + y_offset) # Get the one of four slices in line in the given coordinates. Add the bit in @@ -208,7 +208,7 @@ class Canvas: """ self.buffer.seek(0, SEEK_END) cur = self.buffer.tell() - return math.ceil(cur / 4) + return math.ceil(cur / (self.HEIGHT // 8)) def get_size(self): """ @@ -222,7 +222,7 @@ class Canvas: """ self.buffer.seek(0) image = self.buffer.read() - return image + (b"\x00" * (self.buffer.tell() % 4)) + return image + (b"\x00" * (self.buffer.tell() % (self.HEIGHT // 8))) def empty(self): """ diff --git a/pyproject.toml b/pyproject.toml index 03bdc7e..a4d22b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,10 @@ authors = [{ name = "ysfchn" }] license.file = "LICENSE" readme = "README.md" classifiers = [ + "Programming Language :: Python :: 3.10", + "License :: OSI Approved :: MIT License", + "Development Status :: 5 - Production/Stable", + "Topic :: Printing", "Private :: Do Not Upload" ] dependencies = [