/*
 * keyblcd.c - simple Keyboard and LCD device driver
 *
 * (c) 2006  Avelino Herrera Morales
 * M.H.P. Sistemas de Control S.L.
 * http://www.mhpsc.com
 * http://avelino.gabiot.com/soft
 *
 * Drives a simple AT english keyboard and an HD44780 based LCD. The
 * associated device is /dev/keyblcd (char) with major=250 and minor=0.
 * Reading from this device causes reading from AT keyboard and writing to
 * this device causes writing to LCD screen.
 *
 * Supported #defines:
 *
 * CONFIG_KEYBLCD_CONSOLE --> if defined, register keyblcd as a kernel
 * console.
 * CONFIG_KEYBLCD_GRAB_KEYBOARD --> if defined, grab keyboard ports and irq
 * and acts as a minimalistic keyboard input device.
 *
 *
 * PIN DEFINITIONS
 * ------------------------------------------------
 * PRINTER PORT             LCD
 * GND (1)
 * Vcc (2)
 * Vee (3) (contrast adjust)
 * D7 (9)                   RS (4)
 * INIT (16)                R/W (5)
 * STROBE (1)               E  (6)
 * D0 (2)                   D0 (7)
 * D1 (3)                   D1 (8)
 * D2 (4)                   D2 (9)
 * D3 (5)                   D3 (10)
 * D4 (6)                   D4 (11)
 * D5 (7)                   D5 (12)
 * D6 (8)                   D6 (13)
 * AUTO (14)                D7 (14)
 * SELECT (17)              LIGHT (4K7 resistor from the NPN transistor base)
 * 
 * 
 * PORT BIT DEFINITIONS
 * ----------------------------
 * LP         DATA BUS TO LCD
 * LP+1       INPUTS
 * BIT 2 - !IRQ
 * BIT 3 - !ERROR
 * BIT 4 - SELECT
 * BIT 5 - PE
 * BIT 6 - !ACK
 * BIT 7 - BUSY
 * LP+2       OUTPUTS
 * BIT 0 - !STROBE      *
 * BIT 1 - !AUTO        *
 * BIT 2 - INIT         *
 * BIT 3 - !SELECT      *
 * BIT 4 - IRQENABLE
 * BIT 5 - DIRECTION
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */
#include <linux/config.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/ioport.h>
#include <linux/sched.h>
#include <linux/ctype.h>
#include <linux/timer.h>
#include <linux/delay.h>
#include <linux/types.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/version.h>
#include <linux/malloc.h>
#include <linux/init.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#ifdef  CONFIG_KEYBLCD_CONSOLE
#include <linux/console.h>
#endif

#define  DEVICE_MAJOR  250
#define  DEVICE_MINOR  0

#define  LP0  0x378
#define  LP1  0x278
#define  LP   LP0

#ifdef  CONFIG_KEYBLCD_GRAB_KEYBOARD

#define  LCTRL_SCAN           29
#define  LSHIFT_SCAN          42
#define  RSHIFT_SCAN          54
#define  LCTRL_SCAN_RELEASE   (29 | 0x80)
#define  LSHIFT_SCAN_RELEASE  (42 | 0x80)
#define  RSHIFT_SCAN_RELEASE  (54 | 0x80)

#define  KEY_RELEASED(v)        (((v) & 0x80) > 0)
#define  KEY_SHIFT_PRESSED(v)   (((v) == LSHIFT_SCAN) || ((v) == RSHIFT_SCAN))
#define  KEY_SHIFT_RELEASED(v)  (((v) == LSHIFT_SCAN_RELEASE) || ((v) == RSHIFT_SCAN_RELEASE))
#define  KEY_CTRL_PRESSED(v)    ((v) == LCTRL_SCAN)
#define  KEY_CTRL_RELEASED(v)   ((v) == LCTRL_SCAN_RELEASE)

char normal_code_table[] = " \e1234567890-=\b\tqwertyuiop[]\n asdfghjkl;'` \\zxcvbnm,./ *               789-456+1230.     ";
char shift_code_table[]  = "  !@#$%^&*()_+  QWERTYUIOP{}  ASDFGHJKL:\"~ |ZXCVBNM<>? *               789-456+1230.     ";
char ctrl_code_table[]  = "                \x11""\x17""\x05""\x12""\x14""\x19""\x15""\x09""\x0F""\x10""\x1B""\x1D""  \x01""\x13""\x04""\x06""\x07""\x08""\x0A""\x0B""\x0C""    \x1C""\x1A""\x18""\x03""\x16""\x02""\x0E""\x0D""                    789-456+1230.     ";
char *current_code_table = normal_code_table;

#define  KEYBOARD_BUFFER_SIZE     1024

char keyboard_buffer[KEYBOARD_BUFFER_SIZE];
int keyboard_input_ptr = 0;
int keyboard_output_ptr = 0;
struct wait_queue *keyboard_wait_queue = NULL;

#endif  /* CONFIG_KEYBLCD_GRAB_KEYBOARD */

#define  LCD_BUFFER_SIZE          1024
#define  LCD_COMMAND_BUFFER_SIZE  2048

char lcd_buffer[LCD_BUFFER_SIZE];
int lcd_input_ptr = 0;
int lcd_output_ptr = 0;
struct timer_list lcd_timer;
struct timer_list sound_timer;

#define  ROWS  4
#define  COLS  20
#define  LCD_HANDLER_STATUS_INITIAL                0
#define  LCD_HANDLER_STATUS_WAIT_FOR_DATA          1
#define  LCD_HANDLER_STATUS_WAIT_FOR_DATA_LOOP     2
#define  LCD_HANDLER_STATUS_DATA                   3
#define  LCD_HANDLER_STATUS_WAIT_FOR_COMMAND       4
#define  LCD_HANDLER_STATUS_WAIT_FOR_COMMAND_LOOP  5
#define  LCD_HANDLER_STATUS_COMMAND                6

