# LiquidCrystal.pm
#
# Perl-library to drive LCD-displays controlled by Hitachi HD44780 LCD controller
# 
# Copyright (C) 2013/2014 Norbert Truchsess (norbert.truchsess@t-online.de)
# 
# based on LiquidCrystal.cpp as of:
#
# www.DFRobot.com
# last updated on 21/12/2011
# Tim Starling Fix the reset bug (Thanks Tim)
# wiki doc http://www.dfrobot.com/wiki/index.php?title=I2C/TWI_LCD1602_Module_(SKU:_DFR0063)
# Support Forum: http://www.dfrobot.com/forum/
# Compatible with the Arduino IDE 1.0
# Library version:1.1
#
# When the display powers up, it is configured as follows:
#
# 1. Display clear
# 2. Function set:
#    DL = 1; 8-bit interface data
#    N = 0; 1-line display
#    F = 0; 5x8 dot character font
# 3. Display on/off control:
#    D = 0; Display off
#    C = 0; Cursor off
#    B = 0; Blinking off
# 4. Entry mode set:
#    I/D = 1; Increment by 1
#    S = 0; No shift
#
# Note, however, that resetting the Arduino doesn't reset the LCD, so we
# can't assume that its in that state when a sketch starts (and the
# LiquidCrystal constructor is called).

package LiquidCrystal;

use warnings;
use strict;

#Basic commands and constants
use constant LCD_CLEARDISPLAY   => 0x01;
use constant LCD_RETURNHOME     => 0x02;
use constant LCD_ENTRYMODESET   => 0x04;
use constant LCD_DISPLAYCONTROL => 0x08;
use constant LCD_CURSORSHIFT    => 0x10;
use constant LCD_FUNCTIONSET    => 0x20;
use constant LCD_SETCGRAMADDR   => 0x40;
use constant LCD_SETDDRAMADDR   => 0x80;

# flags for display entry mode
use constant LCD_ENTRYRIGHT          => 0x00;
use constant LCD_ENTRYLEFT           => 0x02;
use constant LCD_ENTRYSHIFTINCREMENT => 0x01;
use constant LCD_ENTRYSHIFTDECREMENT => 0x00;

# flags for display on/off control
use constant LCD_DISPLAYON  => 0x04;
use constant LCD_DISPLAYOFF => 0x00;
use constant LCD_CURSORON   => 0x02;
use constant LCD_CURSOROFF  => 0x00;
use constant LCD_BLINKON    => 0x01;
use constant LCD_BLINKOFF   => 0x00;

# flags for display/cursor shift
use constant LCD_DISPLAYMOVE => 0x08;
use constant LCD_CURSORMOVE  => 0x00;
use constant LCD_MOVERIGHT   => 0x04;
use constant LCD_MOVELEFT    => 0x00;

# flags for function set
use constant LCD_8BITMODE => 0x10;
use constant LCD_4BITMODE => 0x00;
use constant LCD_2LINE    => 0x08;
use constant LCD_1LINE    => 0x00;
use constant LCD_5x10DOTS => 0x04;
use constant LCD_5x8DOTS  => 0x00;

# flags for backlight control
use constant LCD_BACKLIGHT   => 0x08;
use constant LCD_NOBACKLIGHT => 0x00;

use constant En => 0b00000100;    # Enable bit
use constant Rw => 0b00000010;    # Read / Write bit
use constant Rs => 0b00000001;    # Register select bit

our %mapping_bits = (
  RS  => 0,
  RW  => 1,
  E   => 2,
  LED => 3,
  D4  => 4,
  D5  => 5,
  D6  => 6,
  D7  => 7,
);

sub print($$) {
	my ($self,$c) = @_;
	my @buf = unpack "c*",$c;
	foreach my $s (@buf) {
		$self->write($s);		
	}
};

sub write($$) {
	my ( $self, $value ) = @_;
	$self->send( $value, Rs );
	return 0;
}

sub new($$$$@) {
	my ( $class, $lcd_cols, $lcd_rows ) = @_;

  my $lcd = bless {
		cols         => $lcd_cols,
		rows         => $lcd_rows,
		backlightval => LCD_NOBACKLIGHT,
	}, $class;
	$lcd->setMapping();
	return $lcd;
}

#  $lcd_mapping = {
#  'P0' => 'RS',
#  'P1' => 'RW',
#  'P2' => 'E',
#  'P3' => 'LED',
#  'P4' => 'D4',
#  'P5' => 'D5',
#  'P6' => 'D6',
#  'P7' => 'D7',
#};

sub setMapping(@) {
  my ( $self, $lcd_mapping ) = @_;

  my %mapping;
  if ( defined $lcd_mapping ) {
    while (my ($key, $value) = each %$lcd_mapping) {
      die "illegal value for MAPPING: $value, must be one of RS,RW,E,LED,D4,D5,D6,D7" unless grep {$value eq $_} keys %mapping_bits;
      die "illegal value for PIN: $key, must be one of P0..P7" unless $key =~ /^P([\d])$/;
      $mapping{0x01 << $mapping_bits{$value}} = 0x01 << $1; 
    }
  }	else {
    foreach my $i (0..7) {
      $mapping{0x01 << $i} = 0x01 << $i;      
    }
  }
  $self->{mapping} = \%mapping;
}

