diff --git a/glyphs.py b/glyphs.py new file mode 100644 index 0000000..20407fd --- /dev/null +++ b/glyphs.py @@ -0,0 +1,267 @@ +"""Custom fraction glyphs for the TM-T88V receipt printer. + +Each glyph is a 12-wide x 24-tall pixel bitmap defined as a list of 24 strings. +'#' = pixel on, '.' = pixel off. + +To customize a glyph, edit its pattern below. Each row must be exactly 12 +characters wide and there must be exactly 24 rows per glyph. +""" + +# Map each fraction to a character code on the printer. +# Must be in 0x20-0x7E range and not conflict with output chars (A-Z, 0-9, '.', 's', ' ') +CHAR_MAP = { + "1/8": "a", + "1/4": "b", + "3/8": "c", + "1/2": "d", + "5/8": "e", + "3/4": "f", + "7/8": "g", +} + +# Map number of eighths (1-7) to fraction name +EIGHTHS_TO_NAME = { + 1: "1/8", + 2: "1/4", + 3: "3/8", + 4: "1/2", + 5: "5/8", + 6: "3/4", + 7: "7/8", +} + +# fmt: off +GLYPHS = { + "1/8": [ + "............", # 0 + ".........#..", # 1 ── numerator "1" + "........##..", # 2 + ".........#..", # 3 + ".........#..", # 4 + ".........#..", # 5 + ".........#..", # 6 + ".........#..", # 7 + "........#...", # 8 ── slash + ".......#....", # 9 + "......#.....", # 10 + ".....#......", # 11 + "....#.......", # 12 + "...#........", # 13 + "..#.........", # 14 + ".#..........", # 15 + "..##........", # 16 ── denominator "8" + ".#..#.......", # 17 + ".#..#.......", # 18 + "..##........", # 19 + ".#..#.......", # 20 + ".#..#.......", # 21 + "..##........", # 22 + "............", # 23 + ], + "1/4": [ + "............", # 0 + ".........#..", # 1 ── "1" + "........##..", # 2 + ".........#..", # 3 + ".........#..", # 4 + ".........#..", # 5 + ".........#..", # 6 + ".........#..", # 7 + "........#...", # 8 ── slash + ".......#....", # 9 + "......#.....", # 10 + ".....#......", # 11 + "....#.......", # 12 + "...#........", # 13 + "..#.........", # 14 + ".#..........", # 15 + "....#.......", # 16 ── "4" + "...##.......", # 17 + "..#.#.......", # 18 + ".#..#.......", # 19 + ".####.......", # 20 + "....#.......", # 21 + "....#.......", # 22 + "............", # 23 + ], + "3/8": [ + "............", # 0 + "........##..", # 1 ── "3" + ".......#..#.", # 2 + "..........#.", # 3 + "........##..", # 4 + "..........#.", # 5 + ".......#..#.", # 6 + "........##..", # 7 + "........#...", # 8 ── slash + ".......#....", # 9 + "......#.....", # 10 + ".....#......", # 11 + "....#.......", # 12 + "...#........", # 13 + "..#.........", # 14 + ".#..........", # 15 + "..##........", # 16 ── "8" + ".#..#.......", # 17 + ".#..#.......", # 18 + "..##........", # 19 + ".#..#.......", # 20 + ".#..#.......", # 21 + "..##........", # 22 + "............", # 23 + ], + "1/2": [ + "............", # 0 + ".........#..", # 1 ── "1" + "........##..", # 2 + ".........#..", # 3 + ".........#..", # 4 + ".........#..", # 5 + ".........#..", # 6 + ".........#..", # 7 + "........#...", # 8 ── slash + ".......#....", # 9 + "......#.....", # 10 + ".....#......", # 11 + "....#.......", # 12 + "...#........", # 13 + "..#.........", # 14 + ".#..........", # 15 + "..##........", # 16 ── "2" + ".#..#.......", # 17 + "....#.......", # 18 + "...#........", # 19 + "..#.........", # 20 + ".#..........", # 21 + ".####.......", # 22 + "............", # 23 + ], + "5/8": [ + "............", # 0 + ".......####.", # 1 ── "5" + ".......#....", # 2 + ".......###..", # 3 + "..........#.", # 4 + "..........#.", # 5 + ".......#..#.", # 6 + "........##..", # 7 + "........#...", # 8 ── slash + ".......#....", # 9 + "......#.....", # 10 + ".....#......", # 11 + "....#.......", # 12 + "...#........", # 13 + "..#.........", # 14 + ".#..........", # 15 + "..##........", # 16 ── "8" + ".#..#.......", # 17 + ".#..#.......", # 18 + "..##........", # 19 + ".#..#.......", # 20 + ".#..#.......", # 21 + "..##........", # 22 + "............", # 23 + ], + "3/4": [ + "............", # 0 + "........##..", # 1 ── "3" + ".......#..#.", # 2 + "..........#.", # 3 + "........##..", # 4 + "..........#.", # 5 + ".......#..#.", # 6 + "........##..", # 7 + "........#...", # 8 ── slash + ".......#....", # 9 + "......#.....", # 10 + ".....#......", # 11 + "....#.......", # 12 + "...#........", # 13 + "..#.........", # 14 + ".#..........", # 15 + "....#.......", # 16 ── "4" + "...##.......", # 17 + "..#.#.......", # 18 + ".#..#.......", # 19 + ".####.......", # 20 + "....#.......", # 21 + "....#.......", # 22 + "............", # 23 + ], + "7/8": [ + "............", # 0 + ".......####.", # 1 ── "7" + "..........#.", # 2 + ".........#..", # 3 + ".........#..", # 4 + "........#...", # 5 + "........#...", # 6 + "........#...", # 7 + "........#...", # 8 ── slash + ".......#....", # 9 + "......#.....", # 10 + ".....#......", # 11 + "....#.......", # 12 + "...#........", # 13 + "..#.........", # 14 + ".#..........", # 15 + "..##........", # 16 ── "8" + ".#..#.......", # 17 + ".#..#.......", # 18 + "..##........", # 19 + ".#..#.......", # 20 + ".#..#.......", # 21 + "..##........", # 22 + "............", # 23 + ], +} +# fmt: on + + +def glyph_to_escpos(rows): + """Convert a visual glyph (24 rows of 12-char strings) to ESC/POS column data. + + ESC/POS user-defined chars are column-major: each column is 3 bytes + (24 vertical pixels), MSB = topmost pixel. + """ + data = bytearray() + for col in range(12): + for byte_idx in range(3): + val = 0 + for bit in range(8): + row = byte_idx * 8 + bit + if rows[row][col] == "#": + val |= 0x80 >> bit + data.append(val) + return bytes(data) + + +def upload_fractions(printer): + """Upload all custom fraction glyphs to the printer via ESC &.""" + # ESC % 1: enable user-defined character set + printer._raw(b"\x1b\x25\x01") + + for name, char in CHAR_MAP.items(): + rows = GLYPHS[name] + col_data = glyph_to_escpos(rows) + code = ord(char) + # ESC & 3 c1 c2 12 [column data] + # 3 = bytes per column (24 dots), 12 = character width + printer._raw(b"\x1b\x26\x03" + bytes([code, code, 12]) + col_data) + + +def price_to_fraction(price): + """Convert a decimal price to dollar amount + fraction character. + + Rounds to the nearest eighth of a dollar, like old ticker tapes. + Returns e.g. "246" or "57a" where 'a' is the custom ⅛ glyph. + """ + dollars = int(price) + eighths = round((price - dollars) * 8) + if eighths == 8: + dollars += 1 + eighths = 0 + if eighths == 0: + return str(dollars) + name = EIGHTHS_TO_NAME[eighths] + return str(dollars) + CHAR_MAP[name] diff --git a/print_tape.py b/print_tape.py index c8ab96d..d149029 100644 --- a/print_tape.py +++ b/print_tape.py @@ -14,6 +14,8 @@ import sys from escpos.printer import Dummy +from glyphs import price_to_fraction, upload_fractions + # TM-T88V with 90° rotation: Font A chars are 24 dots wide (instead of 12), # so 512 printable dots / 24 = 21 chars fit across the 80mm paper width. LINE_WIDTH = 21 @@ -21,10 +23,11 @@ LINE_WIDTH = 21 def print_trade(p, symbol, lots, price): """Print a single trade as ticker tape characters, one per row.""" + frac = price_to_fraction(price) if lots > 1: - price_str = f"{lots}s{price:.2f}" + price_str = f"{lots}s{frac}" else: - price_str = f"{price:.2f}" + price_str = frac # Symbol characters at the top (right-aligned) for ch in symbol: @@ -49,6 +52,8 @@ def main(): # ESC 3 n: set line spacing to n/180 inch. Font A chars are 12 dots wide, # so n=12 makes rotated characters sit flush with no gap. p._raw(b"\x1b\x33\x0c") + # Upload custom fraction glyphs (⅛ ¼ ⅜ ½ ⅝ ¾ ⅞) + upload_fractions(p) with open(csv_file) as f: reader = csv.reader(f)