char showed_screen[ROWS][COLS];
int lcd_current_row = 0;
int lcd_current_col = 0;
int lcd_handler_status = LCD_HANDLER_STATUS_INITIAL;
int lcd_retry = 0;
char lcd_data = 0;
unsigned char lcd_scrolling = 0;
int lcd_row = -1;
int lcd_col = -1;
unsigned char lcd_line_offset[] = {0x00, 0x40, 0x14, 0x54};

#define  LCD_TOKEN_EMPTY   0
#define  LCD_TOKEN_CHAR    1
#define  LCD_TOKEN_NUMBER  2

typedef struct {
    int type;
    int value;
} lcd_token;

#define  LCD_VT100_EMPTY                  0
#define  LCD_VT100_CHAR                   1
#define  LCD_VT100_CURSOR_HOME            2
#define  LCD_VT100_CURSOR_UP              3
#define  LCD_VT100_CURSOR_DOWN            4
#define  LCD_VT100_CURSOR_FORWARD         5
#define  LCD_VT100_CURSOR_BACKWARD        6
#define  LCD_VT100_SAVE_CURSOR            7
#define  LCD_VT100_RESTORE_CURSOR         8
#define  LCD_VT100_ENABLE_SCROLL          9
#define  LCD_VT100_SCROLL_DOWN            10
#define  LCD_VT100_SCROLL_UP              11
#define  LCD_VT100_CLREOL                 12
#define  LCD_VT100_CLRSOL                 13
#define  LCD_VT100_CLRLINE                14
#define  LCD_VT100_CLRUP                  15
#define  LCD_VT100_CLRDOWN                16
#define  LCD_VT100_CLRSCR                 17
#define  LCD_VT100_RESET                  18
#define  LCD_VT100_EXTENSION_HIDE_CURSOR  19
#define  LCD_VT100_EXTENSION_SHOW_CURSOR  20

typedef struct {
    int command;
    int value1;
    int value2;
} lcd_vt100_command;

int lcd_vt100_status;
int lcd_vt100_value1;
int lcd_vt100_value2;

#define  LCD_COMMAND_COMMAND  0
#define  LCD_COMMAND_CHAR     1
#define  LCD_RESERVED_COMMAND_NEWLINE  0

typedef struct {
    unsigned char type;
    unsigned char data;
    unsigned char scrolling;
    int row;
    int col;
} lcd_command;

lcd_command lcd_command_buffer[LCD_COMMAND_BUFFER_SIZE];
int lcd_command_input_ptr = 0;
int lcd_command_output_ptr = 0;

struct file_operations keyblcd_file_operations;

struct wait_queue *lcd_wait_queue = NULL;

char atoi_number[80];
char read_token_digit_buffer[80];

#ifdef  CONFIG_KEYBLCD_CONSOLE
static struct console keyblcd_console;
#endif

typedef struct {
    int row, col;
} lcd_cursor_position;
#define  LCD_CURSOR_POSITION_STACK_SIZE  1024
lcd_cursor_position lcd_cursor_position_stack[LCD_CURSOR_POSITION_STACK_SIZE];
int lcd_cursor_position_stack_size = 0;


int lcd_cursor_position_push(int row, int col) {
    if (lcd_cursor_position_stack_size >= LCD_CURSOR_POSITION_STACK_SIZE)
        return -1;
    lcd_cursor_position_stack[lcd_cursor_position_stack_size].row = row;
    lcd_cursor_position_stack[lcd_cursor_position_stack_size].col = col;
    lcd_cursor_position_stack_size++;
    return 0;
}


lcd_cursor_position *lcd_cursor_position_pop(void) {
    if (lcd_cursor_position_stack_size < 1)
        return NULL;
    lcd_cursor_position_stack_size--;
    return &lcd_cursor_position_stack[lcd_cursor_position_stack_size];
}


void stop_sound(unsigned long data) {
    outb(inb(0x61) & 0xFC, 0x61);
}


void play_sound(int frequency, unsigned int ticks) {
    unsigned int count = 0;
    if ((frequency >= 20) && (frequency <= 20000))
        count = 1193180 / frequency;
    cli();
    del_timer(&sound_timer);
    if (count > 0) {
        outb(inb(0x61) | 0x03, 0x61);
        outb(0xB6, 0x43);
        outb(count & 0xFF, 0x42);
        outb((count >> 8) & 0xFF, 0x42);
        if (ticks) {
            init_timer(&sound_timer);
            sound_timer.expires = jiffies + ticks;
            sound_timer.data = 0;
            sound_timer.function = stop_sound;
            add_timer(&sound_timer);
        }
    }
    else
        stop_sound(0);
    sti();
}


int atoi(char *s) {
    int ret, factor, i;
    int number_len = 0;
    atoi_number[0] = 0;
    for ( ; isspace(*s); s++)
        ;
    for ( ; isdigit(*s); s++) {
        atoi_number[number_len] = *s;
        number_len++;
        atoi_number[number_len] = 0;
    }
    ret = 0;
    factor = 1;
    for (i = number_len - 1; i >= 0; i--) {
        int digit = (int) (atoi_number[i] - '0');
        ret += digit * factor;
        factor *= 10;
    }
    return ret;
}


#ifdef  CONFIG_KEYBLCD_GRAB_KEYBOARD
/* keyboard related functions */
void keyboard_write_char(char v) {
    if (((keyboard_input_ptr + 1) % KEYBOARD_BUFFER_SIZE) != keyboard_output_ptr) {
        keyboard_buffer[keyboard_input_ptr] = v;
        keyboard_input_ptr = (keyboard_input_ptr + 1) % KEYBOARD_BUFFER_SIZE;
    }
    wake_up_interruptible(&keyboard_wait_queue);
}