sub init($) {
	my $self = shift;
	$self->init_priv();
}

sub attach($$) {
	my ($self,$dev) = @_;
	$self->{io} = $dev;
}

sub init_priv($) {
	my $self = shift;

	$self->{displayfunction} =
	  LCD_4BITMODE | LCD_1LINE |
	  LCD_5x8DOTS;
	$self->begin( $self->{cols}, $self->{rows} );
}

sub begin($$$$) {
	my ( $self, $cols, $lines, $dotsize ) = @_;
	if ( $lines > 1 ) {
		$self->{displayfunction} |= LCD_2LINE;
	}
	$self->{numlines} = $lines;

	# for some 1 line displays you can select a 10 pixel high font
	if ( (defined $dotsize) && ($dotsize != 0) && ( $lines == 1 ) ) {
		$self->{displayfunction} |= LCD_5x10DOTS;
	}

  # SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION!
  # according to datasheet, we need at least 40ms after power rises above 2.7V
  # before sending commands. Arduino can turn on way befer 4.5V so we'll wait 50
	select( undef, undef, undef, 0.050 );

	# Now we pull both RS and R/W low to begin commands
	$self->expanderWrite( $self->{backlightval} )
	  ;    # reset expanderand turn backlight off (Bit 8 =1)
	select( undef, undef, undef, 1 );

	#put the LCD into 4 bit mode
	# this is according to the hitachi HD44780 datasheet
	# figure 24, pg 46

	# we start in 8bit mode, try to set 4 bit mode
	$self->write4bits( 0x03 << 4 );
	select( undef, undef, undef, 0.0045 );    # wait min 4.1ms

	# second try
	$self->write4bits( 0x03 << 4 );
	select( undef, undef, undef, 0.0045 );    # wait min 4.1ms

	# third go!
	$self->write4bits( 0x03 << 4 );
	select( undef, undef, undef, 0.00015 );

	# finally, set to 4-bit interface
	$self->write4bits( 0x02 << 4 );

	# set # lines, font size, etc.
	$self->command(
		LCD_FUNCTIONSET | $self->{displayfunction} );

	# turn the display on with no cursor or blinking default
	$self->{displaycontrol} =
	  LCD_DISPLAYON | LCD_CURSOROFF |
	  LCD_BLINKOFF;
	$self->display();

	# clear it off
	$self->clear();

	# Initialize to default text direction (for roman languages)
	$self->{displaymode} =
	  LCD_ENTRYLEFT |
	  LCD_ENTRYSHIFTDECREMENT;

	# set the entry mode
	$self->command(
		LCD_ENTRYMODESET | $self->{displaymode} );

	$self->home();

}

#********** high level commands, for the user!

sub clear($) {
	my $self = shift;
	$self->command(LCD_CLEARDISPLAY)
	  ;    # clear display, set cursor position to zero
	select( undef, undef, undef, 0.002 );    # this command takes a long time!
}

sub home($) {
	my $self = shift;
	$self->command(LCD_RETURNHOME)
	  ;                                      # set cursor position to zero
	select( undef, undef, undef, 0.002 );    # this command takes a long time!
}

sub setCursor($$$) {
	my ( $self, $col, $row ) = @_;
	my @row_offsets = ( 0x00, 0x40, 0x14, 0x54 );
	if ( $row > $self->{numlines} ) {
		$row = $self->{numlines} - 1;        # we count rows starting w/0
	}
	$self->command(
		LCD_SETDDRAMADDR | ( $col + $row_offsets[$row] ) );
}

# Turn the display on/off (quickly)
sub noDisplay($) {
	my $self = shift;
	$self->{displaycontrol} &=
	  ~LCD_DISPLAYON;    #TODO validate '~'
	$self->command(
		LCD_DISPLAYCONTROL | $self->{displaycontrol} );
}

sub display($) {
	my $self = shift;
	$self->{displaycontrol} |= LCD_DISPLAYON;
	$self->command(
		LCD_DISPLAYCONTROL | $self->{displaycontrol} );
}

# Turns the underline cursor on/off
sub noCursor($) {
	my $self = shift;
	$self->{displaycontrol} &=
	  ~LCD_CURSORON;    #TODO validate '~'
	$self->command(
		LCD_DISPLAYCONTROL | $self->{displaycontrol} );
}

sub cursor($) {
	my $self = shift;
	$self->{displaycontrol} |= LCD_CURSORON;
	$self->command(
		LCD_DISPLAYCONTROL | $self->{displaycontrol} );
}

# Turn on and off the blinking cursor
sub noBlink($) {
	my $self = shift;
	$self->{displaycontrol} &=
	  ~LCD_BLINKON;    #TODO validate '~'
	$self->command(
		LCD_DISPLAYCONTROL | $self->{displaycontrol} );
}

