[12] Last updated 2010-07-30 12:49:28

A really long time ago now, I ordered myself a Pertelian X2040 LCD display. I wanted it mostly as a toy, but it has turned out to be incredibly useful to me.

I found that most of the software available for it under Linux either lacked the feature I wanted, or were so jam-packed with features I didn't need, so I decided to see what I could do about writing my own. While I waited for my display to arrive in the mail I sat down with the owners manual and started writing some Perl to solve my problem.

The result is shown further down.

It's written for the Pertelian X2040, but it might work very well with other character displays, even if it might need some modification. The code is not very elegant, but it was written for maintainability and readability, and without access to actual hardware to test with, but it worked so well when the display arrived that I didn't really need to modify it much.

The code is licensed to you under ESL.
I realize the use of an un-instanced heap like that is a horrible thing to do, but it made it allowed me to debug it faster while I had multiple DemmyLCD objects. Feel free to patch it, as it should be a piece of cake to change.
Also, if you look closely at the code, you'll realize this is the least of your worries... It does work, however, and you're welcome to it.

package DemmyLCD;

BEGIN {
  use strict;
  use warnings;
  use Time::HiRes qw{sleep};
  use IO::File;
  our $VERSION = 0.5;
}

my $usedev = '/dev/ttyUSB0';
my %heap;

my %instruction = (
  clear => chr(254).chr(1),
  lightoff => chr(254).chr(2),
  lighton => chr(254).chr(3),
  entry => chr(254).chr(6),  
  off => chr(254).chr(8),
  on => chr(254).chr(12),
  init => chr(254).chr(0x38),
);

sub new { # Takes one extra argument: Device path
  my $self = shift;
  my $device = shift;
  $device = $usedev unless $device;
  if (-c $device){
    $usedev = $device;
  }else{
    die("Device specified, $device, does not exist, or is not a character device.");
  } 
  my $this = {};
  bless($this);
  $heap{$this}{dev} = IO::File->new(">$usedev") or die "Could not open $usedev: $!";
  $heap{$this}{dev}->autoflush(1);
  $this->out($instruction{on});
  $this->out($instruction{init});
  $this->light('on');
  $heap{$this}{light} = 1;
  $this->character(0,
    '00000',
    '01111',
    '01111',
    '01100',
    '01100',
    '01100',
    '01100',
    '01100',
  );

  return $this;
}
sub out { # Output to device, ment for internal use only
  my $this = shift;
  my @charstream = split("",join("",@_));
  foreach (@charstream){
    $heap{$this}{dev}->print($_);
    sleep(0.003);  # See POD for this!
  }
}
sub light { # Takes an optional argument ('on' or 'off'), argumentless will toggle
  my ($this,$status) = @_;
  $status = '' unless $status;
  if ($status eq 'off'){
    $this->out($instruction{lightoff});
    $heap{$this}{light} = 0;
  }elsif ($status eq 'on'){
    $this->out($instruction{lighton});
    $heap{$this}{light} = 1;
  }else{
    if ($heap{$this}{light}){
      $this->out($instruction{lightoff});
      $heap{$this}{light} = 0;
    }else{
      $this->out($instruction{lighton});
      $heap{$this}{light} = 1;
    }
  }
}
sub clear { # Clears the display
  my $this = shift;
  $this->out($instruction{clear});
  $this->locate;
}
sub locate { # Sets the "print-from-location" of the display
  # Pretty much stolen from "rudedog", with some adaptations.
  # http://pertelian.com/forums/viewtopic.php?t=19

  # Uses chr() and not pack() to avoid a warning:
  # "Character in 'c' format wrapped in pack"

  my ($this,$line,$char,$text) = @_;
  $text = '' unless $text;

  $line = 1 unless $line;
  $line = 1 unless $line < 5;
  $line = 1 unless $line > 0;

  $char = 1 unless $char;
  $char = 1 unless $char < 21;
  $char = 1 unless $char > 0;

  my @offsets = (
    undef,
    0x80 + -1  + $char,
    0x80 + 63 + $char,
    0x80 + 19 + $char,
    0x80 + 83 + $char,
  );
  my $offset = chr(254).chr($offsets[$line]).$text;
  $this->out($offset);
}
sub print { # Takes an unlimited number of arguments and prints it to the display
  my $this = shift;
  my $text = join(" ",@_);
  $this->out($instruction{entry},$text);
}
sub close { # Closes the filehandle, for when you don't want to turn the display off after use
  my $this = shift;
  $heap{$this}{dev}->close;
  $this = undef;
}
sub off {  # Turns off the display (clear, turn off light, turn off device, close filehandle)
  my $this = shift;
  $this->clear;
  $this->light('off');
  $this->out($instruction{off});
  $this->close;
}
sub test { # Test (light on, offset test, print test, full-screen message)
  my $this = shift;
  $this->light('on');
  $this->locate(1,1); $this->print('+------------------+');
  $this->locate(2,1); $this->print('| TESTING  TESTING |');
  $this->locate(3,1); $this->print('| Is this working? |');
  $this->locate(4,1); $this->print('+------------------+');
}
sub blank { # Blanks the specified line
  my ($this,$line) = @_;
  $this->locate($line,1);
  $this->print('                    ');
}
sub character { # Sets up special characters  -- DOES NOT WORK YET
  my $this = shift;
  my $position = shift;
  my @bitstrigns = @_;
  $#bitstrings = 8;

}