int keyboard_has_char(void) {
    return (keyboard_input_ptr != keyboard_output_ptr);
}


char keyboard_read_char(void) {
    char ret = 0;
    if (keyboard_input_ptr != keyboard_output_ptr) {
        ret = keyboard_buffer[keyboard_output_ptr];
        keyboard_output_ptr = (keyboard_output_ptr + 1) % KEYBOARD_BUFFER_SIZE;
    }
    return ret;
}


void keyblcd_keyboard_interrupt_handler(int irq, void *dev_id, struct pt_regs *regs) {
    int v;

    v = inb(0x60);
    if (KEY_SHIFT_PRESSED(v))
        current_code_table = shift_code_table;
    else if (KEY_SHIFT_RELEASED(v))
        current_code_table = normal_code_table;
    else if (KEY_CTRL_PRESSED(v))
        current_code_table = ctrl_code_table;
    else if (KEY_CTRL_RELEASED(v))
        current_code_table = normal_code_table;
    else {
        if (!KEY_RELEASED(v)) {
            if (v < 0)
                v = 0;
            else if (v > 88)
                v = 88;
            keyboard_write_char(current_code_table[v]);
        }
    }
}

#endif  /* CONFIG_KEYBLCD_GRAB_KEYBOARD */


/* lcd related functions */
void lcd_command_buffer_write_with_position(unsigned char type, unsigned char data, unsigned char scrolling, int row, int col) {
    if (((lcd_command_input_ptr + 1) % LCD_COMMAND_BUFFER_SIZE) != lcd_command_output_ptr) {
        lcd_command_buffer[lcd_command_input_ptr].type = type;
        lcd_command_buffer[lcd_command_input_ptr].data = data;
        lcd_command_buffer[lcd_command_input_ptr].scrolling = scrolling;
        lcd_command_buffer[lcd_command_input_ptr].row = row;
        lcd_command_buffer[lcd_command_input_ptr].col = col;
        lcd_command_input_ptr = (lcd_command_input_ptr + 1) % LCD_COMMAND_BUFFER_SIZE;
    }
}


void lcd_command_buffer_write(unsigned char type, unsigned char data, unsigned char scrolling) {
    lcd_command_buffer_write_with_position(type, data, scrolling, -1, -1);
}


lcd_command *lcd_command_buffer_read(void) {
    lcd_command *ret = NULL;
    if (lcd_command_input_ptr != lcd_command_output_ptr) {
        ret = &lcd_command_buffer[lcd_command_output_ptr];
        lcd_command_output_ptr = (lcd_command_output_ptr + 1) % LCD_COMMAND_BUFFER_SIZE;
    }
    return ret;
}


int lcd_command_buffer_empty(void) {
    return (lcd_command_input_ptr == lcd_command_output_ptr);
}


void show_screen_data(void) {
    int row, col;

    printk("keyblcd: screen data\n");
    for (row = 0; row < ROWS; row++) {
        for (col = 0; col < COLS; col++) {
            char c = showed_screen[row][col];
            if (isgraph(c))
                printk("%c", c);
            else
                printk(".");
        }
        printk("\n");
    }
}


void scroll_up_screen_data(void) {
    int row, col;

    for (row = 0; row < (ROWS - 1); row++) {
        for (col = 0; col < COLS; col++)
            showed_screen[row][col] = showed_screen[row + 1][col];
    }
    for (col = 0; col < COLS; col++)
        showed_screen[ROWS - 1][col] = ' ';
}


void scroll_up_lcd(void) {
    int row, col;

    scroll_up_screen_data();
    lcd_command_buffer_write(LCD_COMMAND_COMMAND, 0x01, 1);   /* clear screen */
    lcd_command_buffer_write(LCD_COMMAND_COMMAND, 0x02, 1);   /* home cursor */
    for (row = 0; row < (ROWS - 1); row++) {
        for (col = 0; col < COLS; col++)
            lcd_command_buffer_write(LCD_COMMAND_CHAR, showed_screen[row][col], 1);
        lcd_command_buffer_write(LCD_COMMAND_COMMAND, lcd_line_offset[row + 1] | 0x80, 1);
    }
}


#define  LCD_RS_LOW       outb(inb(LP) & 0x7F, LP)
#define  LCD_RS_HIGH      outb(inb(LP) | 0x80, LP)
#define  LCD_ENABLE_HIGH  outb(inb(LP + 2) & 0xFE, LP + 2)
#define  LCD_ENABLE_LOW   outb(inb(LP + 2) | 0x01, LP + 2)
#define  LCD_READ         outb(inb(LP + 2) | 0x04, LP + 2)
#define  LCD_WRITE        outb(inb(LP + 2) & 0xFB, LP + 2)


void _lcd_cmd(unsigned char data) {
    outb((inb(LP) & 0x80) | (data & 0x7F), LP);
    if (data & 0x80)
        outb(inb(LP + 2) & 0xFD, LP + 2);
    else
        outb(inb(LP + 2) | 0x02, LP + 2);
    LCD_RS_LOW;
    LCD_ENABLE_HIGH;
    udelay(1);
    LCD_ENABLE_LOW;
}


void lcd_buffer_write_char(char v) {
    if (((lcd_input_ptr + 1) % LCD_BUFFER_SIZE) != lcd_output_ptr) {
        lcd_buffer[lcd_input_ptr] = v;
        lcd_input_ptr = (lcd_input_ptr + 1) % LCD_BUFFER_SIZE;
    }
}