sub blink($) {
	my $self = shift;
	$self->{displaycontrol} |= LCD_BLINKON;
	$self->command(
		LCD_DISPLAYCONTROL | $self->{displaycontrol} );
}

# These commands scroll the display without changing the RAM
sub scrollDisplayLeft($) {
	my $self = shift;
	$self->command( LCD_CURSORSHIFT |
		  LCD_DISPLAYMOVE |
		  LCD_MOVELEFT );
}

sub scrollDisplayRight($) {
	my $self = shift;
	$self->command( LCD_CURSORSHIFT |
		  LCD_DISPLAYMOVE |
		  LCD_MOVERIGHT );
}

# This is for text that flows Left to Right
sub leftToRight($) {
	my $self = shift;
	$self->{displaymode} |= LCD_ENTRYLEFT;
	$self->command(
		LCD_ENTRYMODESET | $self->{displaymode} );
}

# This is for text that flows Right to Left
sub rightToLeft($) {
	my $self = shift;
	$self->{displaymode} &=
	  ~LCD_ENTRYLEFT;    #TODO validate '~'
	$self->command(
		LCD_ENTRYMODESET | $self->{displaymode} );
}

# This will 'right justify' text from the cursor
sub autoscroll($) {
	my $self = shift;
	$self->{displaymode} |= LCD_ENTRYSHIFTINCREMENT;
	$self->command(
		LCD_ENTRYMODESET | $self->{displaymode} );
}

# This will 'left justify' text from the cursor
sub noAutoscroll($) {
	my $self = shift;
	$self->{displaymode} &=
	  ~LCD_ENTRYSHIFTINCREMENT;    #TODO validate '~'
	$self->command(
		LCD_ENTRYMODESET | $self->{displaymode} );
}

# Allows us to fill the first 8 CGRAM locations
# with custom characters
sub createChar($$$) {
	my ( $self, $location, $charmap ) = @_;
	$location &= 0x7;    # we only have 8 locations 0-7
	$self->command( LCD_SETCGRAMADDR | ( $location << 3 ) );
	for ( my $i = 0 ; $i < 8 ; $i++ ) {
		$self->write( @$charmap[$i] );
	}
}

# Turn the (optional) backlight off/on
sub noBacklight($) {
	my $self = shift;
	$self->{backlightval} = LCD_NOBACKLIGHT;
	$self->expanderWrite(0);
}

sub backlight($) {
	my $self = shift;
	$self->{backlightval} = LCD_BACKLIGHT;
	$self->expanderWrite(0);
}

#*********** mid level commands, for sending data/cmds

sub command($$) {
	my ( $self, $value ) = @_;
	$self->send( $value, 0 );
}

#************ low level data pushing commands **********

# write either command or data
sub send($$$) {
	my ( $self, $value, $mode ) = @_;
	my $highnib = $value & 0xf0;
	my $lownib  = ( $value << 4 ) & 0xf0;
	$self->write4bits( ($highnib) | $mode );
	$self->write4bits( ($lownib) | $mode );
}

sub write4bits($$) {
	my ( $self, $value ) = @_;
	$self->expanderWrite($value);
	$self->pulseEnable($value);
}

sub expanderWrite($$) {
	my ( $self, $data ) = @_;
	
	$data |= $self->{backlightval};
	
	my $mapped = 0;
	
	while (my ($orig, $remapped) = each %{$self->{mapping}}) {
	  $mapped |= $data & $orig ? $remapped : 0; 
	}
	
	$self->{io}->write($mapped);
}

sub pulseEnable($$) {
	my ( $self, $data ) = @_;
	$self->expanderWrite( $data | En );    # En high
	select( undef, undef, undef, 0.000001 );    # enable pulse must be >450ns
	$self->expanderWrite( $data & ~En )
	  ;                                         # En low TODO: validate '~'
	select( undef, undef, undef, 0.000050 );    # commands need > 37us to settle
}

# Alias functions

sub cursor_on($) {
	my $self = shift;
	$self->cursor();
}

sub cursor_off($) {
	my $self = shift;
	$self->noCursor();
}

sub blink_on($) {
	my $self = shift;
	$self->blink();
}

sub blink_off($) {
	my $self = shift;
	$self->noBlink();
}

sub load_custom_character($$$) {
	my ( $self, $char_num, $rows ) = @_;
	$self->createChar( $char_num, $rows );
}

sub setBacklight($$) {
	my ( $self, $new_val ) = @_;
	if ($new_val) {
		$self->backlight();    # turn backlight on
	}
	else {
		$self->noBacklight();    # turn backlight off
	}
}

# unsupported API functions
sub off($)         { }
sub on($)          { }
sub setDelay ($$$) { }

sub status($) {
	return 0;
}

sub keypad ($) {
	return 0;
}

sub init_bargraph($$) {
	return 0;
}

sub draw_horizontal_graph($$$$$) { }
sub draw_vertical_graph($$$$$)   { }
sub setContrast($$)              { }

1;
