diff --git a/Marlin/Marlin_main.cpp b/Marlin/Marlin_main.cpp index 35a9f3e5d..33384530f 100644 --- a/Marlin/Marlin_main.cpp +++ b/Marlin/Marlin_main.cpp @@ -324,6 +324,8 @@ #if ENABLED(AUTO_BED_LEVELING_UBL) #include "ubl.h" + extern bool defer_return_to_status; + extern bool ubl_lcd_map_control; unified_bed_leveling ubl; #define UBL_MESH_VALID !( ( ubl.z_values[0][0] == ubl.z_values[0][1] && ubl.z_values[0][1] == ubl.z_values[0][2] \ && ubl.z_values[1][0] == ubl.z_values[1][1] && ubl.z_values[1][1] == ubl.z_values[1][2] \ @@ -755,7 +757,7 @@ void report_current_position_detail(); * Set the planner/stepper positions directly from current_position with * no kinematic translation. Used for homing axes and cartesian/core syncing. */ -inline void sync_plan_position() { +void sync_plan_position() { #if ENABLED(DEBUG_LEVELING_FEATURE) if (DEBUGGING(LEVELING)) DEBUG_POS("sync_plan_position", current_position); #endif @@ -7656,6 +7658,12 @@ inline void gcode_M18_M84() { if (parser.seen('E')) disable_e_steppers(); #endif } + + #if ENABLED(AUTO_BED_LEVELING_UBL) + ubl_lcd_map_control = false; + defer_return_to_status = false; + #endif + } } @@ -12429,6 +12437,10 @@ void manage_inactivity(bool ignore_stepper_queue/*=false*/) { #if ENABLED(DISABLE_INACTIVE_E) disable_e_steppers(); #endif + #if ENABLED(AUTO_BED_LEVELING_UBL) + ubl_lcd_map_control = false; + defer_return_to_status = false; + #endif } #ifdef CHDK // Check if pin should be set to LOW after M240 set it to HIGH diff --git a/Marlin/language_en.h b/Marlin/language_en.h index 56a0e5642..dfad170a7 100644 --- a/Marlin/language_en.h +++ b/Marlin/language_en.h @@ -280,6 +280,9 @@ #ifndef MSG_UBL_OUTPUT_MAP_CSV #define MSG_UBL_OUTPUT_MAP_CSV _UxGT("Output for CSV") #endif + #ifndef MSG_UBL_OUTPUT_MAP_BACKUP + #define MSG_UBL_OUTPUT_MAP_BACKUP _UxGT("Off Printer Backup") + #endif #ifndef MSG_UBL_INFO_UBL #define MSG_UBL_INFO_UBL _UxGT("Output UBL Info") #endif diff --git a/Marlin/ubl.cpp b/Marlin/ubl.cpp index e11629659..14d411e5c 100644 --- a/Marlin/ubl.cpp +++ b/Marlin/ubl.cpp @@ -107,11 +107,15 @@ } } + // display_map() currently produces three different mesh map types + // 0 : suitable for PronterFace and Repetier's serial console + // 1 : .CSV file suitable for importation into various spread sheets + // 2 : disply of the map data on a RepRap Graphical LCD Panel + void unified_bed_leveling::display_map(const int map_type) { - const bool map0 = map_type == 0; constexpr uint8_t spaces = 8 * (GRID_MAX_POINTS_X - 2); - if (map0) { + if (map_type == 0) { SERIAL_PROTOCOLLNPGM("\nBed Topography Report:\n"); serial_echo_xy(0, GRID_MAX_POINTS_Y - 1); SERIAL_ECHO_SP(spaces + 3); @@ -123,6 +127,9 @@ SERIAL_EOL(); } + if (map_type == 1) { SERIAL_PROTOCOLLNPGM("\nBed Topography Report for CSV:"); SERIAL_EOL(); } + if (map_type == 2) { SERIAL_PROTOCOLLNPGM("\nBed Topography Report for LCD:"); SERIAL_EOL(); } + const float current_xi = get_cell_index_x(current_position[X_AXIS] + (MESH_X_DIST) / 2.0), current_yi = get_cell_index_y(current_position[Y_AXIS] + (MESH_Y_DIST) / 2.0); @@ -131,37 +138,37 @@ const bool is_current = i == current_xi && j == current_yi; // is the nozzle here? then mark the number - if (map0) SERIAL_CHAR(is_current ? '[' : ' '); + if (map_type == 0) SERIAL_CHAR(is_current ? '[' : ' '); const float f = z_values[i][j]; if (isnan(f)) { - serialprintPGM(map0 ? PSTR(" . ") : PSTR("NAN")); + serialprintPGM((map_type == 0) ? PSTR(" . ") : PSTR("NAN")); } else { // if we don't do this, the columns won't line up nicely - if (map0 && f >= 0.0) SERIAL_CHAR(' '); - SERIAL_PROTOCOL_F(f, 3); + if ((map_type == 0) && f >= 0.0) SERIAL_CHAR(' '); + if (map_type <= 1) SERIAL_PROTOCOL_F(f, 3); idle(); } - if (!map0 && i < GRID_MAX_POINTS_X - 1) SERIAL_CHAR(','); + if (map_type == 1 && i < GRID_MAX_POINTS_X - 1) SERIAL_CHAR(','); #if TX_BUFFER_SIZE > 0 MYSERIAL.flushTX(); #endif safe_delay(15); - if (map0) { + if (map_type == 0) { SERIAL_CHAR(is_current ? ']' : ' '); SERIAL_CHAR(' '); } } SERIAL_EOL(); - if (j && map0) { // we want the (0,0) up tight against the block of numbers + if (j && (map_type == 0)) { // we want the (0,0) up tight against the block of numbers SERIAL_CHAR(' '); SERIAL_EOL(); } } - if (map0) { + if (map_type == 0) { serial_echo_xy(UBL_MESH_MIN_X, UBL_MESH_MIN_Y); SERIAL_ECHO_SP(spaces + 4); serial_echo_xy(UBL_MESH_MAX_X, UBL_MESH_MIN_Y); diff --git a/Marlin/ubl_G29.cpp b/Marlin/ubl_G29.cpp index 64a3820f1..7ae5b3099 100644 --- a/Marlin/ubl_G29.cpp +++ b/Marlin/ubl_G29.cpp @@ -45,6 +45,9 @@ void lcd_mesh_edit_setup(float initial); float lcd_mesh_edit(); void lcd_z_offset_edit_setup(float); + #ifdef DOGLCD + extern void _lcd_ubl_output_map_lcd(); + #endif float lcd_z_offset_edit(); #endif @@ -53,6 +56,9 @@ extern float probe_pt(const float &x, const float &y, bool, int); extern bool set_probe_deployed(bool); extern void set_bed_leveling_enabled(bool); + extern bool ubl_lcd_map_control; + typedef void (*screenFunc_t)(); + extern void lcd_goto_screen(screenFunc_t screen, const uint32_t encoder = 0); #define SIZE_OF_LITTLE_RAISE 1 #define BIG_RAISE_NOT_NEEDED 0 @@ -1191,7 +1197,7 @@ #endif g29_map_type = parser.seen('T') && parser.has_value() ? parser.value_int() : 0; - if (!WITHIN(g29_map_type, 0, 1)) { + if (!WITHIN(g29_map_type, 0, 2)) { SERIAL_PROTOCOLLNPGM("Invalid map type.\n"); return UBL_ERR; } @@ -1535,8 +1541,8 @@ while (ubl_lcd_clicked()) { // debounce and watch for abort idle(); if (ELAPSED(millis(), nxt)) { + ubl_lcd_map_control = false; lcd_return_to_status(); - //SERIAL_PROTOCOLLNPGM("\nFine Tuning of Mesh Stopped."); do_blocking_move_to_z(Z_CLEARANCE_BETWEEN_PROBES); LCD_MESSAGEPGM(MSG_EDITING_STOPPED); @@ -1567,6 +1573,13 @@ LCD_MESSAGEPGM(MSG_UBL_DONE_EDITING_MESH); SERIAL_ECHOLNPGM("Done Editing Mesh"); + + if (ubl_lcd_map_control) { + #ifdef DOGLCD + lcd_goto_screen(_lcd_ubl_output_map_lcd); + #endif + } + else lcd_return_to_status(); } #endif diff --git a/Marlin/ultralcd.cpp b/Marlin/ultralcd.cpp index fa22df4ef..865a6e3db 100644 --- a/Marlin/ultralcd.cpp +++ b/Marlin/ultralcd.cpp @@ -26,6 +26,7 @@ #include "language.h" #include "cardreader.h" #include "temperature.h" +#include "planner.h" #include "stepper.h" #include "configuration_store.h" #include "utility.h" @@ -43,6 +44,11 @@ #include "endstops.h" #endif +#if ENABLED(AUTO_BED_LEVELING_UBL) + #include "ubl.h" + bool ubl_lcd_map_control = false; +#endif + int lcd_preheat_hotend_temp[2], lcd_preheat_bed_temp[2], lcd_preheat_fan_speed[2]; #if ENABLED(FILAMENT_LCD_DISPLAY) && ENABLED(SDSUPPORT) @@ -102,9 +108,6 @@ uint16_t max_display_update_time = 0; extern bool powersupply_on; #endif - #if ENABLED(AUTO_BED_LEVELING_UBL) - #include "ubl.h" - #endif //////////////////////////////////////////// ///////////////// Menu Tree //////////////// @@ -1044,6 +1047,7 @@ void kill_screen(const char* lcd_msg) { float lcd_mesh_edit() { lcd_goto_screen(_lcd_mesh_edit_NOP); + lcdDrawUpdate = LCDVIEW_CALL_REDRAW_NEXT; _lcd_mesh_fine_tune(PSTR("Mesh Editor")); return mesh_edit_value; } @@ -1795,8 +1799,10 @@ void kill_screen(const char* lcd_msg) { custom_hotend_temp = 190, side_points = 3, ubl_fillin_amount = 5, - ubl_height_amount, - map_type; + ubl_height_amount = 1, + n_edit_pts = 1, + x_plot = 0, + y_plot = 0; /** * UBL Build Custom Mesh Command @@ -1856,8 +1862,7 @@ void kill_screen(const char* lcd_msg) { void _lcd_ubl_edit_mesh() { START_MENU(); MENU_BACK(MSG_UBL_TOOLS); - MENU_BACK(MSG_UBL_LEVEL_BED); - MENU_ITEM(gcode, MSG_UBL_FINE_TUNE_ALL, PSTR("G29 P4 R T")); + MENU_ITEM(gcode, MSG_UBL_FINE_TUNE_ALL, PSTR("G29 P4 R999 T")); MENU_ITEM(gcode, MSG_UBL_FINE_TUNE_CLOSEST, PSTR("G29 P4 T")); MENU_ITEM(submenu, MSG_UBL_MESH_HEIGHT_ADJUST, _lcd_ubl_height_adjust_menu); MENU_ITEM(function, MSG_WATCH, lcd_return_to_status); @@ -1944,7 +1949,7 @@ void kill_screen(const char* lcd_msg) { */ void _lcd_ubl_smart_fillin_cmd() { char UBL_LCD_GCODE[12]; - sprintf_P(UBL_LCD_GCODE, PSTR("G29 P3 T%i"), map_type); + sprintf_P(UBL_LCD_GCODE, PSTR("G29 P3 T0")); enqueue_and_echo_command(UBL_LCD_GCODE); } @@ -2045,12 +2050,219 @@ void kill_screen(const char* lcd_msg) { } /** - * UBL Output map Command + * UBL LCD "radar" map homing */ - void _lcd_ubl_output_map_cmd() { - char UBL_LCD_GCODE[10]; - sprintf_P(UBL_LCD_GCODE, PSTR("G29 T%i"), map_type); - enqueue_and_echo_command(UBL_LCD_GCODE); + void _lcd_ubl_output_map_lcd(); + + void _lcd_ubl_map_homing() { + if (lcdDrawUpdate) lcd_implementation_drawedit(PSTR(MSG_LEVEL_BED_HOMING), NULL); + lcdDrawUpdate = LCDVIEW_CALL_NO_REDRAW; + if (axis_homed[X_AXIS] && axis_homed[Y_AXIS] && axis_homed[Z_AXIS]) + lcd_goto_screen(_lcd_ubl_output_map_lcd); + } + + /** + * UBL LCD "radar" map point editing + */ + void _lcd_ubl_map_lcd_edit_cmd() { + char ubl_lcd_gcode [50], str[10], str2[10]; + + ubl_lcd_map_control = true; // Used for returning to the map screen + + dtostrf(pgm_read_float(&ubl._mesh_index_to_xpos[x_plot]), 0, 2, str); + dtostrf(pgm_read_float(&ubl._mesh_index_to_ypos[y_plot]), 0, 2, str2); + snprintf_P(ubl_lcd_gcode, sizeof(ubl_lcd_gcode), PSTR("G29 P4 X%s Y%s R%i"), str, str2, n_edit_pts); + enqueue_and_echo_command(ubl_lcd_gcode); + } + + #ifdef DOGLCD + + /** + * UBL LCD "radar" map data + */ + #define MAP_UPPER_LEFT_CORNER_X 35 // These probably should be moved to the .h file But for now, + #define MAP_UPPER_LEFT_CORNER_Y 8 // it is easier to play with things having them here + #define MAP_MAX_PIXELS_X 53 + #define MAP_MAX_PIXELS_Y 49 + + void _lcd_ubl_plot_drawing_prep() { + uint8_t i, j, x_offset, y_offset, x_map_pixels, y_map_pixels; + uint8_t pixels_per_X_mesh_pnt, pixels_per_Y_mesh_pnt, inverted_y; + + /*********************************************************/ + /************ Scale the box pixels appropriately *********/ + /*********************************************************/ + x_map_pixels = ((MAP_MAX_PIXELS_X - 4) / GRID_MAX_POINTS_X) * GRID_MAX_POINTS_X; + y_map_pixels = ((MAP_MAX_PIXELS_Y - 4) / GRID_MAX_POINTS_Y) * GRID_MAX_POINTS_Y; + + pixels_per_X_mesh_pnt = x_map_pixels / GRID_MAX_POINTS_X; + pixels_per_Y_mesh_pnt = y_map_pixels / GRID_MAX_POINTS_Y; + + x_offset = MAP_UPPER_LEFT_CORNER_X + 1 + (MAP_MAX_PIXELS_X-x_map_pixels-2)/2; + y_offset = MAP_UPPER_LEFT_CORNER_Y + 1 + (MAP_MAX_PIXELS_Y-y_map_pixels-2)/2; + + /*********************************************************/ + /************ Clear the Mesh Map Box**********************/ + /*********************************************************/ + + u8g.setColorIndex(1); // First draw the bigger box in White so we have a border around the mesh map box + u8g.drawBox(x_offset-2, y_offset-2, x_map_pixels+4, y_map_pixels+4); + + u8g.setColorIndex(0); // Now actually clear the mesh map box + u8g.drawBox(x_offset, y_offset, x_map_pixels, y_map_pixels); + + /*********************************************************/ + /************ Display Mesh Point Locations ***************/ + /*********************************************************/ + + u8g.setColorIndex(1); + for (i = 0; i < GRID_MAX_POINTS_X; i++) { + for (j = 0; j < GRID_MAX_POINTS_Y; j++) { + u8g.drawBox(x_offset+i*pixels_per_X_mesh_pnt+pixels_per_X_mesh_pnt/2, + y_offset+j*pixels_per_Y_mesh_pnt+pixels_per_Y_mesh_pnt/2, 1, 1); + } + } + + /*********************************************************/ + /************ Fill in the Specified Mesh Point ***********/ + /*********************************************************/ + + inverted_y = GRID_MAX_POINTS_Y - y_plot - 1; // The origin is typically in the lower right corner. We need to + // invert the Y to get it to plot in the right location. + u8g.drawBox(x_offset+x_plot*pixels_per_X_mesh_pnt, y_offset+inverted_y*pixels_per_Y_mesh_pnt, + pixels_per_X_mesh_pnt, pixels_per_Y_mesh_pnt); + + /*********************************************************/ + /************** Put Relevent Text on Display *************/ + /*********************************************************/ + + // Show X and Y positions at top of screen + u8g.setColorIndex(1); + u8g.setPrintPos(5, 7); + lcd_print("X:"); + lcd_print(ftostr32(LOGICAL_X_POSITION(pgm_read_float(&ubl._mesh_index_to_xpos[x_plot])))); + u8g.setPrintPos(74, 7); + lcd_print("Y:"); + lcd_print(ftostr32(LOGICAL_Y_POSITION(pgm_read_float(&ubl._mesh_index_to_ypos[y_plot])))); + + // Print plot position + u8g.setPrintPos(5, 64); + lcd_print("("); + u8g.print(x_plot); + lcd_print(","); + u8g.print(y_plot); + lcd_print(")"); + + // Show the location value + u8g.setPrintPos(74, 64); + lcd_print("Z:"); + if (!isnan(ubl.z_values[x_plot][y_plot])) { + lcd_print(ftostr43sign(ubl.z_values[x_plot][y_plot])); + } + else { + lcd_print(" -----"); + } + } + + #endif // DOGLCD + + /** + * UBL LCD Map Movement + */ + void ubl_map_move_to_xy() { + current_position[X_AXIS] = LOGICAL_X_POSITION(pgm_read_float(&ubl._mesh_index_to_xpos[x_plot])); + current_position[Y_AXIS] = LOGICAL_Y_POSITION(pgm_read_float(&ubl._mesh_index_to_ypos[y_plot])); + planner.buffer_line_kinematic(current_position, MMM_TO_MMS(XY_PROBE_SPEED), active_extruder); + } + + /** + * UBL LCD "radar" map + */ + void set_current_from_steppers_for_axis(const AxisEnum axis); + void sync_plan_position(); + + void _lcd_ubl_output_map_lcd() { + static int step_scaler=0; + int32_t signed_enc_pos; + + defer_return_to_status = true; + + if (axis_known_position[X_AXIS] && axis_known_position[Y_AXIS] && axis_known_position[Z_AXIS]) { + + if (lcd_clicked) { return _lcd_ubl_map_lcd_edit_cmd(); } + ENCODER_DIRECTION_NORMAL(); + + if (encoderPosition != 0) { + signed_enc_pos = (int32_t)encoderPosition; + step_scaler += signed_enc_pos; + x_plot = (x_plot + step_scaler / ENCODER_STEPS_PER_MENU_ITEM); + + if (abs(step_scaler) >= ENCODER_STEPS_PER_MENU_ITEM) + step_scaler = 0; + refresh_cmd_timeout(); + + lcdDrawUpdate = LCDVIEW_REDRAW_NOW; + } + + encoderPosition = 0; + + // Encoder to the right (++) + if (x_plot >= GRID_MAX_POINTS_X) { x_plot = 0; y_plot++; } + if (y_plot >= GRID_MAX_POINTS_Y) y_plot = 0; + + // Encoder to the left (--) + if (x_plot <= GRID_MAX_POINTS_X - (GRID_MAX_POINTS_X + 1)) { x_plot = GRID_MAX_POINTS_X - 1; y_plot--; } + if (y_plot <= GRID_MAX_POINTS_Y - (GRID_MAX_POINTS_Y + 1)) y_plot = GRID_MAX_POINTS_Y - 1; + + // Prevent underrun/overrun of plot numbers + x_plot = constrain(x_plot, GRID_MAX_POINTS_X - (GRID_MAX_POINTS_X + 1), GRID_MAX_POINTS_X + 1); + y_plot = constrain(y_plot, GRID_MAX_POINTS_Y - (GRID_MAX_POINTS_Y + 1), GRID_MAX_POINTS_Y + 1); + + // Determine number of points to edit + #if IS_KINEMATIC + n_edit_pts = 9; //TODO: Delta accessible edit points + #else + if (x_plot < 1 || x_plot >= GRID_MAX_POINTS_X - 1) + if (y_plot < 1 || y_plot >= GRID_MAX_POINTS_Y - 1) n_edit_pts = 4; // Corners + else n_edit_pts = 6; + else if (y_plot < 1 || y_plot >= GRID_MAX_POINTS_Y - 1) n_edit_pts = 6; // Edges + else n_edit_pts = 9; // Field + #endif + + if (lcdDrawUpdate) { + #if ENABLED(DOGLCD) + _lcd_ubl_plot_drawing_prep(); + #else + _lcd_ubl_output_char_lcd(); + #endif + + ubl_map_move_to_xy(); // Move to current location + + if (planner.movesplanned()>1) { // if the nozzle is moving, cancel the move. There is a new location + #define ENABLE_STEPPER_DRIVER_INTERRUPT() SBI(TIMSK1, OCIE1A) + #define DISABLE_STEPPER_DRIVER_INTERRUPT() CBI(TIMSK1, OCIE1A) + DISABLE_STEPPER_DRIVER_INTERRUPT(); + while (planner.blocks_queued()) planner.discard_current_block(); + stepper.current_block = NULL; + planner.clear_block_buffer_runtime(); + ENABLE_STEPPER_DRIVER_INTERRUPT(); + set_current_from_steppers_for_axis(ALL_AXES); + sync_plan_position(); + ubl_map_move_to_xy(); // Move to new location + } + } + safe_delay(10); + } + else lcd_goto_screen(_lcd_ubl_map_homing); + } + + /** + * UBL Homing before LCD map + */ + void _lcd_ubl_output_map_lcd_cmd() { + if (!(axis_known_position[X_AXIS] && axis_known_position[Y_AXIS] && axis_known_position[Z_AXIS])) + enqueue_and_echo_commands_P(PSTR("G28")); + lcd_goto_screen(_lcd_ubl_map_homing); } /** @@ -2059,9 +2271,10 @@ void kill_screen(const char* lcd_msg) { void _lcd_ubl_output_map() { START_MENU(); MENU_BACK(MSG_UBL_LEVEL_BED); - MENU_ITEM_EDIT(int3, MSG_UBL_MAP_TYPE, &map_type, 0, 1); - if (map_type == 0) MENU_ITEM(function, MSG_UBL_OUTPUT_MAP_HOST, _lcd_ubl_output_map_cmd); - if (map_type == 1) MENU_ITEM(function, MSG_UBL_OUTPUT_MAP_CSV, _lcd_ubl_output_map_cmd); + MENU_ITEM(gcode, MSG_UBL_OUTPUT_MAP_HOST, PSTR("G29 T0")); + MENU_ITEM(gcode, MSG_UBL_OUTPUT_MAP_CSV, PSTR("G29 T1")); + MENU_ITEM(gcode, MSG_UBL_OUTPUT_MAP_BACKUP, PSTR("G29 S-1")); + MENU_ITEM(function, MSG_UBL_OUTPUT_MAP, _lcd_ubl_output_map_lcd_cmd); END_MENU(); } @@ -2091,8 +2304,10 @@ void kill_screen(const char* lcd_msg) { * Load Bed Mesh * Save Bed Mesh * - Output Map - * Map Type: - * Output Bed Mesh Host / Output Bed Mesh CSV + * Topography to Host + * CSV for Spreadsheet + * Mesh Output Backup + * Output to LCD Grid * - UBL Tools * - Build Mesh * Build PLA Mesh @@ -4035,7 +4250,7 @@ void lcd_update() { int32_t encoderMovementSteps = abs(encoderDiff) / ENCODER_PULSES_PER_STEP; if (lastEncoderMovementMillis != 0) { - // Note that the rate is always calculated between to passes through the + // Note that the rate is always calculated between two passes through the // loop and that the abs of the encoderDiff value is tracked. float encoderStepRate = (float)(encoderMovementSteps) / ((float)(ms - lastEncoderMovementMillis)) * 1000.0; diff --git a/Marlin/ultralcd_impl_DOGM.h b/Marlin/ultralcd_impl_DOGM.h index 29445262c..676dfcacf 100644 --- a/Marlin/ultralcd_impl_DOGM.h +++ b/Marlin/ultralcd_impl_DOGM.h @@ -50,6 +50,10 @@ #include +#if ENABLED(AUTO_BED_LEVELING_UBL) + #include "ubl.h" +#endif + #if ENABLED(SHOW_BOOTSCREEN) && ENABLED(SHOW_CUSTOM_BOOTSCREEN) #include "_Bootscreen.h" #endif diff --git a/Marlin/ultralcd_impl_HD44780.h b/Marlin/ultralcd_impl_HD44780.h index 7bc901854..fe21c0ae8 100644 --- a/Marlin/ultralcd_impl_HD44780.h +++ b/Marlin/ultralcd_impl_HD44780.h @@ -1076,4 +1076,12 @@ static void lcd_implementation_status_screen() { #endif // LCD_HAS_STATUS_INDICATORS +#ifdef AUTO_BED_LEVELING_UBL + void lcd_return_to_status(); // These are just place holders for the 20x4 LCD work that + void _lcd_ubl_output_char_lcd() { // is coming up very soon. Soon this will morph into the + lcd_return_to_status(); // real code. + } + +#endif // AUTO_BED_LEVELING_UBL + #endif // ULTRALCD_IMPL_HD44780_H