void lcd_buffer_write_string(char *s) {
    while (*s != 0) {
        lcd_buffer_write_char(*s);
        s++;
    }
}


int lcd_buffer_full(void) {
    return (((lcd_input_ptr + 1) % LCD_BUFFER_SIZE) == lcd_output_ptr);
}


int lcd_buffer_empty(void) {
    return (lcd_input_ptr == lcd_output_ptr);
}


void lcd_buffer_unread_char(char v) {
    lcd_output_ptr = (lcd_output_ptr + LCD_BUFFER_SIZE - 1) % LCD_BUFFER_SIZE;
    lcd_buffer[lcd_output_ptr] = v;
}


char lcd_buffer_read_char(void) {
    char ret = 0;
    if (lcd_input_ptr != lcd_output_ptr) {
        ret = lcd_buffer[lcd_output_ptr];
        lcd_output_ptr = (lcd_output_ptr + 1) % LCD_BUFFER_SIZE;
    }
    wake_up_interruptible(&lcd_wait_queue);
    return ret;
}


void lcd_buffer_read_token(lcd_token *t, int read_possible_number) {
    read_token_digit_buffer[0] = 0;
    while (!lcd_buffer_empty()) {
        char v = lcd_buffer_read_char();
        if (read_possible_number) {
            if (isdigit(v)) {
                int len = strlen(read_token_digit_buffer);
                read_token_digit_buffer[len] = v;
                read_token_digit_buffer[len + 1] = 0;
            }
            else {
                if (read_token_digit_buffer[0] == 0) {
                    t->type = LCD_TOKEN_CHAR;
                    t->value = (int) v;
                    return;
                }
                else {
                    lcd_buffer_unread_char(v);
                    t->type = LCD_TOKEN_NUMBER;
                    t->value = atoi(read_token_digit_buffer);
                    /* t->value = (int) simple_strtol(read_token_digit_buffer, NULL, 10); */
                    return;
                }
            }
        }
        else {
            t->type = LCD_TOKEN_CHAR;
            t->value = (int) v;
            return;
        }
    }
    t->type = LCD_TOKEN_EMPTY;
}


#define  LCD_VT100_STATUS_INITIAL        0
#define  LCD_VT100_STATUS_ESCAPE         1
#define  LCD_VT100_STATUS_OPEN_BRACKET   2
#define  LCD_VT100_STATUS_FIRST_NUMBER   3
#define  LCD_VT100_STATUS_SECOND_NUMBER  4
#define  LCD_VT100_STATUS_EXTENSION      5

