aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/example-extras.cfg27
-rw-r--r--config/example-menu.cfg181
-rw-r--r--klippy/extras/display/display.py8
-rw-r--r--klippy/extras/display/menu.cfg933
-rw-r--r--klippy/extras/display/menu.py1437
5 files changed, 2586 insertions, 0 deletions
diff --git a/config/example-extras.cfg b/config/example-extras.cfg
index 6cd67131..c4728457 100644
--- a/config/example-extras.cfg
+++ b/config/example-extras.cfg
@@ -650,6 +650,33 @@
#a0_pin
# The pins connected to an uc1701 type lcd. These parameters must be
# provided when using an uc1701 display.
+#menu_root:
+# Entry point for menu, root menu container name. If this parameter
+# is not provided then default menu root is used. When provided
+# menu entry is 'deck' type then it'll be initiated immediately at startup.
+# Description of menu items is located in example-menu.cfg file.
+#menu_timeout:
+# Timeout for menu. Being inactive this amount of seconds will trigger
+# menu exit or return to root menu when having autorun enabled.
+# The default is 0 seconds (disabled)
+#encoder_pins:
+# The pins connected to encoder. 2 pins must be provided when
+# using encoder. This parameter must be provided when using menu.
+#click_pin:
+# The pin connected to 'enter' button or encoder 'click'. This parameter
+# must be provided when using menu.
+#back_pin:
+# The pin connected to 'back' button. This parameter is optional, menu
+# can be used without it.
+#up_pin:
+# The pin connected to 'up' button. This parameter must be provided
+# when using menu without encoder.
+#down_pin:
+# The pin connected to 'down' button. This parameter must be provided
+# when using menu without encoder.
+#kill_pin:
+# The pin connected to 'kill' button. This button will call
+# emergency stop.
# Custom thermistors (one may define any number of sections with a
# "thermistor" prefix). A custom thermistor may be used in the
diff --git a/config/example-menu.cfg b/config/example-menu.cfg
new file mode 100644
index 00000000..0e26a986
--- /dev/null
+++ b/config/example-menu.cfg
@@ -0,0 +1,181 @@
+# This file serves as documentation for config parameters. One may
+# copy and edit this file to configure a new menu layout.
+# The snippets in this file may be copied into the main printer.cfg file.
+# See the "example.cfg" file for description of common config parameters.
+
+# Available menu elements:
+# item - purely visual element
+# command - same like 'item' but with gcode trigger
+# input - same like 'command' but has value changing capabilities
+# list - menu element container, with entry and exit gcode triggers
+# vsdcard - same as 'list' but will append files from virtual sdcard
+# deck - special container for custom screens (cards) has entry and exit gcode triggers.
+# card - special content card for custom screens. Can only be used in 'deck'!
+
+#[menu item1]
+#type: item
+# Type will determine menu item properties and behaviours:
+#name:
+# This is mandatory attribute for every menu element.
+# You can use Python output formatting for parameter and transform values.
+# Quotes can be used in the beginning and end of name.
+#cursor:
+# It allows to change cursor character for selected menu element.
+# The default is >
+# This parameter is optional.
+#width:
+# This attribute accepts integer value. Element name is cut to this width.
+# This parameter is optional.
+#scroll:
+# This attribute accepts static boolean value. You can use it together with 'width'.
+# When this is enabled then names longer than width are scrolled back and forth.
+# The default is disabled. This parameter is optional.
+#enable:
+# This attribute accepts static boolean values and parameters (converted to boolean).
+# It accepts multiple logical expressions. Values separated by comma will return True if all elements are true.
+# Values on different lines will return True if any element is true.
+# You can use logical negation by using character ! as parameter prefix.
+#parameter:
+# This attribute accepts float values or special variables. Multiple values are delimited by comma.
+# All available parameter variables can be listed by 'MENU DO=dump' gcode, menu itself must be running.
+# This value is available for output formatting as {0}..{n} Where n is count of parameters.
+#transform:
+# This attribute allows to transform parameters value to something else.
+# More than one transformation can be added. Each transformation must be on separate line.
+# These transformed values are available for output formatting as {n+1}..{x}
+# Where n is count of parameters and x is count of transformations.
+# In order to transform the value of a particular parameter, you must add
+# an parameter index as prefix. Like this "transform: 1.choose('OFF','ON')"
+# If the index is not set then the default index 0 is used.
+#
+# map(fromLow,fromHigh,toLow,toHigh) - interpolate re-maps a parameter value from one range to another.
+# Output value type is taken from toHigh. It can be int or float.
+#
+# choose(e1,e2) - boolean chooser, converts the value of the parameter to the boolean type (0 and 1),
+# and selects the corresponding value by the index from the list.
+#
+# choose(e1,e2,...) - int chooser, converts the value of the parameter to the int type
+# and selects the corresponding value by the index from the list.
+#
+# choose({key:value,..}) - special dictionary chooser, parameter value cast type by first key type.
+# Selects the corresponding value by the key from the dictionary.
+#
+# int(), float(), bool(), str(), abs(), bin(), hex(), oct(), days(), hours(), minutes(), seconds()
+# These will convert parameter value to the special form.
+# int,float,bool,str,abs,bin,hex and oct are python functions.
+# days,hours,minutes,seconds will convert parameter value (it's taken as seconds) to time specific value
+#
+# scale(xx) - Multiplies parameter value by this xx. Pure interger or float value is excpected.
+
+
+#[menu command1]
+#type:command
+#name:
+#cursor:
+#width:
+#scroll:
+#enable:
+#parameter:
+#transform:
+#gcode:
+# When menu element is clicked then gcodes on this attribute will be executed.
+# Can have multiline gcode script and supports output formatting for parameter and transform values.
+#action:
+# Special action can be executed. Supports [back, exit] menu commands
+# and [respond response_info] command. Respond command will send '// response_info' to host.
+
+#[menu input1]
+#name:
+#cursor:
+#width:
+#enable:
+#transform:
+#parameter:
+# Value from parameter (always index 0) is taken as input value when in edit mode.
+#gcode:
+# This will be triggered in realtime or on exit from edit mode.
+#reverse:
+# This attribute accepts static boolean value.
+# When enabled it will reverse increment and decrement directions for input.
+# The default is False. This parameter is optional.
+#readonly:
+# This attribute accepts same logical expression as 'enable'.
+# When true then input element is readonly like 'item' and cannot enter to edit mode.
+# The default is False. This parameter is optional.
+#realtime:
+# This attribute accepts static boolean value.
+# When enabled it will execute gcode after each value change.
+# The default is False. This parameter is optional.
+#input_min:
+# It accepts integer or float value. Will set minimal bound for edit value.
+# The default is 2.2250738585072014e-308. This parameter is optional.
+#input_max:
+# It accepts integer or float value. Will set maximal bound for edit value.
+# The default is 1.7976931348623157e+308. This parameter is optional.
+#input_step:
+# This is mandatory attribute for input.
+# It accepts positive integer or float value. Will determine increment
+# and decrement steps for edit value.
+
+#[menu list1]
+#type:list or vsdcard
+#name:
+#cursor:
+#width:
+#scroll:
+#enable:
+#enter_gcode:
+# Will trigger gcode script when entering to this menu container.
+# This parameter is optional.
+#leave_gcode:
+# Will trigger gcode script when leaving from this menu container.
+# This parameter is optional.
+#show_back:
+# This attribute accepts static boolean value.
+# Show back [..] as first element.
+# The default is True. This parameter is optional.
+#show_title:
+# This attribute accepts static boolean value.
+# Show container name next to back [..] element.
+# The default is True. This parameter is optional.
+#items:
+# Menu elements listed in this container.
+# Each element must be on separate line.
+# Elements can be grouped on same line by separating them with comma
+#
+# When element name stars with . then menu system will add parent
+# container config name as prefix to element name (delimited by space)
+
+#[menu infodeck]
+#type: deck
+#name:
+#cursor:
+#width:
+#scroll:
+#enable:
+#enter_gcode
+#leave_gcode
+#longpress_menu:
+# Entry point to menu container. When this attribute is set then
+# long press > 1s will initiate this menu container if not in edit mode.
+# The default is disabled. This parameter is optional.
+#items:
+# It accepts only 'card' elements. You are able to switch between different card screens
+# by using encoder or up/down buttons.
+
+#[menu card1]
+#type: card
+#name:
+#content:
+# Card screen content. Each line represents display line.
+# Quotes can be used in the beginning and end of line.
+# Rendered elements are available for output formatting as {0}..{x}. It's always string type.
+#items:
+# List of elements in card. Each line represents a single index for content formatting.
+# It's possible to show multiple elements in one place by separating them with comma on single line.
+# If first element is integer then timed cycle is used (integer value is cycle time in seconds)
+# If no integer element then first enabled element is shown.
+# In cycler multiple elements can be grouped into one postition by separating them with |
+# This way only simple menu items can be grouped.
+# Example: 5,prt_time, prt_progress - elements prt_time and prt_progress are switched after 5s
+# Example: msg,xpos|ypos - elements xpos and ypos are grouped and showed together when msg is disabled.
diff --git a/klippy/extras/display/display.py b/klippy/extras/display/display.py
index 80a4515c..607f23e5 100644
--- a/klippy/extras/display/display.py
+++ b/klippy/extras/display/display.py
@@ -7,6 +7,7 @@
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import hd44780, st7920, uc1701, icons
+import menu
LCD_chips = { 'st7920': st7920.ST7920, 'hd44780': hd44780.HD44780, 'uc1701' : uc1701.UC1701 }
M73_TIMEOUT = 5.
@@ -17,6 +18,8 @@ class PrinterLCD:
self.reactor = self.printer.get_reactor()
self.lcd_chip = config.getchoice('lcd_type', LCD_chips)(config)
self.lcd_type = config.get('lcd_type')
+ # menu
+ self.menu = menu.MenuManager(config, self.lcd_chip)
# printer objects
self.gcode = self.toolhead = self.sdcard = None
self.fan = self.extruder0 = self.extruder1 = self.heater_bed = None
@@ -89,6 +92,11 @@ class PrinterLCD:
self.lcd_chip.write_graphics(x, y, 15, [0xff]*width)
# Screen updating
def screen_update_event(self, eventtime):
+ # update menu component
+ ret = self.menu.screen_update_event(eventtime)
+ if ret:
+ return ret
+ # update all else
self.lcd_chip.clear()
if self.lcd_type == 'hd44780':
self.screen_update_hd44780(eventtime)
diff --git a/klippy/extras/display/menu.cfg b/klippy/extras/display/menu.cfg
new file mode 100644
index 00000000..c944ac15
--- /dev/null
+++ b/klippy/extras/display/menu.cfg
@@ -0,0 +1,933 @@
+# This file serves as default menu structure.
+# See the "example-menu.cfg" file for description of common config parameters.
+
+### DEFAULT MENU ###
+
+### menu main ###
+[menu __main]
+type: list
+name: Main Menu
+items:
+ __octoprint
+ __sdcard
+ __control
+ __temp
+ __filament
+ __prepare
+ __test
+
+### menu octoprint ###
+[menu __octoprint]
+type: list
+name: OctoPrint
+items:
+ .__pause
+ .__resume
+ .__abort
+
+[menu __octoprint __pause]
+type: command
+enable: toolhead.is_printing
+name: Pause printing
+action: respond action:pause
+gcode:
+
+[menu __octoprint __resume]
+type: command
+enable: toolhead.is_printing
+name: Resume printing
+action: respond action:resume
+gcode:
+
+[menu __octoprint __abort]
+type: command
+enable: toolhead.is_printing
+name: Abort printing
+action: respond action:cancel
+gcode:
+
+### menu virtual sdcard ###
+[menu __sdcard]
+type: vsdcard
+name: SD Card
+items:
+ .__start
+ .__resume
+ .__pause
+
+[menu __sdcard __start]
+type: command
+enable: !toolhead.is_printing
+name: Start printing
+gcode: M24
+
+[menu __sdcard __resume]
+type: command
+enable: toolhead.is_printing
+name: Resume printing
+gcode: M24
+
+[menu __sdcard __pause]
+type: command
+enable: toolhead.is_printing
+name: Pause printing
+gcode: M25
+
+### menu control ###
+[menu __control]
+type: list
+name: Control
+items:
+ .__home
+ .__homez
+ .__homexy
+ .__move_10mm
+ .__move_1mm
+ .__move_01mm
+ .__disable
+ .__fanonoff
+ .__fanspeed
+ .__caselightonoff
+ .__caselightpwm
+
+[menu __control __home]
+type: command
+name: Home All
+gcode: G28
+enable: !toolhead.is_printing
+
+[menu __control __homez]
+type: command
+enable: !toolhead.is_printing
+name: Home Z
+gcode: G28 Z
+
+[menu __control __homexy]
+type: command
+enable: !toolhead.is_printing
+name: Home X/Y
+gcode: G28 X Y
+
+[menu __control __disable]
+type: command
+name: Disable steppers
+gcode:
+ M84
+ M18
+
+[menu __control __fanonoff]
+type: input
+enable: fan.is_enabled
+name: Fan {1:3s}
+parameter: fan.speed
+transform:
+ choose('OFF','ON')
+ choose(0,255)
+input_min: 0
+input_max: 1
+input_step: 1
+gcode: M106 S{2:d}
+
+[menu __control __fanspeed]
+type: input
+enable: fan.is_enabled
+name: Fan speed: {1:3d}%
+parameter: fan.speed
+transform:
+ map(0,1,0,100)
+ map(0,1,0,255)
+input_min: 0
+input_max: 1
+input_step: 0.01
+gcode: M106 S{2:d}
+
+[menu __control __caselightonoff]
+type: input
+enable: output_pin.caselight.is_enabled
+name: Case light: {1:3s}
+parameter: output_pin.caselight.value
+transform:
+ choose('OFF','ON')
+ choose(0,1)
+input_min: 0
+input_max: 1
+input_step: 1
+gcode: SET_PIN PIN=caselight VALUE={2}
+
+[menu __control __caselightpwm]
+type: input
+enable: output_pin.caselight.is_enabled
+name: Case light: {0:4.0%}
+parameter: output_pin.caselight.value
+input_min: 0.0
+input_max: 1.0
+input_step: 0.01
+gcode: SET_PIN PIN=caselight VALUE={0:.2f}
+
+### menu move 10mm ###
+[menu __control __move_10mm]
+type: list
+enable: !toolhead.is_printing
+name: Move 10mm
+items:
+ .__axis_z
+ .__axis_x, .__axis_y
+ .__axis_e
+
+[menu __control __move_10mm __axis_x]
+type: input
+name: "X:{0:05.1f} "
+parameter: toolhead.xpos
+input_min: 0
+input_max: 200.0
+input_step: 10.0
+gcode: G1 X{0:.1f}
+
+[menu __control __move_10mm __axis_y]
+type: input
+name: "Y:{0:05.1f} "
+parameter: toolhead.ypos
+input_min: 0
+input_max: 200.0
+input_step: 10.0
+gcode: G1 Y{0:.1f}
+
+[menu __control __move_10mm __axis_z]
+type: input
+enable: !toolhead.is_printing
+name: "Move Z:{0:05.1f}"
+parameter: toolhead.zpos
+input_min: 0
+input_max: 200.0
+input_step: 10.0
+gcode: G1 Z{0:.1f}
+
+[menu __control __move_10mm __axis_e]
+type: input
+enable: !toolhead.is_printing
+name: "Move E:{0:+06.1f}"
+parameter: 0
+input_min: -250.0
+input_max: 250.0
+input_step: 10.0
+gcode: G1 E{0:.1f} F240
+
+### menu move 1mm ###
+[menu __control __move_1mm]
+type: list
+enable: !toolhead.is_printing
+name: Move 1mm
+items:
+ .__axis_z
+ .__axis_x, .__axis_y
+ .__axis_e
+
+[menu __control __move_1mm __axis_x]
+type: input
+name: "X:{0:05.1f} "
+parameter: toolhead.xpos
+input_min: 0
+input_max: 100.0
+input_step: 1.0
+gcode: G1 X{0:.1f}
+
+[menu __control __move_1mm __axis_y]
+type: input
+name: "Y:{0:05.1f} "
+parameter: toolhead.ypos
+input_min: 0
+input_max: 100.0
+input_step: 1.0
+gcode: G1 Y{0:.1f}
+
+[menu __control __move_1mm __axis_z]
+type: input
+enable: !toolhead.is_printing
+name: "Move Z:{0:05.1f}"
+parameter: toolhead.zpos
+input_min: 0
+input_max: 100.0
+input_step: 1.0
+gcode: G1 Z{0:.1f}
+
+[menu __control __move_1mm __axis_e]
+type: input
+enable: !toolhead.is_printing
+name: "Move E:{0:+06.1f}"
+parameter: 0
+input_min: -100.0
+input_max: 100.0
+input_step: 1.0
+gcode: G1 E{0:.1f} F240
+
+### menu move 0.1mm ###
+[menu __control __move_01mm]
+type: list
+enable: !toolhead.is_printing
+name: Move 0.1mm
+items:
+ .__axis_z
+ .__axis_x, .__axis_y
+ .__axis_e
+
+[menu __control __move_01mm __axis_x]
+type: input
+name: "X:{0:05.1f} "
+parameter: toolhead.xpos
+input_min: 0
+input_max: 50.0
+input_step: 0.1
+gcode: G1 X{0:.1f}
+
+[menu __control __move_01mm __axis_y]
+type: input
+name: "Y:{0:05.1f} "
+parameter: toolhead.ypos
+input_min: 0
+input_max: 50.0
+input_step: 0.1
+gcode: G1 Y{0:.1f}
+
+[menu __control __move_01mm __axis_z]
+type: input
+enable: !toolhead.is_printing
+name: "Move Z:{0:05.1f}"
+parameter: toolhead.zpos
+input_min: 0
+input_max: 50.0
+input_step: 0.1
+gcode: G1 Z{0:.1f}
+
+[menu __control __move_01mm __axis_e]
+type: input
+enable: !toolhead.is_printing
+name: "Move E:{0:+06.1f}"
+parameter: 0
+input_min: -50.0
+input_max: 50.0
+input_step: 0.1
+gcode: G1 E{0:.1f} F240
+
+### menu temperature ###
+[menu __temp]
+type: list
+name: Temperature
+items:
+ .__hotend0_current, .__hotend0_target
+ .__hotend1_current, .__hotend1_target
+ .__hotbed_current, .__hotbed_target
+ .__preheat_pla
+ .__preheat_abs
+ .__cooldown
+
+[menu __temp __hotend0_current]
+type: item
+enable: extruder0.is_enabled
+name: "Ex0:{0:4.0f} T"
+parameter: extruder0.temperature
+
+[menu __temp __hotend0_target]
+type: input
+enable: extruder0.is_enabled
+name: "{0:4.0f}"
+parameter: extruder0.target
+input_min: 0
+input_max: 250
+input_step: 1
+gcode: M104 T0 S{0:.0f}
+
+[menu __temp __hotend1_current]
+type: item
+enable: extruder1.is_enabled
+name: "Ex1:{0:4.0f} T"
+parameter: extruder1.temperature
+
+[menu __temp __hotend1_target]
+type: input
+enable: extruder1.is_enabled
+name: "{0:4.0f}"
+parameter: extruder1.target
+input_min: 0
+input_max: 250
+input_step: 1
+gcode: M104 T1 S{0:.0f}
+
+[menu __temp __hotbed_current]
+type: item
+enable: heater_bed.is_enabled
+name: "Bed:{0:4.0f} T"
+parameter: heater_bed.temperature
+
+[menu __temp __hotbed_target]
+type: input
+enable: heater_bed.is_enabled
+name: "{0:4.0f}"
+parameter: heater_bed.target
+input_min: 0
+input_max: 130
+input_step: 1
+gcode: M140 S{0:.0f}
+
+[menu __temp __preheat_pla]
+type: list
+name: Preheat PLA
+items:
+ .__all
+ .__hotend
+ .__hotbed
+
+[menu __temp __preheat_pla __all]
+type: command
+enable: extruder0.is_enabled,heater_bed.is_enabled
+name: Preheat all
+gcode:
+ M140 S60
+ M104 S200
+
+[menu __temp __preheat_pla __hotend]
+type: command
+enable: extruder0.is_enabled
+name: Preheat hotend
+gcode: M104 S200
+
+[menu __temp __preheat_pla __hotbed]
+type: command
+enable: heater_bed.is_enabled
+name: Preheat hotbed
+gcode: M140 S60
+
+[menu __temp __preheat_abs]
+type: list
+name: Preheat ABS
+items:
+ .__all
+ .__hotend
+ .__hotbed
+
+[menu __temp __preheat_abs __all]
+type: command
+enable: extruder0.is_enabled,heater_bed.is_enabled
+name: Preheat all
+gcode:
+ M140 S110
+ M104 S245
+
+[menu __temp __preheat_abs __hotend]
+type: command
+enable: extruder0.is_enabled
+name: Preheat hotend
+gcode: M104 S245
+
+[menu __temp __preheat_abs __hotbed]
+type: command
+enable: heater_bed.is_enabled
+name: Preheat hotbed
+gcode: M140 S110
+
+[menu __temp __cooldown]
+type: list
+name: Cooldown
+items:
+ .__all
+ .__hotend
+ .__hotbed
+
+[menu __temp __cooldown __all]
+type: command
+enable: extruder0.is_enabled,heater_bed.is_enabled
+name: Cooldown all
+gcode:
+ M104 S0
+ M140 S0
+
+[menu __temp __cooldown __hotend]
+type: command
+enable: extruder0.is_enabled
+name: Cooldown hotend
+gcode: M104 S0
+
+[menu __temp __cooldown __hotbed]
+type: command
+enable: heater_bed.is_enabled
+name: Cooldown hotbed
+gcode: M140 S0
+
+### menu filament ###
+
+[menu __filament]
+type: list
+name: Filament
+items:
+ __temp __hotend0_current, __temp __hotend0_target
+ .__unload
+ .__load
+ .__feed
+
+[menu __filament __load]
+type: command
+name: Load Filament
+gcode:
+ G1 E200 F1000
+ G1 E100 F300
+
+[menu __filament __unload]
+type: command
+name: Unload Filament
+gcode:
+ G1 E-200 F1000
+ G1 E-200 F1800
+
+[menu __filament __feed]
+type: input
+name: Feed Filament: {0:.1f}
+parameter: toolhead.epos
+input_step: 0.1
+gcode: G1 E{0:.1f} F30
+
+### menu prepare ###
+[menu __prepare]
+type: list
+enable: !toolhead.is_printing
+name: Prepare
+items:
+ .__delta_calib
+ .__bedprobe
+ .__hotend_pid_tuning
+ .__hotbed_pid_tuning
+ .__host_restart
+ .__firmware_restart
+
+[menu __prepare __host_restart]
+type: command
+enable: !toolhead.is_printing
+name: Restart host
+gcode: RESTART
+
+[menu __prepare __firmware_restart]
+type: command
+enable: !toolhead.is_printing
+name: Restart FW
+gcode: FIRMWARE_RESTART
+
+[menu __prepare __delta_calib]
+type: command
+enable: !toolhead.is_printing
+name: Delta calibrate
+gcode: DELTA_CALIBRATE
+
+[menu __prepare __bedprobe]
+type: command
+enable: !toolhead.is_printing
+name: Bed probe
+gcode: PROBE
+
+[menu __prepare __hotend_pid_tuning]
+type: command
+enable: !toolhead.is_printing, extruder0.is_enabled
+name: Tune Hotend PID
+gcode: PID_CALIBRATE HEATER=extruder TARGET=210 WRITE_FILE=1
+
+[menu __prepare __hotbed_pid_tuning]
+type: command
+enable: !toolhead.is_printing, heater_bed.is_enabled
+name: Tune Hotbed PID
+gcode: PID_CALIBRATE HEATER=heater_bed TARGET=60 WRITE_FILE=1
+
+### test menu ###
+[menu __test]
+type: list
+name: Test menu
+items:
+ .__scroll
+ .__dump
+
+[menu __test __scroll]
+type: item
+name: This is very long item name
+width: 18
+scroll: true
+
+[menu __test __dump]
+type: command
+name: Dump parameters
+gcode: MENU DO=dump
+
+### info screens ###
+[menu __screen2004_static]
+type: deck
+name: Deck (static)
+longpress_menu: __main
+items: __card2004_static
+
+[menu __screen2004_input]
+type: deck
+name: Deck (input)
+longpress_menu: __main
+items: __card2004_input
+
+[menu __screen1604_static]
+type: deck
+name: Deck (static)
+longpress_menu: __main
+items: __card1604_static
+
+[menu __screen1604_input]
+type: deck
+name: Deck (input)
+longpress_menu: __main
+items: __card1604_input
+
+[menu __screen_test]
+type: deck
+name: Deck (test)
+longpress_menu: __main
+items: __card1, __card2, __card3
+
+### deck cards ###
+
+### special characters for hd44780
+# \x00 - thermometer
+# \x01 - heater_bed
+# \x02 - speed_factor
+# \x03 - clock
+# \x04 - degrees
+# \x05 - usb
+# \x06 - sdcard
+# \x7e - right arrow
+
+[menu __card2004_static]
+type: card
+name: Card 20x04
+content:
+ "\x00{0:3s}\x04\x7e{1:3s}\x04 {2}"
+ "\x01{3:3s}\x04\x7e{4:3s}\x04 \xa5{8}"
+ "fn{6} \x02{5} {7}"
+ "{9}"
+items:
+ __card_hotend0_current
+ __card_hotend0_target
+ __card_zpos
+ __card_hotbed_current
+ __card_hotbed_target
+ __card_frpeed
+ __card_fnspeed
+ 5,__card_prt_time, __card_usb_progress, __card_sdcard_progress
+ __card_status
+ __card_msg,__card_xpos|__card_ypos|__card_epos
+
+[menu __card1604_static]
+type: card
+name: Card 16x04
+content:
+ "E{0:3s}/{1:3s} {2}"
+ "B{3:3s}/{4:3s} fr{5}"
+ "fn{6} {7}"
+ "[{8}] {9}"
+items:
+ __card_hotend0_current
+ __card_hotend0_target
+ __card_zpos
+ __card_hotbed_current
+ __card_hotbed_target
+ __card_frpeed
+ __card_fnspeed
+ 5,__card_prt_time_2, __card_usb_progress_2, __card_sdcard_progress_2
+ __card_status_slim
+ __card_msg_slim,__card_xpos|__card_ypos
+
+[menu __card2004_input]
+type: card
+name: Card 20x04
+content:
+ "\x00{0:3s}\x04\x7e{1:3s}\x04 {2}"
+ "\x01{3:3s}\x04\x7e{4:3s}\x04 \xa5{8}"
+ "fn{6} \x02{5} {7}"
+ "{9}"
+items:
+ __card_hotend0_current
+ __card_hotend0_target_in
+ __card_zpos
+ __card_hotbed_current
+ __card_hotbed_target_in
+ __card_frpeed_in
+ __card_fnspeed
+ 5,__card_prt_time, __card_usb_progress, __card_sdcard_progress
+ __card_status
+ __card_msg,__card_xpos|__card_ypos|__card_epos
+
+[menu __card1604_input]
+type: card
+name: Card 16x04
+content:
+ "E{0:3s}/{1:3s} {2}"
+ "B{3:3s}/{4:3s} fr{5}"
+ "fn{6} {7}"
+ "[{8}] {9}"
+items:
+ __card_hotend0_current
+ __card_hotend0_target_in
+ __card_zpos
+ __card_hotbed_current
+ __card_hotbed_target_in
+ __card_frpeed_in
+ __card_fnspeed
+ 5,__card_prt_time_2, __card_usb_progress_2, __card_sdcard_progress_2
+ __card_status_slim
+ __card_msg,__card_xpos|__card_ypos
+
+[menu __card1]
+type: card
+name: Card 1
+content:
+ "E0:{0:3s}/{1:3s} B:{4:3s}/{5:3s}"
+ "E1:{2:3s}/{3:3s} {6}"
+ "Fr{7}Fn{8} {9}"
+ "{10}"
+
+items:
+ __card_hotend0_current
+ __card_hotend0_target
+ __card_hotend1_current
+ __card_hotend1_target
+ __card_hotbed_current
+ __card_hotbed_target
+ __card_zpos
+ __card_frpeed_in
+ __card_fnspeed_in
+ 5,__card_prt_time, __card_usb_progress, __card_sdcard_progress
+ __card_msg
+
+[menu __card2]
+type: card
+name: Card 2
+content:
+ "E:{0:3s}/{1:3s} B:{2:3s}/{3:3s}"
+ "{4}{5}{6}"
+ "Fr{7} {8} T{9}"
+ "{10}"
+items:
+ __card_hotend0_current
+ __card_hotend0_target
+ __card_hotbed_current
+ __card_hotbed_target
+ __card_xpos
+ __card_ypos
+ __card_zpos
+ __card_frpeed_in
+ __card_usb_progress_2, __card_sdcard_progress_2
+ __card_prt_time_2
+ __card_msg
+
+[menu __card3]
+type: card
+name: Card 3
+content:
+ "Example card :)"
+ "Try this menu."
+ "({0})"
+items:
+ __test
+
+### card items ###
+
+[menu __card_hotend0_current]
+type: item
+enable: extruder0.is_enabled
+name: "{1:3.0f}"
+parameter: extruder0.temperature
+transform: abs()
+
+[menu __card_hotend0_target]
+type: item
+enable: extruder0.is_enabled
+name: "{1:3.0f}"
+parameter: extruder0.target
+transform: abs()
+
+[menu __card_hotend0_target_in]
+type: input
+enable: extruder0.is_enabled
+name: "{1:3.0f}"
+parameter: extruder0.target
+transform: abs()
+input_min: 0
+input_max: 250
+input_step: 1
+gcode: M104 T0 S{0:.0f}
+
+[menu __card_hotend1_current]
+type: item
+enable: extruder1.is_enabled
+name: "{1:3.0f}"
+parameter: extruder1.temperature
+transform: abs()
+
+[menu __card_hotend1_target]
+type: item
+enable: extruder1.is_enabled
+name: "{1:3.0f}"
+parameter: extruder1.target
+transform: abs()
+
+[menu __card_hotend1_target_in]
+type: input
+enable: extruder1.is_enabled
+name: "{1:3.0f}"
+parameter: extruder1.target
+transform: abs()
+input_min: 0
+input_max: 250
+input_step: 1
+gcode: M104 T1 S{0:.0f}
+
+[menu __card_hotbed_current]
+type: item
+enable: heater_bed.is_enabled
+name: "{1:3.0f}"
+parameter: heater_bed.temperature
+transform: abs()
+
+[menu __card_hotbed_target]
+type: item
+enable: heater_bed.is_enabled
+name: "{1:3.0f}"
+parameter: heater_bed.target
+transform: abs()
+
+[menu __card_hotbed_target_in]
+type: input
+enable: heater_bed.is_enabled
+name: "{1:3.0f}"
+parameter: heater_bed.target
+transform: abs()
+input_min: 0
+input_max: 250
+input_step: 1
+gcode: M140 S{0:.0f}
+
+[menu __card_zpos]
+type: item
+name: "Z{0:06.2f}"
+parameter: toolhead.zpos
+
+[menu __card_xpos]
+type: item
+name: "X{0:05.1f}{2}"
+parameter: toolhead.xpos, menu.is20xx
+transform:
+ 1.choose('',' ')
+
+[menu __card_ypos]
+type: item
+name: "Y{0:05.1f}{2}"
+parameter: toolhead.ypos, menu.is20xx
+transform:
+ 1.choose('',' ')
+
+[menu __card_epos]
+type: item
+name: "E{0:05.1f}{2}"
+parameter: toolhead.epos, menu.is20xx
+transform:
+ 1.choose('',' ')
+
+[menu __card_frpeed]
+type: item
+name: "{0:4.0%}"
+parameter: gcode.speed_factor
+
+[menu __card_fnspeed]
+type: item
+name: "{0:4.0%}"
+parameter: fan.speed
+
+[menu __card_frpeed_in]
+type: input
+name: "{0:4.0%}"
+parameter: gcode.speed_factor
+transform:
+ map(0,1,0,255)
+input_min: 0
+input_max: 1
+input_step: 0.01
+gcode: M220 S{1:d}
+
+[menu __card_fnspeed_in]
+type: input
+enable: fan.is_enabled
+name: "{0:4.0%}"
+parameter: fan.speed
+transform:
+ map(0,1,0,255)
+input_min: 0
+input_max: 1
+input_step: 0.01
+gcode: M106 S{1:d}
+
+[menu __card_prt_time]
+type: item
+name: "\x03{1:02d}:{2:02d}"
+transform:
+ minutes()
+ seconds()
+parameter: toolhead.printing_time
+
+[menu __card_prt_time_2]
+type: item
+name: "T{1:02d}:{2:02d}"
+transform:
+ minutes()
+ seconds()
+parameter: toolhead.printing_time
+
+[menu __card_usb_progress]
+type: item
+enable: !virtual_sdcard.progress, display.progress
+name: "\x05{0:3.0f}%"
+parameter: display.progress
+
+[menu __card_sdcard_progress]
+type: item
+enable: virtual_sdcard.is_enabled, virtual_sdcard.progress
+name: "\x06{0:3.0f}%"
+parameter: virtual_sdcard.progress
+
+[menu __card_usb_progress_2]
+type: item
+#enable: !virtual_sdcard.progress,display.progress
+name: "pr{0:3.0f}%"
+parameter: display.progress
+
+[menu __card_sdcard_progress_2]
+type: item
+enable: virtual_sdcard.is_enabled, virtual_sdcard.progress
+name: "pr{0:3.0f}%"
+parameter: virtual_sdcard.progress
+
+[menu __card_msg]
+enable: display.message
+type: item
+scroll: true
+width: 20
+name: "{0}"
+parameter: display.message
+
+[menu __card_msg_slim]
+enable: display.message
+type: item
+scroll: true
+width: 12
+name: "{0}"
+parameter: display.message
+
+[menu __card_status]
+type: item
+name: "{0}"
+width: 5
+parameter: toolhead.status
+
+[menu __card_status_slim]
+type: item
+name: "{0}"
+width: 1
+parameter: toolhead.status
diff --git a/klippy/extras/display/menu.py b/klippy/extras/display/menu.py
new file mode 100644
index 00000000..da7de43f
--- /dev/null
+++ b/klippy/extras/display/menu.py
@@ -0,0 +1,1437 @@
+# -*- coding: utf-8 -*-
+# Basic LCD menu support
+#
+# Based on the RaspberryPiLcdMenu from Alan Aufderheide, February 2013
+# Copyright (C) 2018 Janar Sööt <janar.soot@gmail.com>
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+import os, ConfigParser, logging
+import sys, ast, re
+import klippy
+
+
+class error(Exception):
+ pass
+
+
+# static class for cursor
+class MenuCursor:
+ NONE = ' '
+ SELECT = '>'
+ EDIT = '*'
+
+
+# Menu element baseclass
+class MenuElement(object):
+ def __init__(self, manager, config, namespace=''):
+ self.cursor = config.get('cursor', MenuCursor.SELECT)
+ self._namespace = namespace
+ self._manager = manager
+ self._width = self._asint(config.get('width', '0'))
+ self._scroll = self._asbool(config.get('scroll', 'false'))
+ self._enable = self._aslist(config.get('enable', 'true'),
+ flatten=False)
+ self._name = self._asliteral(config.get('name'))
+ self.__scroll_offs = 0
+ self.__scroll_diff = 0
+ self.__scroll_dir = None
+ self.__last_state = True
+ if len(self.cursor) < 1:
+ raise error("Cursor with unexpected length, expecting 1.")
+
+ # override
+ def _render(self):
+ return self._name
+
+ # override
+ def _second_tick(self, eventtime):
+ pass
+
+ # override
+ def is_editing(self):
+ return False
+
+ # override
+ def is_readonly(self):
+ return True
+
+ # override
+ def is_scrollable(self):
+ return True
+
+ # override
+ def is_enabled(self):
+ return self._parse_bool(self._enable)
+
+ def init(self):
+ self.__clear_scroll()
+
+ def heartbeat(self, eventtime):
+ state = bool(int(eventtime) & 1)
+ if self.__last_state ^ state:
+ self.__last_state = state
+ if not self.is_editing():
+ self._second_tick(eventtime)
+ self.__update_scroll(eventtime)
+
+ def __clear_scroll(self):
+ self.__scroll_dir = None
+ self.__scroll_diff = 0
+ self.__scroll_offs = 0
+
+ def __update_scroll(self, eventtime):
+ if self.__scroll_dir == 0 and self.__scroll_diff > 0:
+ self.__scroll_dir = 1
+ self.__scroll_offs = 0
+ elif self.__scroll_dir and self.__scroll_diff > 0:
+ self.__scroll_offs += self.__scroll_dir
+ if self.__scroll_offs >= self.__scroll_diff:
+ self.__scroll_dir = -1
+ elif self.__scroll_offs <= 0:
+ self.__scroll_dir = 1
+ else:
+ self.__clear_scroll()
+
+ def __render_scroll(self, s):
+ if self.__scroll_dir is None:
+ self.__scroll_dir = 0
+ self.__scroll_offs = 0
+ return s[
+ self.__scroll_offs:self._width + self.__scroll_offs
+ ].ljust(self._width)
+
+ def render(self, scroll=False):
+ s = str(self._render())
+ if self._width > 0:
+ self.__scroll_diff = len(s) - self._width
+ if (scroll and self._scroll is True and self.is_scrollable()
+ and self.__scroll_diff > 0):
+ s = self.__render_scroll(s)
+ else:
+ self.__clear_scroll()
+ s = s[:self._width].ljust(self._width)
+ else:
+ self.__clear_scroll()
+ return s
+
+ def _parse_bool(self, lst):
+ try:
+ return any([
+ all([
+ self._lookup_bool(l2) for l2 in self._words_aslist(l1)
+ ]) for l1 in lst
+ ])
+ except Exception:
+ logging.exception("Boolean parsing error")
+ return False
+
+ def _lookup_bool(self, b):
+ if not self._asbool(b):
+ if b[0] == '!': # logical negation:
+ return not (not not self._lookup_parameter(b[1:]))
+ else:
+ return not not self._lookup_parameter(b)
+ return True
+
+ def _lookup_parameter(self, literal):
+ if self._isfloat(literal):
+ return float(literal)
+ else:
+ # only 2 level dot notation
+ keys = literal.rsplit('.', 1)
+ name = keys[0] if keys[0:1] else None
+ attr = keys[1] if keys[1:2] else None
+ if isinstance(self._manager.parameters, dict):
+ return (self._manager.parameters.get(name) or {}).get(attr)
+ else:
+ logging.error("Parameter storage is not dictionary")
+ return None
+
+ def _asliteral(self, s):
+ s = str(s).strip()
+ if s.startswith(('"', "'")):
+ s = s[1:]
+ if s.endswith(('"', "'")):
+ s = s[:-1]
+ return s
+
+ def _asbool(self, s, default=False):
+ if s is None:
+ return default
+ if isinstance(s, bool):
+ return s
+ s = str(s).strip()
+ return s.lower() in ('y', 'yes', 't', 'true', 'on', '1')
+
+ def _asint(self, s, default=0):
+ if s is None:
+ return default
+ if isinstance(s, (int, float)):
+ return int(s)
+ s = str(s).strip()
+ return int(float(s)) if self._isfloat(s) else int(default)
+
+ def _asfloat(self, s, default=0.0):
+ if s is None:
+ return default
+ if isinstance(s, (int, float)):
+ return float(s)
+ s = str(s).strip()
+ return float(s) if self._isfloat(s) else float(default)
+
+ def _lines_aslist(self, value, default=[]):
+ if isinstance(value, str):
+ value = filter(None, [x.strip() for x in value.splitlines()])
+ try:
+ return list(value)
+ except Exception:
+ logging.exception("Lines as list parsing error")
+ return list(default)
+
+ def _words_aslist(self, value, sep=',', default=[]):
+ if isinstance(value, str):
+ value = filter(None, [x.strip() for x in value.split(sep)])
+ try:
+ return list(value)
+ except Exception:
+ logging.exception("Words as list parsing error")
+ return list(default)
+
+ def _aslist(self, value, flatten=True, default=[]):
+ values = self._lines_aslist(value)
+ if not flatten:
+ return values
+ result = []
+ for value in values:
+ subvalues = self._words_aslist(value, sep=',')
+ result.extend(subvalues)
+ return result
+
+ def _isfloat(self, value):
+ try:
+ float(value)
+ return True
+ except ValueError:
+ return False
+
+ @property
+ def namespace(self):
+ return self._namespace
+
+ @namespace.setter
+ def namespace(self, ns):
+ self._namespace = ns
+
+
+# menu container baseclass
+class MenuContainer(MenuElement):
+ def __init__(self, manager, config, namespace=''):
+ super(MenuContainer, self).__init__(manager, config, namespace)
+ self._show_back = self._asbool(config.get('show_back', 'true'))
+ self._show_title = self._asbool(config.get('show_title', 'true'))
+ self._allitems = []
+ self._items = []
+ # recursive guard
+ self._parents = []
+
+ # overload
+ def _names_aslist(self):
+ return []
+
+ # overload
+ def is_accepted(self, item):
+ return isinstance(item, MenuElement)
+
+ def is_readonly(self):
+ return False
+
+ def is_editing(self):
+ return any([item.is_editing() for item in self._items])
+
+ def _lookup_item(self, item):
+ if isinstance(item, str):
+ s = item.strip()
+ if s.startswith('.'):
+ s = ' '.join([self.namespace, s[1:]])
+ item = self._manager.lookup_menuitem(s)
+ return item
+
+ def find_item(self, item):
+ index = None
+ if item in self._items:
+ index = self._items.index(item)
+ else:
+ for con in self._items:
+ if isinstance(con, MenuContainer) and item in con:
+ index = self._items.index(con)
+ return index
+
+ def add_parents(self, parents):
+ if isinstance(parents, list):
+ self._parents.extend(parents)
+ else:
+ self._parents.append(parents)
+
+ def assert_recursive_relation(self, parents=None):
+ assert self not in (parents or self._parents), \
+ "Recursive relation of '%s' container" % (self.namespace,)
+
+ def append_item(self, s):
+ item = self._lookup_item(s)
+ if item is not None:
+ if not self.is_accepted(item):
+ raise error("Menu item '%s'is not accepted!" % str(type(item)))
+ if isinstance(item, (MenuContainer)):
+ item.add_parents(self._parents)
+ item.add_parents(self)
+ item.assert_recursive_relation()
+ item.populate_items()
+ self._allitems.append(item)
+
+ def populate_items(self):
+ self._allitems = [] # empty list
+ if self._show_back is True:
+ name = '[..]'
+ if self._show_title:
+ name += ' %s' % str(self._name)
+ self.append_item(MenuCommand(self._manager, {
+ 'name': name, 'gcode': '', 'action': 'back'}, self.namespace))
+ for name in self._names_aslist():
+ self.append_item(name)
+ self.update_items()
+
+ def update_items(self):
+ self._items = [item for item in self._allitems if item.is_enabled()]
+
+ def __iter__(self):
+ return iter(self._items)
+
+ def __len__(self):
+ return len(self._items)
+
+ def __getitem__(self, key):
+ return self._items[key]
+
+
+class MenuItem(MenuElement):
+ def __init__(self, manager, config, namespace=''):
+ super(MenuItem, self).__init__(manager, config, namespace)
+ self.parameter = config.get('parameter', '')
+ self.transform = config.get('transform', '')
+
+ def _parse_transform(self, t):
+ flist = {
+ 'int': int,
+ 'float': float,
+ 'bool': bool,
+ 'str': str,
+ 'abs': abs,
+ 'bin': bin,
+ 'hex': hex,
+ 'oct': oct
+ }
+
+ def mapper(left_min, left_max, right_min, right_max, cast_fn, index=0):
+ # interpolate
+ left_span = left_max - left_min
+ right_span = right_max - right_min
+ scale_factor = float(right_span) / float(left_span)
+
+ def map_fn(values):
+ return cast_fn(
+ right_min + (values[index] - left_min) * scale_factor
+ )
+ return map_fn
+
+ def scaler(scale_factor, cast_fn, index=0):
+ def scale_fn(values):
+ return cast_fn(values[index] * scale_factor)
+ return scale_fn
+
+ def chooser(choices, cast_fn, index=0):
+ def choose_fn(values):
+ return choices[cast_fn(values[index])]
+ return choose_fn
+
+ def timerizer(key, index=0):
+ time = {}
+
+ def time_fn(values):
+ try:
+ seconds = int(values[index])
+ except Exception:
+ logging.exception("Seconds parsing error")
+ seconds = 0
+
+ time['days'], time['seconds'] = divmod(seconds, 86400)
+ time['hours'], time['seconds'] = divmod(time['seconds'], 3600)
+ time['minutes'], time['seconds'] = divmod(time['seconds'], 60)
+
+ if key in time:
+ return time[key]
+ else:
+ return 0
+ return time_fn
+
+ def functionizer(key, index=0):
+ def func_fn(values):
+ if key in flist and callable(flist[key]):
+ return flist[key](values[index])
+ else:
+ logging.error("Unknown function: '%s'" % str(key))
+ return values[index]
+ return func_fn
+
+ fn = None
+ t = str(t).strip()
+ # transform: idx.func(a,b,...)
+ m = re.search(r"^(\d*)(?:\.?)([\S]+)(\(.*\))$", t)
+ if m is not None:
+ index = int(m.group(1) or 0)
+ fname = str(m.group(2)).lower()
+ try:
+ o = ast.literal_eval(m.group(3))
+ if (fname == 'map' and isinstance(o, tuple) and len(o) == 4
+ and isinstance(o[3], (float, int))):
+ # mapper (interpolate), cast type by last parameter type
+ fn = mapper(o[0], o[1], o[2], o[3], type(o[3]), index)
+ elif (fname == 'choose' and isinstance(o, tuple)
+ and len(o) == 2):
+ # boolean chooser for 2 size tuple
+ fn = chooser(o, bool, index)
+ elif fname == 'choose' and isinstance(o, tuple) and len(o) > 2:
+ # int chooser for list
+ fn = chooser(o, int, index)
+ elif (fname == 'choose' and isinstance(o, dict) and o.keys()
+ and isinstance(o.keys()[0], (int, float, str))):
+ # chooser, cast type by first key type
+ fn = chooser(o, type(o.keys()[0]), index)
+ elif fname == 'scale' and isinstance(o, (float, int)):
+ # scaler, cast type depends from scale factor type
+ fn = scaler(o, type(o), index)
+ elif fname in ('days', 'hours', 'minutes', 'seconds'):
+ fn = timerizer(fname, index)
+ elif fname in flist:
+ fn = functionizer(fname, index)
+ else:
+ logging.error(
+ "Unknown transform function: '%s'" % str(m.group(0)))
+ except Exception:
+ logging.exception("Transform parsing error")
+ else:
+ logging.error(
+ "Invalid transform parameter: '%s'" % str(t))
+ return fn
+
+ def _transform_aslist(self):
+ return list(filter(None, (
+ self._parse_transform(t) for t in self._aslist(
+ self.transform, flatten=False)
+ )))
+
+ def _parameter_aslist(self):
+ lst = []
+ for p in self._words_aslist(self.parameter):
+ lst.append(self._lookup_parameter(p))
+ if lst[-1] is None:
+ logging.error("Parameter '%s' not found" % str(p))
+ return list(lst)
+
+ def _prepare_values(self, value=None):
+ values = []
+ for i, v in enumerate(self._parameter_aslist()):
+ values += [value if i == 0 and value is not None else v]
+ if values:
+ try:
+ values += [t(list(values)) for t in self._transform_aslist()]
+ except Exception:
+ logging.exception("Transformation execution failed")
+ return tuple(values)
+
+ def _get_formatted(self, literal, val=None):
+ values = self._prepare_values(val)
+ if isinstance(literal, str) and len(values) > 0:
+ try:
+ literal = literal.format(*values)
+ except Exception:
+ logging.exception("Literal formatting failed")
+ return literal
+
+ def _render(self):
+ return self._get_formatted(self._name)
+
+
+class MenuCommand(MenuItem):
+ def __init__(self, manager, config, namespace=''):
+ super(MenuCommand, self).__init__(manager, config, namespace)
+ self._gcode = config.get('gcode')
+ self._action = config.get('action', None)
+
+ def is_readonly(self):
+ return False
+
+ def get_gcode(self):
+ return self._get_formatted(self._gcode)
+
+ def __call__(self):
+ if self._action is not None:
+ try:
+ fmt = self._get_formatted(self._action)
+ args = fmt.split()
+ self._manager.run_action(args[0], *args[1:])
+ except Exception:
+ logging.exception("Action formatting failed")
+
+
+class MenuInput(MenuCommand):
+ def __init__(self, manager, config, namespace=''):
+ super(MenuInput, self).__init__(manager, config, namespace)
+ self._reverse = self._asbool(config.get('reverse', 'false'))
+ self._realtime = self._asbool(config.get('realtime', 'false'))
+ self._readonly = self._aslist(
+ config.get('readonly', 'false'), flatten=False)
+ self._input_value = None
+ self.__last_value = None
+ self._input_min = config.getfloat('input_min', sys.float_info.min)
+ self._input_max = config.getfloat('input_max', sys.float_info.max)
+ self._input_step = config.getfloat('input_step', above=0.)
+
+ def is_scrollable(self):
+ return False
+
+ def is_readonly(self):
+ return self._parse_bool(self._readonly)
+
+ def _render(self):
+ return self._get_formatted(self._name, self._input_value)
+
+ def get_gcode(self):
+ return self._get_formatted(self._gcode, self._input_value)
+
+ def is_editing(self):
+ return self._input_value is not None
+
+ def _onchange(self):
+ self._manager.run_script(self.get_gcode())
+
+ def init_value(self):
+ self._input_value = None
+ self.__last_value = None
+ if not self.is_readonly():
+ args = self._prepare_values()
+ if len(args) > 0 and self._isfloat(args[0]):
+ self._input_value = float(args[0])
+ if self._realtime:
+ self._onchange()
+ else:
+ logging.error("Cannot init input value")
+
+ def reset_value(self):
+ self._input_value = None
+
+ def inc_value(self):
+ last_value = self._input_value
+ if self._input_value is None:
+ return
+
+ if(self._reverse is True):
+ self._input_value -= abs(self._input_step)
+ else:
+ self._input_value += abs(self._input_step)
+ self._input_value = min(self._input_max, max(
+ self._input_min, self._input_value))
+
+ if self._realtime and last_value != self._input_value:
+ self._onchange()
+
+ def dec_value(self):
+ last_value = self._input_value
+ if self._input_value is None:
+ return
+
+ if(self._reverse is True):
+ self._input_value += abs(self._input_step)
+ else:
+ self._input_value -= abs(self._input_step)
+ self._input_value = min(self._input_max, max(
+ self._input_min, self._input_value))
+
+ if self._realtime and last_value != self._input_value:
+ self._onchange()
+
+
+class MenuGroup(MenuContainer):
+ def __init__(self, manager, config, namespace='', sep=','):
+ super(MenuGroup, self).__init__(manager, config, namespace)
+ self._sep = sep
+ self._show_back = False
+ self.selected = None
+ self.items = config.get('items')
+
+ def is_accepted(self, item):
+ return (super(MenuGroup, self).is_accepted(item)
+ and type(item) is not MenuCard)
+
+ def is_scrollable(self):
+ return False
+
+ def is_enabled(self):
+ return not not len(self)
+
+ def is_readonly(self):
+ return all([item.is_readonly() for item in self._items])
+
+ def _names_aslist(self):
+ return self._words_aslist(self.items, sep=self._sep)
+
+ def init(self):
+ super(MenuGroup, self).init()
+ for item in self._items:
+ item.init()
+
+ def _render_item(self, item, selected=False, scroll=False):
+ name = "%s" % str(item.render(scroll))
+ if selected and not self.is_editing():
+ name = name if self._manager.blink_slow_state else ' '*len(name)
+ elif selected and self.is_editing():
+ name = name if self._manager.blink_fast_state else ' '*len(name)
+ return name
+
+ def _render(self):
+ s = ""
+ if self.selected is not None:
+ self.selected = (
+ (self.selected % len(self)) if len(self) > 0 else None)
+
+ for i, item in enumerate(self):
+ s += self._render_item(item, (i == self.selected), True)
+ return s
+
+ def _call_selected(self, method=None):
+ res = None
+ if self.selected is not None:
+ try:
+ if method is None:
+ res = self[self.selected]
+ else:
+ res = getattr(self[self.selected], method)()
+ except Exception:
+ logging.exception("Call selected error")
+ return res
+
+ def is_editing(self):
+ return self._call_selected('is_editing')
+
+ def inc_value(self):
+ self._call_selected('inc_value')
+
+ def dec_value(self):
+ self._call_selected('dec_value')
+
+ def selected_item(self):
+ return self._call_selected()
+
+ def find_next_item(self):
+ if self.selected is None:
+ self.selected = 0
+ elif self.selected < len(self) - 1:
+ self.selected += 1
+ else:
+ self.selected = None
+ # skip readonly
+ while (self.selected is not None
+ and self.selected < len(self)
+ and self._call_selected('is_readonly')):
+ if self.selected < len(self) - 1:
+ self.selected = (self.selected + 1)
+ else:
+ self.selected = None
+ return self.selected
+
+ def find_prev_item(self):
+ if self.selected is None:
+ self.selected = len(self) - 1
+ elif self.selected > 0:
+ self.selected -= 1
+ else:
+ self.selected = None
+ # skip readonly
+ while (self.selected is not None
+ and self.selected >= 0
+ and self._call_selected('is_readonly')):
+ self.selected = (self.selected - 1) if self.selected > 0 else None
+ return self.selected
+
+
+class MenuItemGroup(MenuGroup):
+ def __init__(self, manager, config, namespace='', sep='|'):
+ super(MenuItemGroup, self).__init__(manager, config, namespace, sep)
+
+ def is_readonly(self):
+ return True
+
+ def is_accepted(self, item):
+ return type(item) is MenuItem
+
+
+class MenuCycler(MenuGroup):
+ def __init__(self, manager, config, namespace='', sep=','):
+ super(MenuCycler, self).__init__(manager, config, namespace, sep)
+ self._interval = 0
+ self.__interval_cnt = 0
+ self.__alllen = 0
+ self._curr_idx = 0
+
+ def is_readonly(self):
+ return True
+
+ def is_accepted(self, item):
+ return type(item) in (MenuItem, MenuItemGroup)
+
+ def _lookup_item(self, item):
+ if isinstance(item, str) and '|' in item:
+ item = MenuItemGroup(self._manager, {
+ 'name': ' '.join([self._name, 'ItemGroup']),
+ 'items': item
+ }, self.namespace, '|')
+ elif isinstance(item, str) and item.isdigit():
+ try:
+ self._interval = max(0, int(item))
+ except Exception:
+ logging.exception("Interval parsing error")
+ item = None
+ return super(MenuCycler, self)._lookup_item(item)
+
+ def _second_tick(self, eventtime):
+ super(MenuCycler, self)._second_tick(eventtime)
+ if self._interval > 0:
+ self.__interval_cnt = (self.__interval_cnt + 1) % self._interval
+ if self.__interval_cnt == 0 and self.__alllen > 0:
+ self._curr_idx = (self._curr_idx + 1) % self.__alllen
+ else:
+ self._curr_idx = 0
+
+ def heartbeat(self, eventtime):
+ super(MenuCycler, self).heartbeat(eventtime)
+ for item in self._items:
+ item.heartbeat(eventtime)
+
+ def update_items(self):
+ items = [item for item in self._allitems if item.is_enabled()]
+ self.__alllen = len(items)
+ if self.__alllen > 0:
+ self._curr_idx = self._curr_idx % self.__alllen
+ self._items = [items[self._curr_idx]]
+ else:
+ self._curr_idx = 0
+ self._items = []
+
+
+class MenuList(MenuContainer):
+ def __init__(self, manager, config, namespace=''):
+ super(MenuList, self).__init__(manager, config, namespace)
+ self._enter_gcode = config.get('enter_gcode', None)
+ self._leave_gcode = config.get('leave_gcode', None)
+ self.items = config.get('items')
+
+ def is_accepted(self, item):
+ return (super(MenuList, self).is_accepted(item)
+ and type(item) is not MenuCard)
+
+ def _names_aslist(self):
+ return self._lines_aslist(self.items)
+
+ def _lookup_item(self, item):
+ if isinstance(item, str) and ',' in item:
+ item = MenuGroup(self._manager, {
+ 'name': ' '.join([self._name, 'Group']),
+ 'items': item
+ }, self.namespace, ',')
+ return super(MenuList, self)._lookup_item(item)
+
+ def update_items(self):
+ super(MenuList, self).update_items()
+ for item in self._items:
+ if isinstance(item, MenuGroup) and not item.is_editing():
+ item.update_items()
+
+ def get_enter_gcode(self):
+ return self._enter_gcode
+
+ def get_leave_gcode(self):
+ return self._leave_gcode
+
+
+class MenuVSDCard(MenuList):
+ def __init__(self, manager, config, namespace=''):
+ super(MenuVSDCard, self).__init__(manager, config, namespace)
+
+ def _populate_files(self):
+ sdcard = self._manager.objs.get('virtual_sdcard')
+ if sdcard is not None:
+ files = sdcard.get_file_list()
+ for fname, fsize in files:
+ gcode = [
+ 'M23 /%s' % str(fname)
+ ]
+ self.append_item(MenuCommand(self._manager, {
+ 'name': '%s' % str(fname),
+ 'cursor': '+',
+ 'gcode': "\n".join(gcode)
+ }))
+
+ def populate_items(self):
+ super(MenuVSDCard, self).populate_items()
+ self._populate_files()
+
+
+class MenuCard(MenuGroup):
+ def __init__(self, manager, config, namespace=''):
+ super(MenuCard, self).__init__(manager, config, namespace)
+ self.content = config.get('content')
+
+ def _names_aslist(self):
+ return self._lines_aslist(self.items)
+
+ def _content_aslist(self):
+ return filter(None, [
+ self._asliteral(s) for s in self._lines_aslist(self.content)
+ ])
+
+ def update_items(self):
+ self._items = self._allitems[:]
+ for item in self._items:
+ if isinstance(item, MenuGroup) and not item.is_editing():
+ item.update_items()
+
+ def _lookup_item(self, item):
+ if isinstance(item, str) and ',' in item:
+ item = MenuCycler(self._manager, {
+ 'name': ' '.join([self._name, 'Cycler']),
+ 'items': item
+ }, self.namespace, ',')
+ return super(MenuCard, self)._lookup_item(item)
+
+ def render_content(self, eventtime):
+ if self.selected is not None:
+ self.selected = (
+ (self.selected % len(self)) if len(self) > 0 else None)
+
+ items = []
+ for i, item in enumerate(self):
+ name = ''
+ if item.is_enabled():
+ item.heartbeat(eventtime)
+ name = self._render_item(item, (i == self.selected), True)
+ items.append(name)
+ lines = []
+ for line in self._content_aslist():
+ try:
+ lines.append(str(line).format(*items))
+ except Exception:
+ logging.exception('Card rendering error')
+ return lines
+
+ def _render(self):
+ return self._name
+
+
+class MenuDeck(MenuList):
+ def __init__(self, manager, config, namespace=''):
+ super(MenuDeck, self).__init__(manager, config, namespace)
+ self._menu = config.get('longpress_menu', None)
+ self.menu = None
+ self._show_back = False
+ self._show_title = False
+
+ def _populate_menu(self):
+ self.menu = None
+ if self._menu is not None:
+ menu = self._manager.lookup_menuitem(self._menu)
+ if isinstance(menu, MenuContainer):
+ menu.assert_recursive_relation(self._parents)
+ menu.populate_items()
+ self.menu = menu
+
+ def populate_items(self):
+ super(MenuDeck, self).populate_items()
+ self._populate_menu()
+
+ def _names_aslist(self):
+ return self._aslist(self.items)
+
+ def is_accepted(self, item):
+ return type(item) is MenuCard
+
+ def _render(self):
+ return self._name
+
+
+menu_items = {
+ 'item': MenuItem,
+ 'command': MenuCommand,
+ 'input': MenuInput,
+ 'list': MenuList,
+ 'vsdcard': MenuVSDCard,
+ 'deck': MenuDeck,
+ 'card': MenuCard
+}
+# Default dimensions for lcds (rows, cols)
+LCD_dims = {'st7920': (4, 16), 'hd44780': (4, 20), 'uc1701': (4, 16)}
+
+MENU_UPDATE_DELAY = .100
+TIMER_DELAY = .200
+BLINK_FAST_SEQUENCE = (True, True, False, False)
+BLINK_SLOW_SEQUENCE = (True, True, True, True, False, False, False)
+
+
+class MenuManager:
+ def __init__(self, config, lcd_chip):
+ self.running = False
+ self.menuitems = {}
+ self.menustack = []
+ self._autorun = False
+ self.top_row = 0
+ self.selected = 0
+ self.blink_fast_state = True
+ self.blink_slow_state = True
+ self.blink_fast_idx = 0
+ self.blink_slow_idx = 0
+ self.timeout_idx = 0
+ self.lcd_chip = lcd_chip
+ self.printer = config.get_printer()
+ self.gcode = self.printer.lookup_object('gcode')
+ self.parameters = {}
+ self.objs = {}
+ self.root = None
+ self._root = config.get('menu_root', '__main')
+ dims = config.getchoice('lcd_type', LCD_dims)
+ self.rows = config.getint('rows', dims[0])
+ self.cols = config.getint('cols', dims[1])
+ self.timeout = config.getint('menu_timeout', 0)
+ self.timer = 0
+ # buttons
+ self.encoder_pins = config.get('encoder_pins', None)
+ self.click_pin = config.get('click_pin', None)
+ self.back_pin = config.get('back_pin', None)
+ self.up_pin = config.get('up_pin', None)
+ self.down_pin = config.get('down_pin', None)
+ self.kill_pin = config.get('kill_pin', None)
+ self._last_click_press = 0
+ # printer objects
+ self.buttons = self.printer.try_load_module(config, "buttons")
+ # register itself for a printer_state callback
+ config.get_printer().add_object('menu', self)
+ # register buttons & encoder
+ if self.buttons:
+ if self.encoder_pins:
+ try:
+ pin1, pin2 = self.encoder_pins.split(',')
+ except Exception:
+ raise config.error("Unable to parse encoder_pins")
+ self.buttons.register_rotary_encoder(
+ pin1.strip(), pin2.strip(),
+ self.encoder_cw_callback, self.encoder_ccw_callback)
+ if self.click_pin:
+ self.buttons.register_buttons(
+ [self.click_pin], self.click_callback)
+ if self.back_pin:
+ self.buttons.register_button_push(
+ self.back_pin, self.back_callback)
+ if self.up_pin:
+ self.buttons.register_button_push(
+ self.up_pin, self.up_callback)
+ if self.down_pin:
+ self.buttons.register_button_push(
+ self.down_pin, self.down_callback)
+ if self.kill_pin:
+ self.buttons.register_button_push(
+ self.kill_pin, self.kill_callback)
+
+ # Add MENU commands
+ self.gcode.register_mux_command("MENU", "DO", 'dump', self.cmd_DO_DUMP,
+ desc=self.cmd_DO_help)
+
+ # Parse local config file in same directory as current module
+ fileconfig = ConfigParser.RawConfigParser()
+ localname = os.path.join(os.path.dirname(__file__), 'menu.cfg')
+ fileconfig.read(localname)
+ localconfig = klippy.ConfigWrapper(self.printer, fileconfig, {}, None)
+
+ # Load items from local config
+ self.load_menuitems(localconfig)
+ # Load items from main config
+ self.load_menuitems(config)
+
+ # Load menu root
+ if self._root is not None:
+ self.root = self.lookup_menuitem(self._root)
+ if isinstance(self.root, MenuDeck):
+ self._autorun = True
+
+ def printer_state(self, state):
+ if state == 'ready':
+ # Load all available printer objects
+ for cfg_name in self.printer.objects:
+ obj = self.printer.lookup_object(cfg_name, None)
+ if obj is not None:
+ name = ".".join(str(cfg_name).split())
+ self.objs[name] = obj
+ logging.debug("Load module '%s' -> %s" % (
+ str(name), str(obj.__class__)))
+ # start timer
+ reactor = self.printer.get_reactor()
+ reactor.register_timer(self.timer_event, reactor.NOW)
+
+ def timer_event(self, eventtime):
+ # take next from sequence
+ self.blink_fast_idx = (
+ (self.blink_fast_idx + 1) % len(BLINK_FAST_SEQUENCE)
+ )
+ self.blink_slow_idx = (
+ (self.blink_slow_idx + 1) % len(BLINK_SLOW_SEQUENCE)
+ )
+ self.timeout_idx = (self.timeout_idx + 1) % 5 # 0.2*5 = 1s
+ self.blink_fast_state = (
+ not not BLINK_FAST_SEQUENCE[self.blink_fast_idx]
+ )
+ self.blink_slow_state = (
+ not not BLINK_SLOW_SEQUENCE[self.blink_slow_idx]
+ )
+ if self.timeout_idx == 0:
+ self.timeout_check(eventtime)
+
+ return eventtime + TIMER_DELAY
+
+ def timeout_check(self, eventtime):
+ # check timeout
+ if (self.is_running() and self.timeout > 0
+ and not self._timeout_autorun_root()):
+ if self.timer >= self.timeout:
+ self.exit()
+ self.timer += 1
+ else:
+ self.timer = 0
+
+ def _timeout_autorun_root(self):
+ return (self._autorun is True and self.root is not None
+ and self.stack_peek() is self.root and self.selected == 0)
+
+ def is_running(self):
+ return self.running
+
+ def begin(self, eventtime):
+ self.menustack = []
+ self.top_row = 0
+ self.selected = 0
+ self.timer = 0
+ if isinstance(self.root, MenuContainer):
+ self.update_parameters(eventtime)
+ self.root.populate_items()
+ self.stack_push(self.root)
+ self.running = True
+ return
+ elif self.root is not None:
+ logging.error("Invalid root '%s', menu stopped!" % str(self._root))
+
+ self.running = False
+
+ def get_status(self, eventtime):
+ return {
+ 'eventtime': eventtime,
+ 'timeout': self.timeout,
+ 'autorun': self._autorun,
+ 'isRunning': self.running,
+ 'is2004': (self.rows == 4 and self.cols == 20),
+ 'is2002': (self.rows == 2 and self.cols == 20),
+ 'is1604': (self.rows == 4 and self.cols == 16),
+ 'is1602': (self.rows == 2 and self.cols == 16),
+ 'is20xx': (self.cols == 20),
+ 'is16xx': (self.cols == 16)
+ }
+
+ def update_parameters(self, eventtime):
+ self.parameters = {}
+ # getting info this way is more like hack
+ # all modules should have special reporting method (maybe get_status)
+ # for available parameters
+ # Only 2 level dot notation
+ for name in self.objs.keys():
+ try:
+ if self.objs[name] is not None:
+ class_name = str(self.objs[name].__class__.__name__)
+ get_status = getattr(self.objs[name], "get_status", None)
+ if callable(get_status):
+ self.parameters[name] = get_status(eventtime)
+ else:
+ self.parameters[name] = {}
+
+ self.parameters[name].update({'is_enabled': True})
+ # get additional info
+ if class_name == 'ToolHead':
+ pos = self.objs[name].get_position()
+ self.parameters[name].update({
+ 'xpos': pos[0],
+ 'ypos': pos[1],
+ 'zpos': pos[2],
+ 'epos': pos[3]
+ })
+ self.parameters[name].update({
+ 'is_printing': (
+ self.parameters[name]['status'] == "Printing"),
+ 'is_ready': (
+ self.parameters[name]['status'] == "Ready"),
+ 'is_idle': (
+ self.parameters[name]['status'] == "Idle")
+ })
+ elif class_name == 'PrinterExtruder':
+ info = self.objs[name].get_heater().get_status(
+ eventtime)
+ self.parameters[name].update(info)
+ elif class_name == 'PrinterLCD':
+ self.parameters[name].update({
+ 'progress': self.objs[name].progress or 0,
+ 'message': self.objs[name].message or '',
+ 'is_enabled': True
+ })
+ elif class_name == 'PrinterHeaterFan':
+ info = self.objs[name].fan.get_status(eventtime)
+ self.parameters[name].update(info)
+ elif class_name in ('PrinterOutputPin', 'PrinterServo'):
+ self.parameters[name].update({
+ 'value': self.objs[name].last_value
+ })
+ else:
+ self.parameters[name] = {'is_enabled': False}
+ except Exception:
+ logging.exception("Parameter '%s' update error" % str(name))
+
+ def stack_push(self, container):
+ if not isinstance(container, MenuContainer):
+ raise error("Wrong type, expected MenuContainer")
+ top = self.stack_peek()
+ if top is not None:
+ self.run_script(top.get_leave_gcode())
+ self.run_script(container.get_enter_gcode())
+ if not container.is_editing():
+ container.update_items()
+ self.menustack.append(container)
+
+ def stack_pop(self):
+ container = None
+ if self.stack_size() > 0:
+ container = self.menustack.pop()
+ if not isinstance(container, MenuContainer):
+ raise error("Wrong type, expected MenuContainer")
+ top = self.stack_peek()
+ if top is not None:
+ if not isinstance(container, MenuContainer):
+ raise error("Wrong type, expected MenuContainer")
+ if not top.is_editing():
+ top.update_items()
+ self.run_script(container.get_leave_gcode())
+ self.run_script(top.get_enter_gcode())
+ else:
+ self.run_script(container.get_leave_gcode())
+ return container
+
+ def stack_size(self):
+ return len(self.menustack)
+
+ def stack_peek(self, lvl=0):
+ container = None
+ if self.stack_size() > lvl:
+ container = self.menustack[self.stack_size() - lvl - 1]
+ return container
+
+ def _unescape_cchars(self, text):
+ def fixup(m):
+ text = str(m.group(0))
+ if text[:2] == "\\x":
+ try:
+ return "%c" % (int(text[2:], 16),)
+ except ValueError:
+ logging.exception('Custom character unescape error')
+ else:
+ return text
+ return re.sub(r'\\x[0-9a-f]{2}', fixup, str(text), flags=re.IGNORECASE)
+
+ def render(self, eventtime):
+ lines = []
+ self.update_parameters(eventtime)
+ container = self.stack_peek()
+ if self.running and isinstance(container, MenuContainer):
+ container.heartbeat(eventtime)
+ # clamps
+ self.top_row = max(0, min(
+ self.top_row, len(container) - self.rows))
+ self.selected = max(0, min(
+ self.selected, len(container) - 1))
+ if isinstance(container, MenuDeck):
+ if not container.is_editing():
+ container.update_items()
+ container[self.selected].heartbeat(eventtime)
+ lines = container[self.selected].render_content(eventtime)
+ else:
+ for row in range(self.top_row, self.top_row + self.rows):
+ s = ""
+ if row < len(container):
+ selected = (row == self.selected)
+ current = container[row]
+ if selected:
+ current.heartbeat(eventtime)
+ if (isinstance(current, (MenuInput, MenuGroup))
+ and current.is_editing()):
+ s += MenuCursor.EDIT
+ elif isinstance(current, MenuElement):
+ s += current.cursor
+ else:
+ s += MenuCursor.SELECT
+ else:
+ s += MenuCursor.NONE
+
+ name = "%s" % str(current.render(selected))
+ i = len(s)
+ if isinstance(current, MenuList):
+ s += name[:self.cols-i-1].ljust(self.cols-i-1)
+ s += '>'
+ else:
+ s += name[:self.cols-i].ljust(self.cols-i)
+ lines.append(s.ljust(self.cols))
+ return lines
+
+ def screen_update_event(self, eventtime):
+ if self.is_running():
+ self.lcd_chip.clear()
+ for y, line in enumerate(self.render(eventtime)):
+ self.lcd_chip.write_text(0, y, self._unescape_cchars(line))
+ self.lcd_chip.flush()
+ return eventtime + MENU_UPDATE_DELAY
+ elif not self.is_running() and self._autorun is True:
+ # lets start and populate the menu items
+ self.begin(eventtime)
+ return eventtime + MENU_UPDATE_DELAY
+ else:
+ return 0
+
+ def up(self):
+ container = self.stack_peek()
+ if self.running and isinstance(container, MenuContainer):
+ self.timer = 0
+ current = container[self.selected]
+ if (isinstance(current, (MenuInput, MenuGroup))
+ and current.is_editing()):
+ current.dec_value()
+ elif (isinstance(current, MenuGroup)
+ and current.find_prev_item() is not None):
+ pass
+ else:
+ if self.selected == 0:
+ return
+ if self.selected > self.top_row:
+ self.selected -= 1
+ else:
+ self.top_row -= 1
+ self.selected -= 1
+ # init element
+ if isinstance(container[self.selected], MenuElement):
+ container[self.selected].init()
+ # wind up group last item or init item
+ if isinstance(container[self.selected], MenuGroup):
+ container[self.selected].find_prev_item()
+
+ def down(self):
+ container = self.stack_peek()
+ if self.running and isinstance(container, MenuContainer):
+ self.timer = 0
+ current = container[self.selected]
+ if (isinstance(current, (MenuInput, MenuGroup))
+ and current.is_editing()):
+ current.inc_value()
+ elif (isinstance(current, MenuGroup)
+ and current.find_next_item() is not None):
+ pass
+ else:
+ if self.selected >= len(container) - 1:
+ return
+ if self.selected < self.top_row + self.rows - 1:
+ self.selected += 1
+ else:
+ self.top_row += 1
+ self.selected += 1
+ # init element
+ if isinstance(container[self.selected], MenuElement):
+ container[self.selected].init()
+ # wind up group first item
+ if isinstance(container[self.selected], MenuGroup):
+ container[self.selected].find_next_item()
+
+ def back(self):
+ container = self.stack_peek()
+ if self.running and isinstance(container, MenuContainer):
+ self.timer = 0
+ current = container[self.selected]
+ if (isinstance(current, (MenuInput, MenuGroup))
+ and current.is_editing()):
+ return
+ parent = self.stack_peek(1)
+ if isinstance(parent, MenuContainer):
+ self.stack_pop()
+ index = parent.find_item(container)
+ if index is not None and index < len(parent):
+ self.top_row = index
+ self.selected = index
+ else:
+ self.top_row = 0
+ self.selected = 0
+ # init element
+ if isinstance(parent[self.selected], MenuElement):
+ parent[self.selected].init()
+ # wind up group first item or init item
+ if isinstance(parent[self.selected], MenuGroup):
+ parent[self.selected].find_next_item()
+ else:
+ self.stack_pop()
+ self.running = False
+
+ def select(self):
+ container = self.stack_peek()
+ if self.running and isinstance(container, MenuContainer):
+ self.timer = 0
+ current = container[self.selected]
+ if isinstance(current, MenuGroup):
+ current = current.selected_item()
+ if isinstance(current, MenuList):
+ self.stack_push(current)
+ self.top_row = 0
+ self.selected = 0
+ elif isinstance(current, MenuInput):
+ if current.is_editing():
+ self.run_script(current.get_gcode())
+ current.reset_value()
+ else:
+ current.init_value()
+ elif isinstance(current, MenuCommand):
+ current()
+ self.run_script(current.get_gcode())
+
+ def exit(self, force=False):
+ container = self.stack_peek()
+ if self.running and isinstance(container, MenuContainer):
+ current = container[self.selected]
+ if (not force and isinstance(current, (MenuInput, MenuGroup))
+ and current.is_editing()):
+ return
+ self.run_script(container.get_leave_gcode())
+ self.running = False
+
+ def run_action(self, action, *args):
+ try:
+ action = str(action).strip().lower()
+ if action == 'back':
+ self.back()
+ elif action == 'exit':
+ self.exit()
+ elif action == 'respond':
+ self.gcode.respond_info("{}".format(' '.join(map(str, args))))
+ else:
+ logging.error("Unknown action %s" % (action))
+ except Exception:
+ logging.exception("Malformed action call")
+
+ def run_script(self, script):
+ if script is not None:
+ try:
+ self.gcode.run_script(script)
+ except Exception:
+ logging.exception("Script running error")
+
+ def add_menuitem(self, name, menu):
+ if name in self.menuitems:
+ logging.info(
+ "Declaration of '%s' hides "
+ "previous menuitem declaration" % (name,))
+ self.menuitems[name] = menu
+
+ def lookup_menuitem(self, name):
+ if name is None:
+ return None
+ if name not in self.menuitems:
+ raise self.printer.config_error(
+ "Unknown menuitem '%s'" % (name,))
+ return self.menuitems[name]
+
+ def load_menuitems(self, config):
+ for cfg in config.get_prefix_sections('menu '):
+ name = " ".join(cfg.get_name().split()[1:])
+ item = cfg.getchoice('type', menu_items)(self, cfg, name)
+ self.add_menuitem(name, item)
+
+ cmd_DO_help = "Menu do things"
+
+ def cmd_DO_DUMP(self, params):
+ for key1 in self.parameters:
+ if type(self.parameters[key1]) == dict:
+ for key2 in self.parameters[key1]:
+ msg = "{0}.{1} = {2}".format(
+ key1, key2,
+ self.parameters[key1].get(key2)
+ )
+ logging.info(msg)
+ self.gcode.respond_info(msg)
+ else:
+ msg = "{0} = {1}".format(key1, self.parameters.get(key1))
+ logging.info(msg)
+ self.gcode.respond_info(msg)
+
+ # buttons & encoder callbacks
+ def encoder_cw_callback(self, eventtime):
+ self.up()
+
+ def encoder_ccw_callback(self, eventtime):
+ self.down()
+
+ def click_callback(self, eventtime, state):
+ if self.click_pin:
+ if state:
+ self._last_press = eventtime
+ else:
+ if eventtime - self._last_press > 1.0:
+ # long click
+ if not self.is_running():
+ # lets start and populate the menu items
+ self.begin(eventtime)
+ else:
+ container = self.stack_peek()
+ if isinstance(container, MenuDeck):
+ menu = container.menu
+ if (isinstance(menu, MenuList)
+ and not container.is_editing()
+ and menu is not container):
+ self.stack_push(menu)
+ self.top_row = 0
+ self.selected = 0
+ else:
+ # short click
+ if self.is_running():
+ self.select()
+ else:
+ # lets start and populate the menu items
+ self.begin(eventtime)
+
+ def back_callback(self, eventtime):
+ if self.back_pin:
+ self.back()
+
+ def up_callback(self, eventtime):
+ if self.up_pin:
+ self.up()
+
+ def down_callback(self, eventtime):
+ if self.down_pin:
+ self.down()
+
+ def kill_callback(self, eventtime):
+ if self.kill_pin:
+ # Emergency Stop
+ self.printer.invoke_shutdown("Shutdown due to kill button!")