2018-02-21 02:50:06 +01:00
|
|
|
# Basic LCD display support
|
|
|
|
#
|
2020-02-16 21:22:16 +01:00
|
|
|
# Copyright (C) 2018-2020 Kevin O'Connor <kevin@koconnor.net>
|
2018-02-21 02:50:06 +01:00
|
|
|
# Copyright (C) 2018 Aleph Objects, Inc <marcio@alephobjects.com>
|
2018-06-26 03:54:48 +02:00
|
|
|
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
|
2018-02-21 02:50:06 +01:00
|
|
|
#
|
|
|
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
2020-02-16 21:22:16 +01:00
|
|
|
import logging, os, ast
|
|
|
|
import hd44780, st7920, uc1701, menu
|
2018-02-21 02:50:06 +01:00
|
|
|
|
2018-09-20 17:10:51 +02:00
|
|
|
LCD_chips = {
|
|
|
|
'st7920': st7920.ST7920, 'hd44780': hd44780.HD44780,
|
2019-12-18 18:23:09 +01:00
|
|
|
'uc1701': uc1701.UC1701, 'ssd1306': uc1701.SSD1306, 'sh1106': uc1701.SH1106,
|
2018-09-20 17:10:51 +02:00
|
|
|
}
|
2018-02-21 02:50:06 +01:00
|
|
|
|
2020-02-16 21:22:16 +01:00
|
|
|
# Storage of [display_template my_template] config sections
|
|
|
|
class DisplayTemplate:
|
|
|
|
def __init__(self, config):
|
|
|
|
self.printer = config.get_printer()
|
|
|
|
name_parts = config.get_name().split()
|
|
|
|
if len(name_parts) != 2:
|
|
|
|
raise config.error("Section name '%s' is not valid"
|
|
|
|
% (config.get_name(),))
|
|
|
|
self.name = name_parts[1]
|
|
|
|
self.params = {}
|
|
|
|
for option in config.get_prefix_options('param_'):
|
|
|
|
try:
|
|
|
|
self.params[option] = ast.literal_eval(config.get(option))
|
|
|
|
except ValueError as e:
|
|
|
|
raise config.error(
|
|
|
|
"Option '%s' in section '%s' is not a valid literal" % (
|
|
|
|
option, config.get_name()))
|
2020-05-05 20:10:30 +02:00
|
|
|
gcode_macro = self.printer.load_object(config, 'gcode_macro')
|
2020-02-16 21:22:16 +01:00
|
|
|
self.template = gcode_macro.load_template(config, 'text')
|
|
|
|
def render(self, context, **kwargs):
|
|
|
|
params = dict(self.params)
|
|
|
|
params.update(**kwargs)
|
|
|
|
if len(params) != len(self.params):
|
|
|
|
raise self.printer.command_error(
|
|
|
|
"Invalid parameter to display_template %s" % (self.name,))
|
|
|
|
context = dict(context)
|
|
|
|
context.update(params)
|
|
|
|
return self.template.render(context)
|
|
|
|
|
|
|
|
# Store [display_data my_group my_item] sections (one instance per group name)
|
|
|
|
class DisplayGroup:
|
|
|
|
def __init__(self, config, name, data_configs):
|
|
|
|
# Load and parse the position of display_data items
|
|
|
|
items = []
|
|
|
|
for c in data_configs:
|
|
|
|
pos = c.get('position')
|
|
|
|
try:
|
|
|
|
row, col = [int(v.strip()) for v in pos.split(',')]
|
|
|
|
except:
|
|
|
|
raise config.error("Unable to parse 'position' in section '%s'"
|
|
|
|
% (c.get_name(),))
|
|
|
|
items.append((row, col, c.get_name()))
|
|
|
|
# Load all templates and store sorted by display position
|
|
|
|
configs_by_name = {c.get_name(): c for c in data_configs}
|
|
|
|
printer = config.get_printer()
|
2020-05-05 20:10:30 +02:00
|
|
|
gcode_macro = printer.load_object(config, 'gcode_macro')
|
2020-02-16 21:22:16 +01:00
|
|
|
self.data_items = []
|
|
|
|
for row, col, name in sorted(items):
|
|
|
|
c = configs_by_name[name]
|
|
|
|
if c.get('text'):
|
|
|
|
template = gcode_macro.load_template(c, 'text')
|
|
|
|
self.data_items.append((row, col, template))
|
|
|
|
def show(self, display, templates, eventtime):
|
|
|
|
swrap = self.data_items[0][2].create_status_wrapper(eventtime)
|
|
|
|
context = { 'printer': swrap,
|
|
|
|
'draw_progress_bar': display.draw_progress_bar }
|
|
|
|
def render(name, **kwargs):
|
|
|
|
return templates[name].render(context, **kwargs)
|
|
|
|
context['render'] = render
|
|
|
|
for row, col, template in self.data_items:
|
|
|
|
text = template.render(context)
|
|
|
|
display.draw_text(row, col, text.replace('\n', ''), eventtime)
|
|
|
|
|
2018-02-21 02:50:06 +01:00
|
|
|
class PrinterLCD:
|
|
|
|
def __init__(self, config):
|
|
|
|
self.printer = config.get_printer()
|
|
|
|
self.reactor = self.printer.get_reactor()
|
2020-02-16 21:22:16 +01:00
|
|
|
# Load low-level lcd handler
|
2018-03-06 17:16:25 +01:00
|
|
|
self.lcd_chip = config.getchoice('lcd_type', LCD_chips)(config)
|
2020-02-16 21:22:16 +01:00
|
|
|
# Load menu and display_status
|
2020-03-04 18:31:09 +01:00
|
|
|
self.menu = None
|
|
|
|
name = config.get_name()
|
|
|
|
if name == 'display':
|
|
|
|
# only load menu for primary display
|
|
|
|
self.menu = menu.MenuManager(config, self.lcd_chip)
|
2020-05-05 20:10:30 +02:00
|
|
|
self.printer.load_object(config, "display_status")
|
2020-02-16 21:22:16 +01:00
|
|
|
# Configurable display
|
|
|
|
self.display_templates = {}
|
|
|
|
self.display_data_groups = {}
|
|
|
|
self.load_config(config)
|
|
|
|
dgroup = "_default_16x4"
|
|
|
|
if self.lcd_chip.get_dimensions()[0] == 20:
|
|
|
|
dgroup = "_default_20x4"
|
|
|
|
dgroup = config.get('display_group', dgroup)
|
|
|
|
self.show_data_group = self.display_data_groups.get(dgroup)
|
|
|
|
if self.show_data_group is None:
|
2020-03-21 17:12:29 +01:00
|
|
|
raise config.error("Unknown display_data group '%s'" % (dgroup,))
|
2020-02-16 21:22:16 +01:00
|
|
|
# Screen updating
|
|
|
|
self.glyph_helpers = { 'animated_bed': self.animate_bed,
|
|
|
|
'animated_fan': self.animate_fan }
|
2019-01-08 16:55:18 +01:00
|
|
|
self.printer.register_event_handler("klippy:ready", self.handle_ready)
|
2018-03-06 17:16:25 +01:00
|
|
|
self.screen_update_timer = self.reactor.register_timer(
|
|
|
|
self.screen_update_event)
|
2020-02-16 21:22:16 +01:00
|
|
|
# Configurable display
|
|
|
|
def load_config(self, config):
|
|
|
|
# Load default display config file
|
|
|
|
pconfig = self.printer.lookup_object('configfile')
|
|
|
|
filename = os.path.join(os.path.dirname(__file__), 'display.cfg')
|
|
|
|
try:
|
|
|
|
dconfig = pconfig.read_config(filename)
|
|
|
|
except Exception:
|
|
|
|
raise self.printer.config_error("Cannot load config '%s'"
|
|
|
|
% (filename,))
|
|
|
|
# Load display_template sections
|
|
|
|
dt_main = config.get_prefix_sections('display_template ')
|
|
|
|
dt_main_names = { c.get_name(): 1 for c in dt_main }
|
|
|
|
dt_def = [c for c in dconfig.get_prefix_sections('display_template ')
|
|
|
|
if c.get_name() not in dt_main_names]
|
|
|
|
for c in dt_main + dt_def:
|
|
|
|
dt = DisplayTemplate(c)
|
|
|
|
self.display_templates[dt.name] = dt
|
|
|
|
# Load display_data sections
|
|
|
|
dd_main = config.get_prefix_sections('display_data ')
|
|
|
|
dd_main_names = { c.get_name(): 1 for c in dd_main }
|
|
|
|
dd_def = [c for c in dconfig.get_prefix_sections('display_data ')
|
|
|
|
if c.get_name() not in dd_main_names]
|
|
|
|
groups = {}
|
|
|
|
for c in dd_main + dd_def:
|
|
|
|
name_parts = c.get_name().split()
|
|
|
|
if len(name_parts) != 3:
|
|
|
|
raise config.error("Section name '%s' is not valid"
|
|
|
|
% (c.get_name(),))
|
|
|
|
groups.setdefault(name_parts[1], []).append(c)
|
|
|
|
for group_name, data_configs in groups.items():
|
|
|
|
dg = DisplayGroup(config, group_name, data_configs)
|
|
|
|
self.display_data_groups[group_name] = dg
|
2018-02-21 02:50:06 +01:00
|
|
|
# Initialization
|
2019-01-08 16:55:18 +01:00
|
|
|
def handle_ready(self):
|
|
|
|
self.lcd_chip.init()
|
|
|
|
# Start screen update timer
|
|
|
|
self.reactor.update_timer(self.screen_update_timer, self.reactor.NOW)
|
2018-02-21 02:50:06 +01:00
|
|
|
# Screen updating
|
2018-03-06 17:16:25 +01:00
|
|
|
def screen_update_event(self, eventtime):
|
2018-08-20 12:15:12 +02:00
|
|
|
# update menu component
|
2020-03-04 18:31:09 +01:00
|
|
|
if self.menu is not None:
|
|
|
|
ret = self.menu.screen_update_event(eventtime)
|
|
|
|
if ret:
|
|
|
|
return ret
|
2020-02-16 21:22:16 +01:00
|
|
|
# Update normal display
|
2018-02-21 02:50:06 +01:00
|
|
|
self.lcd_chip.clear()
|
2020-02-16 21:22:16 +01:00
|
|
|
try:
|
|
|
|
self.show_data_group.show(self, self.display_templates, eventtime)
|
|
|
|
except:
|
|
|
|
logging.exception("Error during display screen update")
|
2018-03-06 17:16:25 +01:00
|
|
|
self.lcd_chip.flush()
|
|
|
|
return eventtime + .500
|
2020-02-16 21:22:16 +01:00
|
|
|
# Rendering helpers
|
|
|
|
def animate_bed(self, row, col, eventtime):
|
|
|
|
frame = int(eventtime) & 1
|
|
|
|
return self.lcd_chip.write_glyph(col, row, 'bed_heat%d' % (frame + 1,))
|
|
|
|
def animate_fan(self, row, col, eventtime):
|
|
|
|
frame = int(eventtime) & 1
|
|
|
|
return self.lcd_chip.write_glyph(col, row, 'fan%d' % (frame + 1,))
|
|
|
|
def draw_text(self, row, col, mixed_text, eventtime):
|
|
|
|
pos = col
|
2018-09-20 19:01:51 +02:00
|
|
|
for i, text in enumerate(mixed_text.split('~')):
|
|
|
|
if i & 1 == 0:
|
|
|
|
# write text
|
2020-02-16 21:22:16 +01:00
|
|
|
self.lcd_chip.write_text(pos, row, text)
|
2018-09-20 19:01:51 +02:00
|
|
|
pos += len(text)
|
2020-02-16 21:22:16 +01:00
|
|
|
elif text in self.glyph_helpers:
|
|
|
|
pos += self.glyph_helpers[text](row, pos, eventtime)
|
2018-09-20 19:01:51 +02:00
|
|
|
else:
|
|
|
|
# write glyph
|
2020-02-16 21:22:16 +01:00
|
|
|
pos += self.lcd_chip.write_glyph(pos, row, text)
|
|
|
|
def draw_progress_bar(self, row, col, width, value):
|
|
|
|
value = int(value * 100.)
|
|
|
|
data = [0x00] * width
|
|
|
|
char_pcnt = int(100/width)
|
|
|
|
for i in range(width):
|
|
|
|
if (i+1)*char_pcnt <= value:
|
|
|
|
# Draw completely filled bytes
|
|
|
|
data[i] |= 0xFF
|
|
|
|
elif (i*char_pcnt) < value:
|
|
|
|
# Draw partially filled bytes
|
|
|
|
data[i] |= (-1 << 8-((value % char_pcnt)*8/char_pcnt)) & 0xff
|
|
|
|
data[0] |= 0x80
|
|
|
|
data[-1] |= 0x01
|
|
|
|
self.lcd_chip.write_graphics(col, row, 0, [0xff]*width)
|
|
|
|
for i in range(1, 15):
|
|
|
|
self.lcd_chip.write_graphics(col, row, i, data)
|
|
|
|
self.lcd_chip.write_graphics(col, row, 15, [0xff]*width)
|
|
|
|
return ""
|
2018-02-21 02:50:06 +01:00
|
|
|
|
|
|
|
def load_config(config):
|
|
|
|
return PrinterLCD(config)
|