void lcd_get_vt100_command(lcd_vt100_command *vtc) {
    lcd_token tok;
    while (1) {
        if (lcd_vt100_status == LCD_VT100_STATUS_INITIAL) {
            lcd_buffer_read_token(&tok, 0);
            if (tok.type == LCD_TOKEN_CHAR) {
                if (tok.value == 27)
                    lcd_vt100_status = LCD_VT100_STATUS_ESCAPE;
                else {
                    vtc->command = LCD_VT100_CHAR;
                    vtc->value1 = tok.value;
                    return;
                }
            }
            else {
                vtc->command = LCD_VT100_EMPTY;
                lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                return;
            }
        }
        else if (lcd_vt100_status == LCD_VT100_STATUS_ESCAPE) {
            lcd_buffer_read_token(&tok, 0);
            if (tok.type == LCD_TOKEN_CHAR) {
                if ((tok.value == 'D') || (tok.value == 'M') || (tok.value == 'c') || (tok.value == '7') || (tok.value == '8')) {
                    /* SCROLL DOWN/SCROLL UP/RESET/SAVE CURSOR/RESTORE CURSOR */
                    if (tok.value == 'D')
                        vtc->command = LCD_VT100_SCROLL_DOWN;
                    else if (tok.value == 'M')
                        vtc->command = LCD_VT100_SCROLL_UP;
                    else if (tok.value == 'c')
                        vtc->command = LCD_VT100_RESET;
                    else if (tok.value == '7')
                        vtc->command = LCD_VT100_SAVE_CURSOR;
                    else if (tok.value == '8')
                        vtc->command = LCD_VT100_RESTORE_CURSOR;
                    lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                    return;
                }
                else if (tok.value == '[')
                    lcd_vt100_status = LCD_VT100_STATUS_OPEN_BRACKET;
                else if (tok.value == 'x')
                    lcd_vt100_status = LCD_VT100_STATUS_EXTENSION;
            }
            else if (tok.type == LCD_TOKEN_EMPTY) {
                vtc->command = LCD_VT100_EMPTY;
                return;
            }
            else {
                vtc->command = LCD_VT100_EMPTY;
                lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                return;
            }
        }
        else if (lcd_vt100_status == LCD_VT100_STATUS_OPEN_BRACKET) {
            lcd_buffer_read_token(&tok, 1);
            if (tok.type == LCD_TOKEN_CHAR) {
                if ((tok.value == 'r') || (tok.value == 's') || (tok.value == 'u') || (tok.value == 'J') || (tok.value == 'K')) {
                    /* ENABLE SCROLL/SAVE CURSOR/RESTORE CURSOR/CLRDOWN/CLREOL */
                    if (tok.value == 'r') {
                        vtc->command = LCD_VT100_ENABLE_SCROLL;
                        vtc->value1 = -1;
                        vtc->value2 = -1;
                    }
                    else if (tok.value == 's')
                        vtc->command = LCD_VT100_SAVE_CURSOR;
                    else if (tok.value == 'u')
                        vtc->command = LCD_VT100_RESTORE_CURSOR;
                    else if (tok.value == 'J')
                        vtc->command = LCD_VT100_CLRDOWN;
                    else if (tok.value == 'K')
                        vtc->command = LCD_VT100_CLREOL;
                    lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                    return;
                }
                else if ((tok.value == 'A') || (tok.value == 'B') || (tok.value == 'C') || (tok.value == 'D')) {
                    /* CURSOR UP/DOWN/FORWARD/BACKWARD */
                    if (tok.value == 'A')
                        vtc->command = LCD_VT100_CURSOR_UP;
                    else if (tok.value == 'B')
                        vtc->command = LCD_VT100_CURSOR_DOWN;
                    else if (tok.value == 'C')
                        vtc->command = LCD_VT100_CURSOR_FORWARD;
                    else if (tok.value == 'D')
                        vtc->command = LCD_VT100_CURSOR_BACKWARD;
                    vtc->value1 = 1;
                    lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                    return;
                }
                else if ((tok.value == 'H') || (tok.value == 'f')) {
                    vtc->command = LCD_VT100_CURSOR_HOME;
                    vtc->value1 = 1;
                    vtc->value2 = 1;
                    lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                    return;
                }
                else {
                    vtc->command = LCD_VT100_EMPTY;
                    lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                    return;
                }
            }
            else if (tok.type == LCD_TOKEN_NUMBER) {
                lcd_vt100_value1 = tok.value;
                lcd_vt100_status = LCD_VT100_STATUS_FIRST_NUMBER;
            }
            else if (tok.type == LCD_TOKEN_EMPTY) {
                vtc->command = LCD_VT100_EMPTY;
                return;
            }
            else {
                vtc->command = LCD_VT100_EMPTY;
                lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                return;
            }
        }
        else if (lcd_vt100_status == LCD_VT100_STATUS_FIRST_NUMBER) {
            lcd_buffer_read_token(&tok, 0);
            if (tok.type == LCD_TOKEN_CHAR) {
                if ((tok.value == 'A') || (tok.value == 'B') || (tok.value == 'C') || (tok.value == 'D')) {
                    /* CURSOR UP/DOWN/FORWARD/BACKWARD */
                    if (tok.value == 'A')
                        vtc->command = LCD_VT100_CURSOR_UP;
                    else if (tok.value == 'B')
                        vtc->command = LCD_VT100_CURSOR_DOWN;
                    else if (tok.value == 'C')
                        vtc->command = LCD_VT100_CURSOR_FORWARD;
                    else if (tok.value == 'D')
                        vtc->command = LCD_VT100_CURSOR_BACKWARD;
                    vtc->value1 = lcd_vt100_value1;
                    lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                    return;
                }
                else if (((lcd_vt100_value1 == 1) || (lcd_vt100_value1 == 2)) && ((tok.value == 'K') || (tok.value == 'J'))) {
                    if (lcd_vt100_value1 == 1) {
                        if (tok.value == 'K')
                            vtc->command = LCD_VT100_CLRSOL;
                        else if (tok.value == 'J')
                            vtc->command = LCD_VT100_CLRUP;
                    }
                    else if (lcd_vt100_value1 == 2) {
                        if (tok.value == 'K')
                            vtc->command = LCD_VT100_CLRLINE;
                        else if (tok.value == 'J')
                            vtc->command = LCD_VT100_CLRSCR;
                    }
                    lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                    return;
                }
                else if (tok.value == ';') {
                    lcd_buffer_read_token(&tok, 1);
                    if (tok.type == LCD_TOKEN_NUMBER) {
                        lcd_vt100_value2 = tok.value;
                        lcd_vt100_status = LCD_VT100_STATUS_SECOND_NUMBER;
                    }
                    else {
                        vtc->command = LCD_VT100_EMPTY;
                        lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                        return;
                    }
                }
                else {
                    vtc->command = LCD_VT100_EMPTY;
                    lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                    return;
                }
            }
            else if (tok.type == LCD_TOKEN_EMPTY) {
                vtc->command = LCD_VT100_EMPTY;
                return;
            }
            else {
                vtc->command = LCD_VT100_EMPTY;
                lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                return;
            }
        }
        else if (lcd_vt100_status == LCD_VT100_STATUS_SECOND_NUMBER) {
            lcd_buffer_read_token(&tok, 0);
            if (tok.type == LCD_TOKEN_CHAR) {
                if (tok.value == 'r')
                    vtc->command = LCD_VT100_ENABLE_SCROLL;
                else if ((tok.value == 'f') || (tok.value == 'H')) {
                    vtc->command = LCD_VT100_CURSOR_HOME;
                }
                else {
                    vtc->command = LCD_VT100_EMPTY;
                }
                vtc->value1 = lcd_vt100_value1;
                vtc->value2 = lcd_vt100_value2;
                lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                return;
            }
            else if (tok.type == LCD_TOKEN_EMPTY) {
                vtc->command = LCD_VT100_EMPTY;
                return;
            }
            else {
                vtc->command = LCD_VT100_EMPTY;
                lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                return;
            }
        }
        else if (lcd_vt100_status == LCD_VT100_STATUS_EXTENSION) {
            lcd_buffer_read_token(&tok, 0);
            if (tok.type == LCD_TOKEN_CHAR) {
                if (tok.value == '0')
                    vtc->command = LCD_VT100_EXTENSION_HIDE_CURSOR;
                else if (tok.value == '1')
                    vtc->command = LCD_VT100_EXTENSION_SHOW_CURSOR;
                lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                return;
            }
            else if (tok.type == LCD_TOKEN_EMPTY) {
                vtc->command = LCD_VT100_EMPTY;
                return;
            }
            else {
                vtc->command = LCD_VT100_EMPTY;
                lcd_vt100_status = LCD_VT100_STATUS_INITIAL;
                return;
            }
        }
    }
}