1;

__END__

=head1 NAME

  DemmyLCD

=head1 SYNOPSIS

  use DemmyLCD;
  my $lcd = new DemmyLCD;
  $lcd->clear;
  $lcd->locate(3,2,'Hello world');
  $lcd->close;

=head1 DESCRIPTION AND AUTHOR INFORMATION

  DemmyLCD - Fredik "Demonen" Vold's LCD character device interface module.
  Written for Pertelian x2040 displays, but might work for other displays.
  Some code borrowed, or at least greatly inspired by, people over at http://pertelian.com/forums/

  Most of the module was actually written while waiting for the display to arrive, then adjusted to the real world when it did.
  The code is licensed under ESL - http://fredrikvold.info/ESL.htm

=head1 DELAY NOTE

  All characters are streamed to the character device with a 0.003 second delay between them.
  This is to make sure the display can keep up, even with the instructions.
  You shouldn't notice, unless you're updating the display a whole lot.
  It uses Time::HiRes::sleep(0.003), see the "out" method documentation for more on this.

=head1 METHODS

  Except for the new method, nothing is ever returned.
  I hope to be able to provide a true or false return based on success, but it might prove difficult to determine if a command is successful.

=head2

new

=over 4

  Takes one optional argument:  The path to device to be opened.
  The default value is /dev/ttyUSB0.  The default is usually fine.

  It is safe to open multiple displays this way, specifying a device path for all except the one at /dev/ttyUSB0

  Turns the display on and enables the backlight.

=back

=head2

clear

=over 4

  Clear the display.  Does not take any arguments.

=back

=head2

light

=over 4

  Takes an optional argument for the desired state of the backlight.
  $lcd->light('on') for on and $lcd->light('off') for off.
  If no argument is given, the light is toggled.

=back

=head2

locate

=over 4

  Takes two optional arguments, the first is a row, the second is a caracter.
  $lcd->locate(3,10,'foo') puts the cursor on the third row, 10th character, and prints the string "foo" there.
  To specify a character, a row must be given.

  The optional third argument is the text you want at that location.
  If given with no arguments, location is reset to 1,1 (top left character)

=back

=head2

print

=over 4

  Takes an unlimited number of arguments, and will print theese to the display with a space between them.

    $lcd->print("One",2,"Three")
  will result in the string 
    One 2 Three
  being printed to the display

=back

=head2

blank

=over 4

  Blanks the specified line.

  For example,
    $lcd->blank(2)
  blanks the second line.

  It actually uses the locate method for the line, then the print method to output 20 spaces.

=back

=head2

close

=over 4

  Closes the filehandle without turning off the display.
  Useful for when you're done with the display, but want it to remain on.

=back

=head2

off

=over 4

  Clears the display, turns the light off, turns the display off and closes the filehandle.
  For when you're really really done with the display.
  To pretend it's off, use the clear and light methods to blank and darken.

=back

=head2

test

=over 4

  Turns the light on and displays a test screen:

  +------------------+
  | TESTING  TESTING |
  | Is this working? |
  +------------------+

=back

=head2

out

=over 4

  Intended for internal use, but can be called from the outside as an alternative to the print method.
  See the Pertelian display manual for any magic characters you can use.

  Any arguments are joined together without a splitting character before being relayed to the filehandle.

  All character data sent to the device that passes through this sub/method is given a slight delay between characters.
  At least on my system, ~0.004 seems to be the shortest possible sleep time for Time::HiRes
  According to the documentation, the needed value is somewhere around 0.0002 (two milliseconds)

  If you're putting enough data on the display to notice a difference, I think the real issue is more likely to be ...
    WHY are you putting so much data on a 4x20 display?!
  I suggest you filter the data first so there is no need for more frequent updates.


  The process is actually joining to one string, then splitting into an array of characters, then sending each to the filehandle followed by a slight delay.

=back

=head1 CONTACT INFORMATION

  To get in touch with me, Demonen, the easiest way is to use demmydemon@gmail.com

Markdown source