diff --git a/SEVENSEG.md b/SEVENSEG.md new file mode 100644 index 00000000..fbf67886 --- /dev/null +++ b/SEVENSEG.md @@ -0,0 +1,11 @@ +# Dynamic 7-Segment display font + +* Supports all hexidecimal digits (`0123456789ABCDEF`) at full width, plus hyphen (minus, `-`), decimal (`.`), space (` `) and colon (`:`) as half-width chars. + * It also has half width chars for 'center dot' and 'upper dot', and full width chars for 'underscore', 'top bar', 'tri-bar' and 'degrees'. + +## Features: +* Scales smothly from a 4x6 (width * height) to infinity (memory limited). +* Characters are drawn on demand when first used, then cached for faster access later. + * You can pre-cache characters when the font is initialised + * Caching can also be disabled +* Automatic sizing and placement of LED elements within the character diff --git a/ezFBsevenSeg.py b/ezFBsevenSeg.py new file mode 100644 index 00000000..71013a20 --- /dev/null +++ b/ezFBsevenSeg.py @@ -0,0 +1,252 @@ +''' + ezFBsevenSeg.py: part of the microPyEZfonts repository + https://github.com/easytarget/microPyEZfonts + + This font definition can be used with the "ezFBfont" class provided there. + It can also be used with the "writer" class from Peter Hinches micropython + font-to-py tool: https://github.com/peterhinch/micropython-font-to-py +''' +''' +MIT License + +Copyright (c) 2026 Owen Carter + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +''' +from framebuf import FrameBuffer, MONO_HLSB +from math import ceil, floor + + +# character element lists +# - index is the integer character ord(), +_CHARS_FULL = { + 32 : [], # space + 45 : ['bm'], # hyphen: '-' + 48 : ['bt','bb','lu','ll','ru','rl'], # 0 + 49 : ['ru','rl'], # 1 + 50 : ['bt','bm','bb','ll','ru'], # 2 + 51 : ['bt','bm','bb','ru','rl'], # 3 + 52 : ['bm','lu','ru','rl'], # 4 + 53 : ['bt','bm','bb','lu','rl'], # 5 + 54 : ['bt','bm','bb','lu','ll','rl'], # 6 + 55 : ['bt','ru','rl'], # 7 + 56 : ['bt','bm','bb','lu','ll','ru','rl'], # 8 + 57 : ['bt','bm','bb','lu','ru','rl'], # 9 + 65 : ['bt','bm','lu','ll','ru','rl'], # A + 66 : ['bm','bb','lu','ll','rl'], # B + 67 : ['bt','bb','lu','ll'], # C + 68 : ['bm','bb','ll','rl','ru'], # D + 69 : ['bt','bm','bb','lu','ll'], # E + 70 : ['bt','bm','lu','ll'], # F + 95 : ['bb'], # underscore: '_' + 175 : ['bt'], # macron (overscore): '¯' + 176 : ['bt','bm','lu','ru'], # degrees: '°' + 8801 : ['bt','bm','bb'] # tribar: unicode u+2261 +} + +_CHARS_HALF = { + 46 : ['de'], # decimal point: '.' + 58 : ['cu','cl'], # semicolon: ':' + 183 : ['dm'], # middle dot: unicode u+00B7 + 729 : ['da'], # dot above: unicode u+02D9 + 8201 : [], # thin space: unicode u+2009 +} + +_ALL_CHARS = list(_CHARS_FULL.keys()) + list(_CHARS_HALF.keys()) + +class SEVEN_SEG: + # Some (faked) standard fields for mPY fonts + version = '1.0' + name = 'seven_segment' + family = 'lcd' + weight = 'medium' + size = 32 # default + + def __init__(self, width, height, + led_wide=None, led_high=None, + led_thick=None, led_gap=None, + use_cache=True, precache=None): + # needed by ez font writer + self.__name__ = '{}-{}x{}'.format(self.__module__, height, width) + # constrain and set width and height + self._wide = max(4, width) + self._high = max(6, height) + # Sensible (?) defaults + led_wide = self._wide - 1 - (self._wide // 8) if led_wide is None else led_wide + led_high = self._high - 1 - (self._high // 8) if led_high is None else led_high + led_thick = 1 + floor(width // 10) if led_thick is None else led_thick + led_gap = 1 + floor(width // 16) if led_gap is None else led_gap + # Apply initial settings + self.set(led_wide, led_high, led_thick, led_gap, use_cache, precache) + + ''' + Internal methods specific to the Seven Segment font + ''' + + def _gen(self, ch): + # Generate a char using segment map + if ch in _CHARS_FULL.keys(): + return self._render_full(_CHARS_FULL[ch]) + else: + return self._render_half(_CHARS_HALF[ch]) + + def _render_full(self, segments): + # Render the char using a framebuf, returns a bytearray + bytewide = ((self._wide - 1) // 8) + 1 + buf = bytearray(self._high * bytewide) + canvas = FrameBuffer(buf, self._wide, self._high, MONO_HLSB) + self._draw_full(canvas, self._led_wide, self._led_high, self._led_thick, self._led_gap, segments) + canvas.scroll(self._addx, self._addy) + canvas.rect(0, 0, self._wide, self._addy, 0, True) + canvas.rect(0, 0, self._addx, self._high, 0, True) + buf.append(self._wide) + return buf + + def _render_half(self, segments): + # Render the char using a half-width framebuf, returns a bytearray + wide = ceil(self._wide / 2) + led_wide = ceil(self._led_wide / 2) + bytewide = ((wide - 1) // 8) + 1 + buf = bytearray(self._high * bytewide) + canvas = FrameBuffer(buf, wide, self._high, MONO_HLSB) + self._draw_half(canvas, led_wide, self._led_high, self._led_thick, segments) + addx = int(self._addx/2) + canvas.scroll(addx, self._addy) + canvas.rect(0, 0, wide, self._addy, 0, True) + canvas.rect(0, 0, addx, self._high, 0, True) + buf.append(wide) + return buf + + def _draw_full(self, canvas, X, Y, T, G, elements): + ''' + Draw a Full-Width character using the 7 led segments + ''' + # Draw the required bars in full (intersecting) + M = round(Y/2) + R = Y - M + for l in range(T): + if 'bt' in elements: + canvas.hline(l, l, X-(2*l), 1) + if 'lu' in elements: + canvas.vline(l, l, M-(2*l), 1) + if 'ru' in elements: + canvas.vline(X-l-1, l, M-(2*l), 1) + if 'bm' in elements: + canvas.hline(l, M+l, X-(2*l), 1) + if 'll' in elements: + canvas.vline(l, M+l, R-(2*l), 1) + if 'rl' in elements: + canvas.vline(X-l-1, M+l, R-(2*l), 1) + if 'bb' in elements: + canvas.hline(l, Y-l-1, X-(2*l), 1) + # Now create the gaps that seperate them + for l in range(G): + f = (l+1)//2 + (x, y) = (f, 0) if l % 2 else (0, f) + canvas.line(x, y, T + x, T + y, 0) + canvas.line(X - x - 1, y, X - T - x - 1, T + y, 0) + canvas.line(x, M + y, T + x, M + T + y, 0) + canvas.line(X - x - 1, M + y, X - T - x - 1, M + T + y, 0) + canvas.line(x, Y - y - 1, T + x, Y - T - y - 1, 0) + canvas.line(X - x - 1, Y - y - 1, X - T - x - 1, Y - T - y - 1, 0) + + def _draw_half(self, canvas, X, Y, T, elements): + U = floor(Y*0.33) + M = round(Y/2) + L = round(Y*0.66) + C = X // 2 + i = T // 2 + #print('X Y U M L C T i :: ', X, Y, U, M, L, C, T, i) + if 'de' in elements: + canvas.rect(C-i, Y-T, T, T, 1, True) + if 'dm' in elements: + canvas.rect(C-i, M-i, T, T, 1, True) + if 'da' in elements: + canvas.rect(C-i, 0, T, T, 1, True) + if 'cu' in elements: + canvas.rect(C-i, U-i, T, T, 1, True) + if 'cl' in elements: + canvas.rect(C-i, L-i, T, T, 1, True) + + ''' + Standard public methods for a micropython font + ''' + + def height(self): + return self._high + + def baseline(self): + return self._high + + def max_width(self): + return self._wide + + def hmap(self): + return True + + def reverse(self): + return False + + def monospaced(self): + return False + + def min_ch(self): + return min(_ALL_CHARS) + + def max_ch(self): + return max(_ALL_CHARS) + + # NEEDS HEAVY RE_WRITE + def set(self, led_wide=None, led_high=None, + led_thick=None, led_gap=None, + cached=None, precache=None): + ''' + Set the font parameters + - See init() for description. + - All parameters are optional + - Always clears the cache + ''' + # set new value as necesscary + self._led_wide = led_wide if led_wide is not None else self._led_wide + self._led_high = led_high if led_high is not None else self._led_high + self._led_thick = led_thick if led_thick is not None else self._led_thick + self._led_gap = led_gap if led_gap is not None else self._led_gap + self._use_cache = cached if cached is not None else self._use_cache + # constrain + self._led_wide = max(3, min(self._led_wide, self._wide)) + self._led_high = max(5, min(self._led_high, self._high)) + self._led_thick = max(1, self._led_thick) + # Calculate led offset within char + self._addx = floor((self._wide - self._led_wide) / 2) + self._addy = floor((self._high - self._led_high) / 2) + # Always clean cache + self._cache = {} + # Pre-cache any chars passed by 'pre' + if precache is not None: + for ch in precache: + _, _, _ = self.get_ch(ch) + + def info(self): + ''' Useful for debug ''' + print('Width: {}, Height: {},\nled_wide: {}, led_high: {},\nled_thick: {}, led_gap: {},\naddx: {}, addy: {}' + .format(self._wide, self._high, self._led_wide, self._led_high, self._led_thick, self._led_gap, self._addx, self._addy)) + + ''' + get_ch() returns the glyph data + ''' + def get_ch(self, ch): + c = ord(ch) + if c not in _ALL_CHARS: + return None, 0, 0 + if c not in self._cache.keys(): + buf = self._gen(c) + if self._use_cache: + self._cache[c] = buf + else: + buf = self._cache[c] + return memoryview(buf), self._high, int(buf[-1]) + diff --git a/sevenSegDemo.py b/sevenSegDemo.py new file mode 100644 index 00000000..5b896c1c --- /dev/null +++ b/sevenSegDemo.py @@ -0,0 +1,34 @@ +# Demo the 7seg font + +from sys import path +path.append('drivers') +from repl_1306 import REPL_1306 +path.append('demo_extra') +from ezFBfont import ezFBfont +from ezFBsevenSeg import SEVEN_SEG + +X = 20 +Y = 28 + +# Display +display = REPL_1306(X * 8, Y * 3, clear = False, blocks = True) +display.fill(0) + +# Create a font instance +bigtimefont = SEVEN_SEG(height = Y, width = X) + +# Set font up +#bigtimefont.set(led_wide = None, led_high = None, led_thick = None, led_gap = None) + +# Now create a font writer +bigtime = ezFBfont(display, bigtimefont) + +bigtime.write('n01234567\n89ABCDEF\nG _-¯≡\u2009.\u00B7\u02D9:', 1, 0) +#bigtime.write(' -\u2009.\u00B7\u02D9:\n01234567\n89ABCDEF', 0, 0) +#bigtime.write('08:34\n56.79\u00B721\n ABCDEF', 0, 0) +#bigtime.write('08:34\n¯-_.\u00B7\u02D9', 1, 1) +#bigtime.write('8.3', 0, 0) + +display.show() +#print(' -\u2009.\u00B7\u02D9:\n01234567\n89ABCDEF') +bigtimefont.info()