void lcd_get_row_col(int lcd_command, int *row, int *col) {
    int r;
    lcd_command &= 0x7F;
    *row = 0;
    for (r = 0; r < (ROWS - 1); r++) {
        if ((lcd_command >= lcd_line_offset[r]) && (lcd_command < lcd_line_offset[r + 1])) {
            *row = r;
            break;
        }
    }
    *col = lcd_command - lcd_line_offset[*row];
}


void lcd_timer_handler(unsigned long data) {
    lcd_vt100_command vtc;
    mod_timer(&lcd_timer, 0);
    if (lcd_handler_status == LCD_HANDLER_STATUS_INITIAL) {
        if (lcd_command_buffer_empty()) {
            lcd_get_vt100_command(&vtc);
            /*
               raw char display (NO VT100 parsing)

            if (!lcd_buffer_empty()) {
                vtc.command = LCD_VT100_CHAR;
                do {
                    vtc.value1 = lcd_buffer_read_char() & 0x7F;
                } while (!isprint(vtc.value1) && (vtc.value1 != '\n') && (vtc.value1 != '\r') && (vtc.value1 != 0));
                if (vtc.value1 == 0)
                    vtc.command = LCD_VT100_EMPTY;
            }
            else {
                vtc.command = LCD_VT100_EMPTY;
            }
            */
            if (vtc.command == LCD_VT100_CHAR) {
                if (vtc.value1 == '\n') {
                    lcd_command_buffer_write(LCD_COMMAND_COMMAND, LCD_RESERVED_COMMAND_NEWLINE, 0);
                    return;
                }
                else if (!isprint(vtc.value1))
                    return;
                lcd_data = vtc.value1;
                lcd_handler_status = LCD_HANDLER_STATUS_WAIT_FOR_DATA;
            }
            else if (vtc.command == LCD_VT100_RESET) {
                lcd_command_buffer_write(LCD_COMMAND_COMMAND, 0x01, 0);   /* clear screen */
                lcd_command_buffer_write_with_position(LCD_COMMAND_COMMAND, 0x02, 0, 0, 0);   /* home cursor */
                return;
            }
            else if (vtc.command == LCD_VT100_EXTENSION_HIDE_CURSOR) {
                lcd_command_buffer_write(LCD_COMMAND_COMMAND, 0x0C, 0);
                return;
            }
            else if (vtc.command == LCD_VT100_EXTENSION_SHOW_CURSOR) {
                lcd_command_buffer_write(LCD_COMMAND_COMMAND, 0x0C | 0x03, 0);
                return;
            }
            else if (vtc.command == LCD_VT100_CURSOR_HOME) {
                int row = (vtc.value1 < 1) ? 1 : ((vtc.value1 > ROWS) ? ROWS : vtc.value1);
                int col = (vtc.value2 < 1) ? 1 : ((vtc.value2 > COLS) ? COLS : vtc.value2);
                row--;
                col--;
                lcd_command_buffer_write_with_position(LCD_COMMAND_COMMAND, (lcd_line_offset[row] + col) | 0x80, 0, row, col);
                return;
            }
            else if (vtc.command == LCD_VT100_CURSOR_UP) {
                if (lcd_current_row > 0) {
                    int row = lcd_current_row - 1;
                    lcd_command_buffer_write_with_position(LCD_COMMAND_COMMAND, (lcd_line_offset[row] + lcd_current_col) | 0x80, 0, row, lcd_current_col);
                }
                return;
            }
            else if (vtc.command == LCD_VT100_CURSOR_DOWN) {
                if (lcd_current_row < (ROWS - 1)) {
                    int row = lcd_current_row + 1;
                    lcd_command_buffer_write_with_position(LCD_COMMAND_COMMAND, (lcd_line_offset[row] + lcd_current_col) | 0x80, 0, row, lcd_current_col);
                }
                return;
            }
            else if (vtc.command == LCD_VT100_CURSOR_BACKWARD) {
                if (lcd_current_col > 0) {
                    int col = lcd_current_col - 1;
                    lcd_command_buffer_write_with_position(LCD_COMMAND_COMMAND, (lcd_line_offset[lcd_current_row] + col) | 0x80, 0, lcd_current_row, col);
                }
                return;
            }
            else if (vtc.command == LCD_VT100_CURSOR_FORWARD) {
                if (lcd_current_col < (COLS - 1)) {
                    int col = lcd_current_col + 1;
                    lcd_command_buffer_write_with_position(LCD_COMMAND_COMMAND, (lcd_line_offset[lcd_current_row] + col) | 0x80, 0, lcd_current_row, col);
                }
                return;
            }
            else
                return;
        }
        else {
            lcd_command *cmd = lcd_command_buffer_read();
            lcd_data = cmd->data;
            lcd_scrolling = cmd->scrolling;
            lcd_row = cmd->row;
            lcd_col = cmd->col;
            if (cmd->type == LCD_COMMAND_COMMAND)
                lcd_handler_status = LCD_HANDLER_STATUS_WAIT_FOR_COMMAND;
            else if (cmd->type == LCD_COMMAND_CHAR)
                lcd_handler_status = LCD_HANDLER_STATUS_WAIT_FOR_DATA;
        }
    }
    if ((lcd_handler_status == LCD_HANDLER_STATUS_WAIT_FOR_DATA) || (lcd_handler_status == LCD_HANDLER_STATUS_WAIT_FOR_COMMAND)) {
        LCD_ENABLE_LOW;
        outb(inb(LP + 2) & 0xFD, LP + 2);   /* DB7 to 1 */
        LCD_RS_LOW;
        LCD_READ;
        lcd_retry = 0;
        if (lcd_handler_status == LCD_HANDLER_STATUS_WAIT_FOR_DATA)
            lcd_handler_status = LCD_HANDLER_STATUS_WAIT_FOR_DATA_LOOP;
        else
            lcd_handler_status = LCD_HANDLER_STATUS_WAIT_FOR_COMMAND_LOOP;
    }
    if ((lcd_handler_status == LCD_HANDLER_STATUS_WAIT_FOR_DATA_LOOP) || (lcd_handler_status == LCD_HANDLER_STATUS_WAIT_FOR_COMMAND_LOOP)) {
        char v;
        do {
            LCD_ENABLE_LOW;
            LCD_ENABLE_HIGH;
            v = inb(LP + 2);
            lcd_retry++;
            if (lcd_retry >= 100000) {
                printk("keyblcd: lcd timeout\n");
                return;
            }
        } while ((v & 0x02) == 0);
        /* not busy */
        LCD_ENABLE_LOW;
        LCD_WRITE;
        if (lcd_handler_status == LCD_HANDLER_STATUS_WAIT_FOR_DATA_LOOP)
            lcd_handler_status = LCD_HANDLER_STATUS_DATA;
        else
            lcd_handler_status = LCD_HANDLER_STATUS_COMMAND;
    }
    if ((lcd_handler_status == LCD_HANDLER_STATUS_DATA) || (lcd_handler_status == LCD_HANDLER_STATUS_COMMAND)) {
        char v = lcd_data;
        if ((lcd_handler_status == LCD_HANDLER_STATUS_COMMAND) && (lcd_data == LCD_RESERVED_COMMAND_NEWLINE)) {
            /* explicit new line */
            int c;
            for (c = lcd_current_col; c < COLS; c++)
                showed_screen[lcd_current_row][c] = ' ';
            lcd_current_col = 0;
            lcd_current_row++;
            if (lcd_current_row >= ROWS) {
                scroll_up_lcd();
                lcd_current_row = ROWS - 1;
                lcd_current_row = 0;
            }
            else {
                if (!lcd_scrolling)
                    lcd_command_buffer_write(LCD_COMMAND_COMMAND, (lcd_line_offset[lcd_current_row] + lcd_current_col) | 0x80, 0);
            }
            lcd_handler_status = LCD_HANDLER_STATUS_INITIAL;
            return;
        }
        if ((lcd_handler_status == LCD_HANDLER_STATUS_COMMAND) && (v == 0x02)) {
            /* home cursor command */
            lcd_current_row = 0;
            lcd_current_col = 0;
        }
        if ((lcd_handler_status == LCD_HANDLER_STATUS_COMMAND) && (lcd_row >= 0) && (lcd_col >= 0)) {
            /* any other position change command */
            lcd_current_row = lcd_row;
            lcd_current_col = lcd_col;
        }
        /* send data or command to LCD */
        outb((inb(LP) & 0x80) | (v & 0x7F), LP);
        if (v & 0x80)
            outb(inb(LP + 2) & 0xFD, LP + 2);
        else
            outb(inb(LP + 2) | 0x02, LP + 2);
        if (lcd_handler_status == LCD_HANDLER_STATUS_DATA)
            LCD_RS_HIGH;
        else
            LCD_RS_LOW;
        LCD_ENABLE_HIGH;
        LCD_ENABLE_LOW;
        if (lcd_handler_status == LCD_HANDLER_STATUS_DATA) {
            /* update screen */
            showed_screen[lcd_current_row][lcd_current_col] = v;
            lcd_current_col++;
            if (lcd_current_col >= COLS) {
                lcd_current_col = 0;
                lcd_current_row++;
                if (lcd_current_row >= ROWS) {
                    /* do scroll */
                    scroll_up_lcd();
                    lcd_current_row = ROWS - 1;
                }
                else {
                    if (!lcd_scrolling) {
                        lcd_command_buffer_write(LCD_COMMAND_COMMAND, (lcd_line_offset[lcd_current_row] + lcd_current_col) | 0x80, 0);
                    }
                }
            }
        }
        lcd_handler_status = LCD_HANDLER_STATUS_INITIAL;
    }
}


