def __init__(self, side='L',*, mode=NORMAL, model=EMBEDDED_ARTISTS, use_flash=False, up_time=None): self.flash = None # Assume flash is unused self.in_context = False try: intside = {'l':0, 'r':1}[side.lower()] except (KeyError, AttributeError): raise ValueError("Side must be 'L' or 'R'") if model not in (EMBEDDED_ARTISTS, ADAFRUIT): raise ValueError('Unsupported model') if mode == FAST and use_flash: raise ValueError('Flash memory unavailable in fast mode') if mode == NORMAL and up_time is not None: raise ValueError('Cannot set up_time in normal mode') if mode == NORMAL: from epd import EPD self.epd = EPD(intside, model) elif mode == FAST: from epdpart import EPD self.epd = EPD(intside, model, up_time) else: raise ValueError('Unsupported mode {}'.format(mode)) self.mode = mode self.font = Font() gc.collect() self.locate(0, 0) # Text cursor: default top left self.mounted = False # umountflash() not to sync if use_flash: from flash import FlashClass self.flash = FlashClass(intside) self.umountflash() # In case mounted by prior tests. self.mountflash() gc.collect()
def __init__(self, side='L',*, mode=NORMAL, model=EMBEDDED_ARTISTS, use_flash=False, up_time=None): self.flash = None # Assume flash is unused self.in_context = False try: intside = {'l':0, 'r':1}[side.lower()] except (KeyError, AttributeError): raise ValueError("Side must be 'L' or 'R'") if model not in (EMBEDDED_ARTISTS, ADAFRUIT): raise ValueError('Unsupported model') if mode == FAST and use_flash: raise ValueError('Flash memory unavailable in fast mode') if mode == NORMAL and up_time is not None: raise ValueError('Cannot set up_time in normal mode') if mode == NORMAL: from epd import EPD self.epd = EPD(intside, model) elif mode == FAST: from epdpart import EPD self.epd = EPD(intside, model, up_time) else: raise ValueError('Unsupported mode {}'.format(mode)) self.mode = mode self.font = Font() gc.collect() self.locate(0, 0) # Text cursor: default top left self.mounted = False # umountflash() not to sync if use_flash: from flash import FlashClass gc.collect() self.flash = FlashClass(intside) self.umountflash() # In case mounted by prior tests. self.mountflash() gc.collect()
def __init__(self, debug=False): self.cfg = None self.rtc = RTC() self.debug = debug self.battery = Battery() # use this pin for debug self.wifi_pin = Pin("GP24", mode=Pin.IN, pull=Pin.PULL_UP) # Empty WDT object self.wdt = None if not debug: if not self.wifi_pin(): self.wdt = WDT(timeout=20000) self.sd = None else: from machine import SD try: self.sd = SD() mount(self.sd, '/sd') self.logfile = open("/sd/display.log", "a") except OSError: self.sd = None self.logfile = None self.epd = EPD() self.log("Time left on the alarm: %dms" % self.rtc.alarm_left()) # Don't flash when we're awake outside of debug heartbeat(self.debug)
def __init__(self, side = 'L', use_flash = False, pwr_controller = None): self.flash = None # Assume flash is unused try: self.intside = {'x':1, 'X':1, 'y':0,'Y':0, 'l':0, 'L':0, 'r':1, 'R':1}[side] except KeyError: raise ValueError("Side must be 'L' or 'R'") self.pwr_controller = pwr_controller self.epd = EPD(self.intside, pwr_controller) self.font = Font() self.locate(0, 0) # Text cursor: default top left self.mounted = False # umountflash() not to sync if use_flash: from flash import FlashClass self.flash = FlashClass(self.intside, pwr_controller) self.umountflash() # In case mounted by prior tests. if self.pwr_controller is None: # Normal operation: flash is mounted continuously self.mountflash()
def update_loop(url, interval=0, port=80): """ Download image, update display, sleep, loop. Gives up on any error :param url: :param interval: :param port: :return: """ e = EPD() e.enable() sep = url.find('/') host = url[:sep] path = url[sep:] del sep while True: print("Mem free: %d" % gc.mem_free()) c = Connect(host, port, debug=True) content = c.get_quick(path) print("Uploading...", end='') e.upload_whole_image(content) print("done.") e.display_update() del content del c if interval > 0: to_sleep = interval print("Sleeping for %ds" % interval, end='') time.sleep(to_sleep) print('.') else: input("Press enter to update (Ctrl-C to stop).") gc.collect()
class Display(object): FONT_HEADER_LENGTH = 4 def __init__(self, side='L',*, mode=NORMAL, model=EMBEDDED_ARTISTS, use_flash=False, up_time=None): self.flash = None # Assume flash is unused self.in_context = False try: intside = {'l':0, 'r':1}[side.lower()] except (KeyError, AttributeError): raise ValueError("Side must be 'L' or 'R'") if model not in (EMBEDDED_ARTISTS, ADAFRUIT): raise ValueError('Unsupported model') if mode == FAST and use_flash: raise ValueError('Flash memory unavailable in fast mode') if mode == NORMAL and up_time is not None: raise ValueError('Cannot set up_time in normal mode') if mode == NORMAL: from epd import EPD self.epd = EPD(intside, model) elif mode == FAST: from epdpart import EPD self.epd = EPD(intside, model, up_time) else: raise ValueError('Unsupported mode {}'.format(mode)) self.mode = mode self.font = Font() gc.collect() self.locate(0, 0) # Text cursor: default top left self.mounted = False # umountflash() not to sync if use_flash: from flash import FlashClass gc.collect() self.flash = FlashClass(intside) self.umountflash() # In case mounted by prior tests. self.mountflash() gc.collect() def checkcm(self): if not (self.mode == NORMAL or self.in_context): raise EPDError('Fast mode must be run using a context manager') def __enter__(self): # Power up checkstate(self.mode == FAST, "In normal mode, can't use context manager") self.in_context = True self.epd.enter() return self def __exit__(self, *_): # shut down self.in_context = False self.epd.exit() pass def mountflash(self): if self.flash is None: # Not being used return self.flash.begin() # Initialise. vfs = uos.VfsFat(self.flash) # Instantiate FAT filesystem uos.mount(vfs, self.flash.mountpoint) self.mounted = True def umountflash(self): # Unmount flash if self.flash is None: return if self.mounted: self.flash.synchronise() try: uos.umount(self.flash.mountpoint) except OSError: pass # Don't care if it wasn't mounted self.flash.end() # Shut down self.mounted = False # flag unmounted to prevent spurious syncs def show(self): self.checkcm() self.umountflash() # sync, umount flash, shut it down and disable SPI if self.mode == NORMAL: # EPD functions which access the display electronics must be with self.epd as epd: # called from a with block to ensure proper startup & shutdown epd.showdata() else: # Fast mode: already in context manager self.epd.showdata() self.mountflash() def clear_screen(self, show=True): self.checkcm() self.locate(0, 0) # Reset text cursor self.epd.clear_data() if show: if self.mode == NORMAL: self.show() else: self.epd.EPD_clear() def refresh(self, fast =True): # Fast mode only functions checkstate(self.mode == FAST, 'refresh() invalid in normal mode') self.checkcm() self.epd.refresh(fast) def exchange(self, clear_data): checkstate(self.mode == FAST, 'exchange() invalid in normal mode') self.checkcm() self.epd.exchange(clear_data) @property def temperature(self): # return temperature as integer in Celsius return self.epd.temperature @property def location(self): return self.char_x, self.char_y @micropython.native def setpixel(self, x, y, black): # 41uS. Clips to borders. x, y must be integer if y < 0 or y >= LINES_PER_DISPLAY or x < 0 or x >= BITS_PER_LINE : return image = self.epd.image omask = 1 << (x & 0x07) index = (x >> 3) + y *BYTES_PER_LINE if black: image[index] |= omask else: image[index] &= (omask ^ 0xff) @micropython.viper def setpixelfast(self, x: int, y: int, black: int): # 27uS. Caller checks bounds image = ptr8(self.epd.image) omask = 1 << (x & 0x07) index = (x >> 3) + y * 33 #BYTES_PER_LINE if black: image[index] |= omask else: image[index] &= (omask ^ 0xff) # ****** Simple graphics support ****** def _line(self, x0, y0, x1, y1, black = True): # Sinle pixel line dx = x1 -x0 dy = y1 -y0 dx_sym = 1 if dx > 0 else -1 dy_sym = 1 if dy > 0 else -1 dx = dx_sym*dx dy = dy_sym*dy dx_x2 = dx*2 dy_x2 = dy*2 if (dx >= dy): di = dy_x2 - dx while (x0 != x1): self.setpixel(x0, y0, black) x0 += dx_sym if (di<0): di += dy_x2 else : di += dy_x2 - dx_x2 y0 += dy_sym self.setpixel(x0, y0, black) else: di = dx_x2 - dy while (y0 != y1): self.setpixel(x0, y0, black) y0 += dy_sym if (di < 0): di += dx_x2 else: di += dx_x2 - dy_x2 x0 += dx_sym self.setpixel(x0, y0, black) def line(self, x0, y0, x1, y1, width =1, black = True): # Draw line x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) if abs(x1 - x0) > abs(y1 - y0): # < 45 degrees for w in range(-width//2 +1, width//2 +1): self._line(x0, y0 +w, x1, y1 +w, black) else: for w in range(-width//2 +1, width//2 +1): self._line(x0 +w, y0, x1 +w, y1, black) def _rect(self, x0, y0, x1, y1, black): # Draw rectangle self.line(x0, y0, x1, y0, 1, black) self.line(x0, y0, x0, y1, 1, black) self.line(x0, y1, x1, y1, 1, black) self.line(x1, y0, x1, y1, 1, black) def rect(self, x0, y0, x1, y1, width =1, black = True): # Draw rectangle x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) x0, x1 = (x0, x1) if x1 > x0 else (x1, x0) # x0, y0 is top left, x1, y1 is bottom right y0, y1 = (y0, y1) if y1 > y0 else (y1, y0) for w in range(width): self._rect(x0 +w, y0 +w, x1 -w, y1 -w, black) def fillrect(self, x0, y0, x1, y1, black = True): # Draw filled rectangle x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) x0, x1 = (x0, x1) if x1 > x0 else (x1, x0) y0, y1 = (y0, y1) if y1 > y0 else (y1, y0) for x in range(x0, x1): for y in range(y0, y1): self.setpixel(x, y, black) def _circle(self, x0, y0, r, black = True): # Single pixel circle x = -r y = 0 err = 2 -2*r while x <= 0: self.setpixel(x0 -x, y0 +y, black) self.setpixel(x0 +x, y0 +y, black) self.setpixel(x0 +x, y0 -y, black) self.setpixel(x0 -x, y0 -y, black) e2 = err if (e2 <= y): y += 1 err += y*2 +1 if (-x == y and e2 <= x): e2 = 0 if (e2 > x): x += 1 err += x*2 +1 def circle(self, x0, y0, r, width =1, black = True): # Draw circle x0, y0, r = int(x0), int(y0), int(r) for r in range(r, r -width, -1): self._circle(x0, y0, r, black) def fillcircle(self, x0, y0, r, black = True): # Draw filled circle x0, y0, r = int(x0), int(y0), int(r) x = -r y = 0 err = 2 -2*r while x <= 0: self._line(x0 -x, y0 -y, x0 -x, y0 +y, black) self._line(x0 +x, y0 -y, x0 +x, y0 +y, black) e2 = err if (e2 <= y): y +=1 err += y*2 +1 if (-x == y and e2 <= x): e2 = 0 if (e2 > x): x += 1 err += x*2 +1 # ****** Image display ****** def load_xbm(self, sourcefile, x = 0, y = 0): g = get_xbm_data(sourcefile) width = next(g) height = next(g) self.loadgfx(g, width, height, x, y) # Load a rectangular region with a bitmap supplied by a generator. def loadgfx(self, gen, width, height, x0, y0): byteoffset = x0 >> 3 bitshift = x0 & 7 # Offset of image relative to byte boundary bytes_per_line = width >> 3 if width & 7 > 0: bytes_per_line += 1 for line in range(height): y = y0 + line if y >= LINES_PER_DISPLAY: break index = y * BYTES_PER_LINE + byteoffset bitsleft = width x = x0 for byte in range(bytes_per_line): val = next(gen) bits_to_write = min(bitsleft, 8) x += bits_to_write if x <= BITS_PER_LINE: if bitshift == 0 and bits_to_write == 8: self.epd.image[index] = val index += 1 else: mask = ((1 << bitshift) -1) # Bits in current byte to preserve bitsused = bitshift + bits_to_write overflow = max(0, bitsused -8) underflow = max(0, 8 -bitsused) if underflow: # Underflow in current byte mask = (mask | ~((1 << bitsused) -1)) & 0xff nmask = ~mask & 0xff # Bits to overwrite self.epd.image[index] = (self.epd.image[index] & mask) | ((val << bitshift) & nmask) index += 1 if overflow : # Bits to write to next byte mask = ~((1 << overflow) -1) & 0xff # Preserve self.epd.image[index] = (self.epd.image[index] & mask) | (val >> (8 - bitshift)) bitsleft -= bits_to_write # ****** Text support ****** def locate(self, x, y): # set cursor position self.char_x = x # Text input cursor to (x, y) self.char_y = y # font.bytes_horiz # In cse of font file it's the pysical width of every character as stored in file # In case of Python font it's the value of max_width converted to bytes def _character(self, c, usefile): font = self.font # Cache for speed bits_vert = font.bits_vert if usefile: ff = font.fontfile ff.seek(self.FONT_HEADER_LENGTH + (c -32) * (font.bytes_per_ch + 1)) buf = ff.read(font.bytes_per_ch + 1) # Characters are stored as constant width. bytes_horiz = font.bytes_horiz # No. of bytes before next row # Advance = bits_horiz if variable pitch else font.bits_horiz bits_horiz = buf[0] offset = 1 else: modfont = font.modfont buf, height, bits_horiz = modfont.get_ch(chr(c)) # Width varies between characters bytes_horiz = (bits_horiz + 7) // 8 offset = 0 # Sanity checks: prevent index errors. Wrapping should be done at string/word level. if (self.char_x + bytes_horiz * 8) > BITS_PER_LINE : self.char_x = 0 self.char_y += bits_vert if self.char_y >= (LINES_PER_DISPLAY - bits_vert): self.char_y = 0 image = self.epd.image y = self.char_y # x, y are pixel coordinates for bit_vert in range(bits_vert): # for each vertical line x = self.char_x for byte_horiz in range(bytes_horiz): fontbyte = buf[bit_vert * bytes_horiz + byte_horiz + offset] index = (x >> 3) + y * BYTES_PER_LINE nbits = x & 0x07 if nbits == 0: image[index] = fontbyte else: image[index] &= (0xff >> (8 - nbits)) image[index] |= (fontbyte << nbits) image[index + 1] &= (0xff << nbits) image[index + 1] |= (fontbyte >> (8 - nbits)) x += 8 y += 1 self.char_x += font.bits_horiz if font.monospaced else bits_horiz def _putc(self, value, usefile): # print char if (value == NEWLINE): self.char_x = 0 self.char_y += self.font.bits_vert if (self.char_y >= LINES_PER_DISPLAY - self.font.bits_vert): self.char_y = 0 else: self._character(value, usefile) return value def puts(self, s): # Output a string at cursor if self.font.exists: if self.font.modfont is None: # No font module: using binary file for char in s: c = ord(char) if (c > 31 and c < 127) or c == NEWLINE: self._putc(c, True) else: # Python font file is self-checking for char in s: self._putc(ord(char), False) else: raise FontFileError("There is no current font")
class Display(object): FONT_HEADER_LENGTH = 4 def __init__(self, side = 'L', use_flash = False, pwr_controller = None): self.flash = None # Assume flash is unused try: self.intside = {'x':1, 'X':1, 'y':0,'Y':0, 'l':0, 'L':0, 'r':1, 'R':1}[side] except KeyError: raise ValueError("Side must be 'L' or 'R'") self.pwr_controller = pwr_controller self.epd = EPD(self.intside, pwr_controller) self.font = Font() self.locate(0, 0) # Text cursor: default top left self.mounted = False # umountflash() not to sync if use_flash: from flash import FlashClass self.flash = FlashClass(self.intside, pwr_controller) self.umountflash() # In case mounted by prior tests. if self.pwr_controller is None: # Normal operation: flash is mounted continuously self.mountflash() def mountflash(self): if self.flash is None: # Not being used return self.flash.begin() # Turn on power if under control. Initialise. pyb.mount(self.flash, self.flash.mountpoint) self.mounted = True def umountflash(self): # Unmount flash and power it down if self.flash is None: return if self.mounted: self.flash.sync() try: pyb.mount(None, self.flash.mountpoint) except OSError: pass # Don't care if it wasn't mounted self.flash.end() # Shut down, turn off power if under control self.mounted = False # flag unmounted to prevent spurious syncs def show(self): self.umountflash() # sync, umount flash, shut it down and disable SPI # EPD functions which access the display electronics must be with self.epd as epd: # called from a with block to ensure proper startup & shutdown epd.showdata() if self.pwr_controller is None: # Normal operation without power control: remount self.mountflash() @property def temperature(self): # return temperature as integer in Celsius return self.epd.temperature def clear_screen(self, show = True): self.locate(0, 0) # Reset text cursor self.epd.clear_data() if show: self.show() @micropython.native def setpixel(self, x, y, black): # 41uS. Clips to borders if y < 0 or y >= LINES_PER_DISPLAY or x < 0 or x >= BITS_PER_LINE : return image = self.epd.image omask = 1 << (x & 0x07) index = (x >> 3) + y *BYTES_PER_LINE if black: image[index] |= omask else: image[index] &= (omask ^ 0xff) @micropython.viper def setpixelfast(self, x: int, y: int, black: int): # 27uS. Caller checks bounds image = ptr8(self.epd.image) omask = 1 << (x & 0x07) index = (x >> 3) + y * 33 #BYTES_PER_LINE if black: image[index] |= omask else: image[index] &= (omask ^ 0xff) # ****** Simple graphics support ****** def _line(self, x0, y0, x1, y1, black = True): # Sinle pixel line dx = x1 -x0 dy = y1 -y0 dx_sym = 1 if dx > 0 else -1 dy_sym = 1 if dy > 0 else -1 dx = dx_sym*dx dy = dy_sym*dy dx_x2 = dx*2 dy_x2 = dy*2 if (dx >= dy): di = dy_x2 - dx while (x0 != x1): self.setpixel(x0, y0, black) x0 += dx_sym if (di<0): di += dy_x2 else : di += dy_x2 - dx_x2 y0 += dy_sym self.setpixel(x0, y0, black) else: di = dx_x2 - dy while (y0 != y1): self.setpixel(x0, y0, black) y0 += dy_sym if (di < 0): di += dx_x2 else: di += dx_x2 - dy_x2 x0 += dx_sym self.setpixel(x0, y0, black) def line(self, x0, y0, x1, y1, width =1, black = True): # Draw line if abs(x1 - x0) > abs(y1 - y0): # < 45 degrees for w in range(-width//2 +1, width//2 +1): self._line(x0, y0 +w, x1, y1 +w, black) else: for w in range(-width//2 +1, width//2 +1): self._line(x0 +w, y0, x1 +w, y1, black) def _rect(self, x0, y0, x1, y1, black): # Draw rectangle self.line(x0, y0, x1, y0, 1, black) self.line(x0, y0, x0, y1, 1, black) self.line(x0, y1, x1, y1, 1, black) self.line(x1, y0, x1, y1, 1, black) def rect(self, x0, y0, x1, y1, width =1, black = True): # Draw rectangle x0, x1 = (x0, x1) if x1 > x0 else (x1, x0) # x0, y0 is top left, x1, y1 is bottom right y0, y1 = (y0, y1) if y1 > y0 else (y1, y0) for w in range(width): self._rect(x0 +w, y0 +w, x1 -w, y1 -w, black) def fillrect(self, x0, y0, x1, y1, black = True): # Draw filled rectangle x0, x1 = (x0, x1) if x1 > x0 else (x1, x0) y0, y1 = (y0, y1) if y1 > y0 else (y1, y0) for x in range(x0, x1): for y in range(y0, y1): self.setpixel(x, y, black) def _circle(self, x0, y0, r, black = True): # Single pixel circle x = -r y = 0 err = 2 -2*r while x <= 0: self.setpixel(x0 -x, y0 +y, black) self.setpixel(x0 +x, y0 +y, black) self.setpixel(x0 +x, y0 -y, black) self.setpixel(x0 -x, y0 -y, black) e2 = err if (e2 <= y): y += 1 err += y*2 +1 if (-x == y and e2 <= x): e2 = 0 if (e2 > x): x += 1 err += x*2 +1 def circle(self, x0, y0, r, width =1, black = True): # Draw circle for r in range(r, r -width, -1): self._circle(x0, y0, r, black) def fillcircle(self, x0, y0, r, black = True): # Draw filled circle x = -r y = 0 err = 2 -2*r while x <= 0: self._line(x0 -x, y0 -y, x0 -x, y0 +y, black) self._line(x0 +x, y0 -y, x0 +x, y0 +y, black) e2 = err if (e2 <= y): y +=1 err += y*2 +1 if (-x == y and e2 <= x): e2 = 0 if (e2 > x): x += 1 err += x*2 +1 # ****** Image display ****** def load_xbm(self, sourcefile, x = 0, y = 0): g = get_xbm_data(sourcefile) width = next(g) height = next(g) self.loadgfx(g, width, height, x, y) # Load a rectangular region with a bitmap supplied by a generator. This must supply bytes for each line in turn. These # are displyed left to right, LSB of the 1st byte being at the top LH corner. Unused bits at the end of the line are # ignored with a new line starting on the next byte. def loadgfx(self, gen, width, height, x0, y0): byteoffset = x0 >> 3 bitshift = x0 & 7 # Offset of image relative to byte boundary bytes_per_line = width >> 3 if width & 7 > 0: bytes_per_line += 1 for line in range(height): y = y0 + line if y >= LINES_PER_DISPLAY: break index = y * BYTES_PER_LINE + byteoffset bitsleft = width x = x0 for byte in range(bytes_per_line): val = next(gen) bits_to_write = min(bitsleft, 8) x += bits_to_write if x <= BITS_PER_LINE: if bitshift == 0 and bits_to_write == 8: self.epd.image[index] = val index += 1 else: mask = ((1 << bitshift) -1) # Bits in current byte to preserve bitsused = bitshift + bits_to_write overflow = max(0, bitsused -8) underflow = max(0, 8 -bitsused) if underflow: # Underflow in current byte mask = (mask | ~((1 << bitsused) -1)) & 0xff nmask = ~mask & 0xff # Bits to overwrite self.epd.image[index] = (self.epd.image[index] & mask) | ((val << bitshift) & nmask) index += 1 if overflow : # Bits to write to next byte mask = ~((1 << overflow) -1) & 0xff # Preserve self.epd.image[index] = (self.epd.image[index] & mask) | (val >> (8 - bitshift)) bitsleft -= bits_to_write # ****** Text support ****** def locate(self, x, y): # set cursor position self.char_x = x # Text input cursor to (x, y) self.char_y = y def _character(self, c): font = self.font # Cache for speed ff = font.fontfile bv = font.bits_vert ff.seek(self.FONT_HEADER_LENGTH + (c -32) * font.bytes_per_ch) fontbuf = ff.read(font.bytes_per_ch) bh = font.bits_horiz if font.monospaced else fontbuf[0] # Char width if (self.char_x + bh) > BITS_PER_LINE : self.char_x = 0 self.char_y += bv if self.char_y >= (LINES_PER_DISPLAY - bv): self.char_y = 0 # write out the character for bit_vert in range(bv): # for each vertical line bytenum = bit_vert >> 3 bit = 1 << (bit_vert & 0x07) # Faster than divmod for bit_horiz in range(bh): # horizontal line fontbyte = fontbuf[font.bytes_vert * bit_horiz + bytenum +1] self.setpixelfast(self.char_x +bit_horiz, self.char_y +bit_vert, (fontbyte & bit) > 0) self.char_x += bh # width of current char def _putc(self, value): # print char if (value == NEWLINE): self.char_x = 0 self.char_y += self.font.bits_vert if (self.char_y >= LINES_PER_DISPLAY - self.font.bits_vert): self.char_y = 0 else: self._character(value) return value def puts(self, s): # Output a string at cursor if self.font.exists: for char in s: c = ord(char) if (c > 31 and c < 128) or c == NEWLINE: self._putc(c) else: raise FontFileError("There is no current font")
class Display(object): FONT_HEADER_LENGTH = 4 def __init__(self, side='L',*, mode=NORMAL, model=EMBEDDED_ARTISTS, use_flash=False, up_time=None): self.flash = None # Assume flash is unused self.in_context = False try: intside = {'l':0, 'r':1}[side.lower()] except (KeyError, AttributeError): raise ValueError("Side must be 'L' or 'R'") if model not in (EMBEDDED_ARTISTS, ADAFRUIT): raise ValueError('Unsupported model') if mode == FAST and use_flash: raise ValueError('Flash memory unavailable in fast mode') if mode == NORMAL and up_time is not None: raise ValueError('Cannot set up_time in normal mode') if mode == NORMAL: from epd import EPD self.epd = EPD(intside, model) elif mode == FAST: from epdpart import EPD self.epd = EPD(intside, model, up_time) else: raise ValueError('Unsupported mode {}'.format(mode)) self.mode = mode self.font = Font() gc.collect() self.locate(0, 0) # Text cursor: default top left self.mounted = False # umountflash() not to sync if use_flash: from flash import FlashClass self.flash = FlashClass(intside) self.umountflash() # In case mounted by prior tests. self.mountflash() gc.collect() def checkcm(self): if not (self.mode == NORMAL or self.in_context): raise EPDError('Fast mode must be run using a context manager') def __enter__(self): # Power up checkstate(self.mode == FAST, "In normal mode, can't use context manager") self.in_context = True self.epd.enter() return self def __exit__(self, *_): # shut down self.in_context = False self.epd.exit() pass def mountflash(self): if self.flash is None: # Not being used return self.flash.begin() # Initialise. pyb.mount(self.flash, self.flash.mountpoint) self.mounted = True def umountflash(self): # Unmount flash if self.flash is None: return if self.mounted: self.flash.sync() try: pyb.mount(None, self.flash.mountpoint) except OSError: pass # Don't care if it wasn't mounted self.flash.end() # Shut down self.mounted = False # flag unmounted to prevent spurious syncs def show(self): self.checkcm() self.umountflash() # sync, umount flash, shut it down and disable SPI if self.mode == NORMAL: # EPD functions which access the display electronics must be with self.epd as epd: # called from a with block to ensure proper startup & shutdown epd.showdata() else: # Fast mode: already in context manager self.epd.showdata() self.mountflash() def clear_screen(self, show=True): self.checkcm() self.locate(0, 0) # Reset text cursor self.epd.clear_data() if show: if self.mode == NORMAL: self.show() else: self.epd.EPD_clear() def refresh(self, fast =True): # Fast mode only functions checkstate(self.mode == FAST, 'refresh() invalid in normal mode') self.checkcm() self.epd.refresh(fast) def exchange(self, clear_data): checkstate(self.mode == FAST, 'exchange() invalid in normal mode') self.checkcm() self.epd.exchange(clear_data) @property def temperature(self): # return temperature as integer in Celsius return self.epd.temperature @property def location(self): return self.char_x, self.char_y @micropython.native def setpixel(self, x, y, black): # 41uS. Clips to borders. x, y must be integer if y < 0 or y >= LINES_PER_DISPLAY or x < 0 or x >= BITS_PER_LINE : return image = self.epd.image omask = 1 << (x & 0x07) index = (x >> 3) + y *BYTES_PER_LINE if black: image[index] |= omask else: image[index] &= (omask ^ 0xff) @micropython.viper def setpixelfast(self, x: int, y: int, black: int): # 27uS. Caller checks bounds image = ptr8(self.epd.image) omask = 1 << (x & 0x07) index = (x >> 3) + y * 33 #BYTES_PER_LINE if black: image[index] |= omask else: image[index] &= (omask ^ 0xff) # ****** Simple graphics support ****** def _line(self, x0, y0, x1, y1, black = True): # Sinle pixel line dx = x1 -x0 dy = y1 -y0 dx_sym = 1 if dx > 0 else -1 dy_sym = 1 if dy > 0 else -1 dx = dx_sym*dx dy = dy_sym*dy dx_x2 = dx*2 dy_x2 = dy*2 if (dx >= dy): di = dy_x2 - dx while (x0 != x1): self.setpixel(x0, y0, black) x0 += dx_sym if (di<0): di += dy_x2 else : di += dy_x2 - dx_x2 y0 += dy_sym self.setpixel(x0, y0, black) else: di = dx_x2 - dy while (y0 != y1): self.setpixel(x0, y0, black) y0 += dy_sym if (di < 0): di += dx_x2 else: di += dx_x2 - dy_x2 x0 += dx_sym self.setpixel(x0, y0, black) def line(self, x0, y0, x1, y1, width =1, black = True): # Draw line x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) if abs(x1 - x0) > abs(y1 - y0): # < 45 degrees for w in range(-width//2 +1, width//2 +1): self._line(x0, y0 +w, x1, y1 +w, black) else: for w in range(-width//2 +1, width//2 +1): self._line(x0 +w, y0, x1 +w, y1, black) def _rect(self, x0, y0, x1, y1, black): # Draw rectangle self.line(x0, y0, x1, y0, 1, black) self.line(x0, y0, x0, y1, 1, black) self.line(x0, y1, x1, y1, 1, black) self.line(x1, y0, x1, y1, 1, black) def rect(self, x0, y0, x1, y1, width =1, black = True): # Draw rectangle x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) x0, x1 = (x0, x1) if x1 > x0 else (x1, x0) # x0, y0 is top left, x1, y1 is bottom right y0, y1 = (y0, y1) if y1 > y0 else (y1, y0) for w in range(width): self._rect(x0 +w, y0 +w, x1 -w, y1 -w, black) def fillrect(self, x0, y0, x1, y1, black = True): # Draw filled rectangle x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) x0, x1 = (x0, x1) if x1 > x0 else (x1, x0) y0, y1 = (y0, y1) if y1 > y0 else (y1, y0) for x in range(x0, x1): for y in range(y0, y1): self.setpixel(x, y, black) def _circle(self, x0, y0, r, black = True): # Single pixel circle x = -r y = 0 err = 2 -2*r while x <= 0: self.setpixel(x0 -x, y0 +y, black) self.setpixel(x0 +x, y0 +y, black) self.setpixel(x0 +x, y0 -y, black) self.setpixel(x0 -x, y0 -y, black) e2 = err if (e2 <= y): y += 1 err += y*2 +1 if (-x == y and e2 <= x): e2 = 0 if (e2 > x): x += 1 err += x*2 +1 def circle(self, x0, y0, r, width =1, black = True): # Draw circle x0, y0, r = int(x0), int(y0), int(r) for r in range(r, r -width, -1): self._circle(x0, y0, r, black) def fillcircle(self, x0, y0, r, black = True): # Draw filled circle x0, y0, r = int(x0), int(y0), int(r) x = -r y = 0 err = 2 -2*r while x <= 0: self._line(x0 -x, y0 -y, x0 -x, y0 +y, black) self._line(x0 +x, y0 -y, x0 +x, y0 +y, black) e2 = err if (e2 <= y): y +=1 err += y*2 +1 if (-x == y and e2 <= x): e2 = 0 if (e2 > x): x += 1 err += x*2 +1 # ****** Image display ****** def load_xbm(self, sourcefile, x = 0, y = 0): g = get_xbm_data(sourcefile) width = next(g) height = next(g) self.loadgfx(g, width, height, x, y) # Load a rectangular region with a bitmap supplied by a generator. def loadgfx(self, gen, width, height, x0, y0): byteoffset = x0 >> 3 bitshift = x0 & 7 # Offset of image relative to byte boundary bytes_per_line = width >> 3 if width & 7 > 0: bytes_per_line += 1 for line in range(height): y = y0 + line if y >= LINES_PER_DISPLAY: break index = y * BYTES_PER_LINE + byteoffset bitsleft = width x = x0 for byte in range(bytes_per_line): val = next(gen) bits_to_write = min(bitsleft, 8) x += bits_to_write if x <= BITS_PER_LINE: if bitshift == 0 and bits_to_write == 8: self.epd.image[index] = val index += 1 else: mask = ((1 << bitshift) -1) # Bits in current byte to preserve bitsused = bitshift + bits_to_write overflow = max(0, bitsused -8) underflow = max(0, 8 -bitsused) if underflow: # Underflow in current byte mask = (mask | ~((1 << bitsused) -1)) & 0xff nmask = ~mask & 0xff # Bits to overwrite self.epd.image[index] = (self.epd.image[index] & mask) | ((val << bitshift) & nmask) index += 1 if overflow : # Bits to write to next byte mask = ~((1 << overflow) -1) & 0xff # Preserve self.epd.image[index] = (self.epd.image[index] & mask) | (val >> (8 - bitshift)) bitsleft -= bits_to_write # ****** Text support ****** def locate(self, x, y): # set cursor position self.char_x = x # Text input cursor to (x, y) self.char_y = y # font.bytes_horiz # In cse of font file it's the pysical width of every character as stored in file # In case of Python font it's the value of max_width converted to bytes def _character(self, c, usefile): font = self.font # Cache for speed bits_vert = font.bits_vert if usefile: ff = font.fontfile ff.seek(self.FONT_HEADER_LENGTH + (c -32) * (font.bytes_per_ch + 1)) buf = ff.read(font.bytes_per_ch + 1) # Characters are stored as constant width. bytes_horiz = font.bytes_horiz # No. of bytes before next row # Advance = bits_horiz if variable pitch else font.bits_horiz bits_horiz = buf[0] offset = 1 else: modfont = font.modfont buf, height, bits_horiz = modfont.get_ch(chr(c)) # Width varies between characters bytes_horiz = (bits_horiz + 7) // 8 offset = 0 if (self.char_x + bytes_horiz * 8) > BITS_PER_LINE : self.char_x = 0 self.char_y += bits_vert if self.char_y >= (LINES_PER_DISPLAY - bits_vert): self.char_y = 0 image = self.epd.image y = self.char_y # x, y are pixel coordinates for bit_vert in range(bits_vert): # for each vertical line x = self.char_x for byte_horiz in range(bytes_horiz): fontbyte = buf[bit_vert * bytes_horiz + byte_horiz + offset] index = (x >> 3) + y * BYTES_PER_LINE nbits = x & 0x07 if nbits == 0: image[index] = fontbyte else: image[index] &= (0xff >> (8 - nbits)) image[index] |= (fontbyte << nbits) image[index + 1] &= (0xff << nbits) image[index + 1] |= (fontbyte >> (8 - nbits)) x += 8 y += 1 self.char_x += font.bits_horiz if font.monospaced else bits_horiz def _putc(self, value, usefile): # print char if (value == NEWLINE): self.char_x = 0 self.char_y += self.font.bits_vert if (self.char_y >= LINES_PER_DISPLAY - self.font.bits_vert): self.char_y = 0 else: self._character(value, usefile) return value def puts(self, s): # Output a string at cursor if self.font.exists: usefile = self.font.modfont is None # No font module for char in s: c = ord(char) if (c > 31 and c < 127) or c == NEWLINE: self._putc(c, usefile) else: raise FontFileError("There is no current font")
self.c1.set_text(self.sdate, x=10) if self.stime != ttime: self.stime = ttime self.c2.set_text(self.stime, x=4) if __name__ == '__main__': from epd import EPD # Display layout instance L1 = Layout_1() # E-Paper Display instance epd = EPD(True, L1) for i in range(5): L1.inc_water(1) L1.inc_gas(0.01) L1.set_date_time() epd.update() L1.clear_all() epd.show() for i in range(5): L1.inc_water(1) L1.inc_gas(0.01) L1.set_date_time() epd.update()
class Display(object): IMG_DIR = '/flash/imgs' def __init__(self, debug=False): self.cfg = None self.rtc = RTC() self.debug = debug self.battery = Battery() # use this pin for debug self.wifi_pin = Pin("GP24", mode=Pin.IN, pull=Pin.PULL_UP) # Empty WDT object self.wdt = None if not debug: if not self.wifi_pin(): self.wdt = WDT(timeout=20000) self.sd = None else: from machine import SD try: self.sd = SD() mount(self.sd, '/sd') self.logfile = open("/sd/display.log", "a") except OSError: self.sd = None self.logfile = None self.epd = EPD() self.log("Time left on the alarm: %dms" % self.rtc.alarm_left()) # Don't flash when we're awake outside of debug heartbeat(self.debug) def log(self, msg, end='\n'): time = "%d, %d, %d, %d, %d, %d" % self.rtc.now()[:-2] msg = time + ", " + msg if self.logfile: self.logfile.write(msg + end) print(msg, end=end) def feed_wdt(self): if self.wdt: self.wdt.feed() def connect_wifi(self): from network import WLAN if not self.cfg: raise ValueError("Can't initialise wifi, no config") self.log('Starting WLAN, attempting to connect to ' + ','.join(self.cfg.wifi.keys())) wlan = WLAN(0, WLAN.STA) wlan.ifconfig(config='dhcp') while not wlan.isconnected(): nets = wlan.scan() for network in nets: if network.ssid in self.cfg.wifi.keys(): self.log('Connecting to ' + network.ssid) self.feed_wdt() # just in case wlan.connect(ssid=network.ssid, auth=(network.sec, self.cfg.wifi[network.ssid])) while not wlan.isconnected(): idle() break self.feed_wdt() # just in case sleep_ms(2000) self.log('Connected as %s' % wlan.ifconfig()[0]) @staticmethod def reset_cause(): import machine val = machine.reset_cause() if val == machine.POWER_ON: return "power" elif val == machine.HARD_RESET: return "hard" elif val == machine.WDT_RESET: return "wdt" elif val == machine.DEEPSLEEP_RESET: return "sleep" elif val == machine.SOFT_RESET: return "soft" def set_alarm(self, now, json_metadata): import json json_dict = json.loads(json_metadata) # Now we know the time too self.rtc = RTC(datetime=now) list_int = json_dict["wakeup"][:6] time_str = ",".join([str(x) for x in list_int]) self.log("Setting alarm for " + time_str) self.rtc.alarm(time=tuple(list_int)) if self.rtc.alarm_left() == 0: self.log("Alarm failed, setting for +1 hour") self.rtc.alarm(time=3600000) del json def display_file_image(self, file_obj): towrite = 15016 max_chunk = 250 while towrite > 0: c = max_chunk if towrite > max_chunk else towrite buff = file_obj.read(c) self.epd.upload_image_data(buff, delay_us=2000) self.feed_wdt() towrite -= c self.epd.display_update() def display_no_config(self): self.log("Displaying no config msg") with open(Display.IMG_DIR + '/no_config.bin', 'rb') as pic: self.display_file_image(pic) def display_low_battery(self): self.log("Displaying low battery msg") with open(Display.IMG_DIR + '/low_battery.bin', 'rb') as pic: self.display_file_image(pic) def display_cannot_connect(self): self.log("Displaying no server comms msg") with open(Display.IMG_DIR + '/no_server.bin', 'rb') as pic: self.display_file_image(pic) def display_no_wifi(self): self.log("Displaying no wifi msg") with open(Display.IMG_DIR + '/no_wifi.bin', 'rb') as pic: self.display_file_image(pic) def check_battery_level(self): now_batt = 200 last_batt = self.battery.battery_raw() while now_batt > last_batt: sleep_ms(50) last_batt = now_batt self.feed_wdt() now_batt = self.battery.battery_raw() self.log("Battery value: %d (%d)" % (self.battery.value(), self.battery.battery_raw())) if not self.battery.safe(): self.log("Battery voltage (%d) low! Turning off" % self.battery.battery_raw()) self.feed_wdt() self.display_low_battery() return False else: self.log("Battery value: %d (%d)" % (self.battery.value(), self.battery.battery_raw())) return True def run_deepsleep(self): if not self.run(): # RTC wasn't set, try to sleep forever self.rtc.alarm(time=2000000000) # Set the wakeup (why do it earlier?) rtc_i = self.rtc.irq(trigger=RTC.ALARM0, wake=DEEPSLEEP) self.log("Going to sleep, waking in %dms" % self.rtc.alarm_left()) # Close files on the SD card if self.sd: self.logfile.close() self.logfile = None unmount('/sd') self.sd.deinit() # Turn the screen off self.epd.disable() if not self.wifi_pin(): # Basically turn off deepsleep() else: self.log("DEBUG MODE: Staying awake") pass # Do nothing, allow network connections in def run(self): woken = self.wifi_pin() self.epd.enable() if not self.check_battery_level(): return False try: self.epd.get_sensor_data() except ValueError: self.log("Can't communicate with display, flashing light and giving up") heartbeat(True) sleep_ms(15000) return True if self.rtc.alarm_left() > 0: self.log("Woken up but the timer is still running, refreshing screen only") self.epd.display_update() self.feed_wdt() return True try: self.cfg = Config.load(sd=self.sd) self.log("Loaded config") except (OSError, ValueError) as e: self.log("Failed to load config: " + str(e)) self.display_no_config() try: self.connect_wifi() except: pass # everything while True: sleep_ms(10) self.feed_wdt() self.feed_wdt() self.connect_wifi() content = b'' try: self.log("Connecting to server %s:%d" % (self.cfg.host, self.cfg.port)) c = Connect(self.cfg.host, self.cfg.port, debug=self.debug) self.feed_wdt() cause = Display.reset_cause() if woken: cause = "user" self.log("Reset cause: " + cause) if len(self.cfg.upload_path) > 0: temp = self.epd.get_sensor_data() # we read this already c.post(self.cfg.upload_path, battery=self.battery.value(), reset=cause, screen=temp) self.log("Fetching metadata from " + self.cfg.metadata_path) metadata = c.get_quick(self.cfg.metadata_path, max_length=1024, path_type='json') # This will set the time to GMT, not localtime self.set_alarm(c.last_fetch_time, metadata) self.feed_wdt() del metadata del self.battery self.log("Fetching image from " + self.cfg.image_path) self.epd.image_erase_frame_buffer() self.feed_wdt() length, socket = c.get_object(self.cfg.image_path) if length != 15016: raise ValueError("Wrong data size for image: %d" % length) self.feed_wdt() except (RuntimeError, ValueError, OSError) as e: self.log("Failed to get remote info: " + str(e)) self.display_cannot_connect() self.rtc.alarm(time=3600000) return True sleep_ms(1000) # How do we make the write to display more reliable? self.feed_wdt() self.log("Uploading to display") self.display_file_image(socket) c.get_object_done() # close off socket if self.cfg.src == "sd": # If we've got a working config from SD instead of flash self.log("Transferring working config") Config.transfer() self.log("SUCCESS") self.log("Finished. Mem free: %d" % gc.mem_free()) return True
self.set_value_1(0) self.set_value_2(0) self.set_value_3(0) self.set_value_4(0) self.set_value_5(0) if __name__ == '__main__': from epd import EPD # Display Layout instance L1 = Escale() # E-Paper Display instance epd = EPD(True, L1) # TODO # epd = EPD(True) # epd.add( L1.components ) # epd.show() val = 123 for i in range(10): val += .22 val_25 = L1.round_to(val, 0.25) val_50 = L1.round_to(val, 0.5) val_1 = L1.round_to(val, 1.0) L1.set_value_1(val_25) L1.set_value_2(val_25) L1.set_value_3(val_50) L1.set_value_4(val_50)
self.cdate.set_text(self.sdate, align=1) if self.stime != ttime: self.stime = ttime self.ctime.set_text(self.stime, x=3) if __name__ == '__main__': from epd import EPD # Display Layout instance L2 = Layout_2() # E-Paper Display instance epd = EPD(False, L2) # Random values for test L2.water = 890 L2.gas = 2.64 L2.electricity = 0 L2.day_electricity = 12.3 L2.eur_water = 0.23 L2.eur_gas = 0.66 L2.eur_electricity = 1.79 L2.eur_total = L2.eur_water + L2.eur_gas + L2.eur_electricity for i in range(5): L2.inc_water(1) L2.inc_gas(0.01)
self.cdate.set_text(self.sdate, align=1) if self.stime != ttime: self.stime = ttime self.ctime.set_text(self.stime, x=5) if __name__ == '__main__': from epd import EPD # Display Layout instance L2 = Layout_2() # E-Paper Display instance epd = EPD(False, L2, partial_update = True) # Random values for test L2.water = 890 L2.gas = 2.64 L2.electricity = 0 L2.day_electricity = 12.3 L2.eur_water = 0.23 L2.eur_gas = 0.66 L2.eur_electricity = 1.79 L2.eur_total = L2.eur_water + L2.eur_gas + L2.eur_electricity for i in range(18): L2.egraph.set_bar(i, i+1)