void lcd_init(void) {
    /* init port */
    outb(0x00, LP);
    outb(0x09, LP + 2);   /* light off */
    udelay(100);
    /* init display */
    _lcd_cmd(0x38);              /* 8 bit interface, 2 lines, 5x7 dots */
    udelay(100);
    _lcd_cmd(0x38);              /* do it 3 times */
    udelay(100);
    _lcd_cmd(0x38);
    udelay(100);
    _lcd_cmd(0x0c);              /* display on, cursor off, cursor not blinking */
    udelay(100);
    _lcd_cmd(0x06);              /* move cursor with data write */
    udelay(100);
    /* timer */
    lcd_command_buffer_write(LCD_COMMAND_COMMAND, 0x01, 0);           /* clear screen */
    lcd_command_buffer_write(LCD_COMMAND_COMMAND, 0x02, 0);           /* home cursor */
    lcd_command_buffer_write(LCD_COMMAND_COMMAND, 0x0C | 0x03, 0);    /* cursor on */
    memset(showed_screen, ' ', ROWS * COLS);
    lcd_current_row = 0;
    lcd_current_col = 0;
    init_timer(&lcd_timer);
    lcd_timer.expires = 0;
    lcd_timer.data = 0;
    lcd_timer.function = lcd_timer_handler;
    add_timer(&lcd_timer);
}


/* file i/o */
int keyblcd_dev_ioctl(struct inode *ip, struct file *fp, unsigned int cmd, unsigned long arg) {
    return -ENOIOCTLCMD;
}


ssize_t keyblcd_dev_write(struct file *fp, const char *buffer, size_t count, loff_t *offset) {
    int i;
    char data;
    for (i = 0; i < count; i++) {
        while (lcd_buffer_full())
            interruptible_sleep_on_timeout(&lcd_wait_queue, 100);
        if (copy_from_user(&data, &buffer[i], 1))
            return -EFAULT;
        lcd_buffer_write_char(data);
    }
    if (offset != NULL)
        *offset += count;
    return count;
}

#ifdef  CONFIG_KEYBLCD_GRAB_KEYBOARD

ssize_t keyblcd_dev_read(struct file *fp, char* buffer, size_t count, loff_t *offset) {
    int i;
    char data;
    for (i = 0; i < count; i++) {
        while (!keyboard_has_char())
            interruptible_sleep_on_timeout(&keyboard_wait_queue, 100);
        data = keyboard_read_char();
        if (copy_to_user(&buffer[i], &data, 1))
            return -EFAULT;
    }
    if (offset != NULL)
        *offset += count;
    return count;
}

#else

ssize_t keyblcd_dev_read(struct file *fp, char* buffer, size_t count, loff_t *offset) {
    return 0;
}

#endif  /* CONFIG_KEYBLCD_GRAB_KEYBOARD */

#ifdef  CONFIG_KEYBLCD_CONSOLE
/* console i/o */
void keyblcd_console_write(struct console *cons, const char *buffer, unsigned count) {
    int i;
    for (i = 0; i < count; i++) {
        while (lcd_buffer_full())
            interruptible_sleep_on_timeout(&lcd_wait_queue, 100);
        lcd_buffer_write_char(buffer[i]);
    }
}


#ifdef  CONFIG_KEYBLCD_GRAB_KEYBOARD
int keyblcd_console_wait_key(struct console *cons) {
    while (!keyboard_has_char())
        interruptible_sleep_on_timeout(&keyboard_wait_queue, 100);
    return (int) keyboard_read_char();
}
#else
int keyblcd_console_wait_key(struct console *cons) {
    return 13;
}
#endif  /* CONFIG_KEYBLCD_GRAB_KEYBOARD */


int keyblcd_console_setup(struct console *cons, char *options) {
    play_sound(440, 10);
    printk("keyblcd console selected\n");
    return 0;
}
#endif  /* CONFIG_KEYBLCD_CONCOSE */


int keyblcd_init(void) {
    int err;

#ifdef  CONFIG_KEYBLCD_GRAB_KEYBOARD
    /* request keyboard i/o ports */
    if ((err = check_region(0x60, 5)) < 0)
        return err;
    request_region(0x60, 5, "keyblcd");
    /* request irq 1 (the keyboard's irq) */
    if ((err = request_irq(1, keyblcd_keyboard_interrupt_handler, SA_INTERRUPT, "keyblcd", NULL)) < 0) {
        release_region(0x60, 5);
        return err;
    }
    /* enable keyboard */
    outb(0xAE, 0x64);
#endif  /* CONFIG_KEYBLCD_GRAB_KEYBOARD */
    /* request printer i/o ports for LCD */
    if ((err = check_region(LP, 3)) < 0) {
        release_region(0x60, 5);
        free_irq(1, "keyblcd");
        return err;
    }
    request_region(LP, 3, "keyblcd");
    /* create char device (non tty) */
    memset(&keyblcd_file_operations, 0, sizeof(struct file_operations));
    keyblcd_file_operations.read = keyblcd_dev_read;
    keyblcd_file_operations.write = keyblcd_dev_write;
    keyblcd_file_operations.ioctl = keyblcd_dev_ioctl;
    if ((err = register_chrdev(DEVICE_MAJOR, "keyblcd", &keyblcd_file_operations)) < 0)
        return err;
    /* init sound timer */
    init_timer(&sound_timer);
    sound_timer.expires = 0;
    sound_timer.data = 0;
    sound_timer.function = stop_sound;
    /* init display */
    lcd_init();
#ifdef  CONFIG_KEYBLCD_CONSOLE
    strcpy(keyblcd_console.name, "keyblcd");
    keyblcd_console.write = keyblcd_console_write;
    keyblcd_console.read = NULL;
    keyblcd_console.device = NULL;   /* no associated tty device */
    keyblcd_console.wait_key = keyblcd_console_wait_key;
    keyblcd_console.unblank = NULL;
    keyblcd_console.setup = keyblcd_console_setup;
    keyblcd_console.flags = CON_PRINTBUFFER;
    keyblcd_console.index = -1;
    keyblcd_console.cflag = 0;
    keyblcd_console.next = NULL;
    register_console(&keyblcd_console);
#endif
#ifdef  CONFIG_KEYBLCD_GRAB_KEYBOARD
    printk("keyblcd: keyboard & LCD terminal driver (major=%d, minor=%d)\n", DEVICE_MAJOR, DEVICE_MINOR);
#else
    printk("keyblcd: LCD terminal driver (major=%d, minor=%d)\n", DEVICE_MAJOR, DEVICE_MINOR);
#endif  /* CONFIG_KEYBLCD_GRAB_KEYBOARD */
    printk("keyblcd: M.H.P. Sistemas de Control S.L.\n");
    return 0;
}


void keyblcd_exit(void) {
#ifdef  CONFIG_KEYBLCD_CONSOLE
    unregister_console(&keyblcd_console);
#endif
    unregister_chrdev(DEVICE_MAJOR, "keyblcd");
#ifdef  CONFIG_KEYBLCD_GRAB_KEYBOARD
    release_region(0x60, 5);
    free_irq(1, "keyblcd");
#endif  /* CONFIG_KEYBLCD_GRAB_KEYBOARD */
    release_region(LP, 5);
}


#ifdef  MODULE

int init_module(void) {
    return keyblcd_init();
}


void cleanup_module(void) {
    keyblcd_exit();
}

#else

module_init(keyblcd_init);
module_exit(keyblcd_exit);

#endif
