1123 lines
32 KiB
C
1123 lines
32 KiB
C
/* This is the LCDproc driver for various serial VFD Devices
|
|
|
|
Copyright (C) 2006 Stefan Herdler
|
|
|
|
This driver is based on wirz-sli.c, hd44780.c, drv_base.c and NoritakeVFD
|
|
driver.
|
|
It may contain parts of other drivers of this package too.
|
|
|
|
2006-02-13 Version 0.2: everything should work (not all hardware tested!)
|
|
|
|
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
|
|
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
|
|
|
|
|
|
|
|
The serialVFD-driver should work with all NEC FIPC8367 based VFDs:
|
|
NEC FC20X2JA
|
|
NEC FM20X2KB-AB
|
|
NEC FC20X1SA-AB/AA
|
|
...
|
|
Tested only on: NEC FM20X2KB-AB
|
|
The driver also works (tested) on "KD Rev 2.1" (an ATMEL AT90S....
|
|
based FM20X2KB-AB replacement).
|
|
Note: Above char. 128 the FIPC-displays and KD's charactersets
|
|
are different.
|
|
|
|
The driver may work or will be easy to get working on the following
|
|
Displays / Controllers (not tested!).
|
|
Display: Controller:
|
|
FUTABA M204SD01AA FUTABA 5P00A016
|
|
Noritake CU20026SCPB-T (microcontroller)
|
|
Noritake CU20045SCPB-T28A ?
|
|
... or compatible.
|
|
(most Futaba/Noritake 7x5 dot VFDs with serial(rs232) interface
|
|
(including serial(rs232)/parallel types))
|
|
Maybe there are little changes in the commands to do to get all
|
|
functions to work correctly. But because of the similarity of the
|
|
protocols, it wont be much work.
|
|
|
|
If you want to add a new device to the driver add a new section
|
|
to the displaytype-switch-case in the init-function and fill it
|
|
with the corrrect commands for the display.
|
|
(Try wich displaytype works best with your display, copy and modify
|
|
it's section that is the easiest way I guess.)
|
|
|
|
|
|
On this page I found pictures and datasheets of the VFD's:
|
|
http://www.maltepoeggel.de/html/vfd/index.html
|
|
|
|
|
|
|
|
|
|
To enable the bignumbers on 2-line displays (only this driver supports
|
|
this yet) you have to edit "/clients/lcdproc/chrono.c" before compiling.
|
|
In function "big_clock_screen" you have to replace the "4" by a "2" in
|
|
this line: "if (lcd_hgt < 4)".
|
|
|
|
|
|
|
|
This driver uses the serial mode of the displays.
|
|
It is NOT possible to connect most of the displays directly to the
|
|
serialport. The signal has to be inverted. I use the following
|
|
circuit to do that job.
|
|
|
|
Computer Display
|
|
(signal)
|
|
HDD Powerconnector
|
|
color(Voltage)
|
|
|
|
red(+5V) --------------------------------o----- +5V
|
|
|
|
|
black(GND) --------------------o |
|
|
| R
|
|
| R 10k
|
|
Serial(SUB-D 9Pin female) V* R
|
|
pin(signal) |
|
|
|
|
|
3(TxD) --RRRR--o--o o-----------------o----- RxD
|
|
10k | | |
|
|
| | C|
|
|
| | ----- BC547c (or similar NPN)
|
|
R | B| / |
|
|
10k R o--|-| |
|
|
R | \ |
|
|
| --->- A*
|
|
| E| |
|
|
| | |
|
|
5(GND) --------o----------o----o----------------- GND
|
|
|
|
Shield ------------------------------------------
|
|
optional
|
|
|
|
*connect near display
|
|
|
|
|
|
The pins on the different displays vary.
|
|
|
|
|
|
|
|
FM20X2KB-AB:
|
|
|
|
CN1:
|
|
Pin 33 <--- RxD
|
|
(Testmode: connect pins 25 & 26 then power up)
|
|
|
|
CN2:
|
|
Pin 1 <--- +5V
|
|
Pin 2 ---- GND
|
|
|
|
|
|
|
|
KD Rev 2.1:
|
|
|
|
blue connector (6pin in a row) (the important one!):
|
|
--------------------------------
|
|
| +5V +5V RxD GND GND GND |
|
|
--------------------------------
|
|
NOTE: hold the display in that position where you can read
|
|
the "KD Rev 2.1" marking normally!
|
|
|
|
gray connector (10pin 2 rows):
|
|
Do not use. (the ATMEL ISP connector I guess)
|
|
|
|
The two jumpers next to the gray connector:
|
|
Normally not used.
|
|
You can activate two different testmodes with them.
|
|
|
|
|
|
|
|
|
|
|
|
List of driver entry point:
|
|
|
|
init Implemented.
|
|
close Implemented.
|
|
width Implemented.
|
|
height Implemented.
|
|
clear Implemented by space filling no custom char info.
|
|
flush Calling draw_frame only.
|
|
string Implemented.
|
|
chr Implemented.
|
|
vbar Implemented.
|
|
hbar Implemented.
|
|
num Implemented.
|
|
heartbeat Implemented.
|
|
icon Implemented.
|
|
cursor NOT IMPLEMENTED: Is it really used?
|
|
set_char Implemented.
|
|
get_free_chars Implemented.
|
|
cellwidth Implemented.
|
|
cellheight Implemented.
|
|
get_contrast Not implemented, no software control.
|
|
set_contrast Not implemented, no software control.
|
|
get_brightness Implemented.
|
|
set_brightness Implemented.
|
|
backlight Not implemented, no software control.
|
|
output Not implemented.
|
|
get_key Not implemented, no keys.
|
|
get_info Implemented.
|
|
|
|
*/
|
|
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <termios.h>
|
|
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <syslog.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
|
|
#include "lcd.h"
|
|
#include "serialVFD.h"
|
|
|
|
#include "report.h"
|
|
#include "lcd_lib.h"
|
|
#include "adv_bignum.h"
|
|
|
|
#define pos1_cursor 4 //moves cursor to top left character.
|
|
#define mv_cursor 5 //moves cursor to position specified by the next byte.
|
|
#define reset 6 //reset
|
|
#define init_cmds 7 //commands needed to initialize the display.
|
|
#define set_user_char 8 //set user character.
|
|
#define hor_tab 9 //moves cursor 1 chr right
|
|
|
|
typedef struct driver_private_data {
|
|
char device[200];
|
|
int fd;
|
|
int speed;
|
|
/* dimensions */
|
|
int width, height;
|
|
int cellwidth, cellheight;
|
|
/* framebuffer and buffer for old LCD contents */
|
|
unsigned char *framebuf;
|
|
unsigned char *backingstore;
|
|
/* defineable characters */
|
|
int ccmode;
|
|
int brightness;
|
|
int customchars;
|
|
int ISO_8859_1;
|
|
unsigned int refresh_timer;
|
|
unsigned char charmap[128];
|
|
int display_type;
|
|
char custom_char[31][7]; // used for "KD Rev 2.1" only
|
|
char custom_char_changed[32]; // used only if need_refresh==1
|
|
int need_refresh; // 1 if displays need refresh after changing custonchars
|
|
char hw_cmd[10][4]; // hardwarespecific commands
|
|
int usr_chr_dot_assignment[57]; // how to setup usercharacters
|
|
unsigned int usr_chr_mapping[31];// where to place the usercharacters (0..30) in the asciicode
|
|
int hbar_cc_offset; // character offset of the bars
|
|
int vbar_cc_offset; // character offset of the bars
|
|
char info[255];
|
|
} PrivateData;
|
|
|
|
/* Vars for the server core */
|
|
MODULE_EXPORT char *api_version = API_VERSION;
|
|
MODULE_EXPORT int stay_in_foreground = 0;
|
|
MODULE_EXPORT int supports_multiple = 0;
|
|
MODULE_EXPORT char *symbol_prefix = "serialVFD_";
|
|
|
|
/* Internal functions */
|
|
static void serialVFD_init_vbar (Driver *drvthis);
|
|
static void serialVFD_init_hbar (Driver *drvthis);
|
|
static void serialVFD_draw_frame (Driver *drvthis, unsigned char *dat);
|
|
static void serialVFD_put_brightness (Driver *drvthis);
|
|
static void serialVFD_put_char (Driver *drvthis, int n);
|
|
|
|
// Opens com port and sets baud correctly...
|
|
//
|
|
MODULE_EXPORT int
|
|
serialVFD_init (Driver *drvthis)
|
|
{
|
|
struct termios portset;
|
|
int tmp, w, h, brightness;
|
|
char size[200] = DEFAULT_SIZE;
|
|
|
|
PrivateData *p;
|
|
|
|
/* Allocate and store private data */
|
|
p = (PrivateData *) calloc(1, sizeof(PrivateData));
|
|
if (p == NULL)
|
|
return -1;
|
|
if (drvthis->store_private_ptr(drvthis, p))
|
|
return -1;
|
|
|
|
/* Initialize the PrivateData structure */
|
|
p->cellwidth = DEFAULT_CELL_WIDTH;
|
|
p->cellheight = DEFAULT_CELL_HEIGHT;
|
|
p->ccmode = CCMODE_STANDARD;
|
|
p->ISO_8859_1 = 1;
|
|
p->refresh_timer = 0;
|
|
|
|
debug(RPT_INFO, "%s(%p)", __FUNCTION__, drvthis );
|
|
|
|
/* Read config file */
|
|
/* Which device should be used */
|
|
strncpy(p->device, drvthis->config_get_string(drvthis->name, "Device", 0, DEFAULT_DEVICE), sizeof(p->device));
|
|
p->device[sizeof(p->device)-1] = '\0';
|
|
report(RPT_INFO, "%s: using Device %s", drvthis->name, p->device);
|
|
|
|
/* Which size */
|
|
strncpy(size, drvthis->config_get_string(drvthis->name, "Size", 0, DEFAULT_SIZE), sizeof(size));
|
|
size[sizeof(size)-1] = '\0';
|
|
if ((sscanf(size, "%dx%d", &w, &h) != 2)
|
|
|| (w <= 0) || (w > LCD_MAX_WIDTH)
|
|
|| (h <= 0) || (h > LCD_MAX_HEIGHT)) {
|
|
report(RPT_WARNING, "%s: cannot parse Size: %s; using default %s",
|
|
drvthis->name, size, DEFAULT_SIZE);
|
|
sscanf(DEFAULT_SIZE, "%dx%d", &w, &h);
|
|
}
|
|
p->width = w;
|
|
p->height = h;
|
|
|
|
/* Which backlight brightness */
|
|
tmp = drvthis->config_get_int(drvthis->name, "Brightness", 0, DEFAULT_BRIGHTNESS);
|
|
if ((tmp < 0) || (tmp > 1000)) {
|
|
report(RPT_WARNING, "%s: Brightness must be between 0 and 1000; using default %d",
|
|
drvthis->name, DEFAULT_BRIGHTNESS);
|
|
tmp = DEFAULT_BRIGHTNESS;
|
|
}
|
|
brightness = tmp;
|
|
|
|
|
|
/* ISO 8859 1 */
|
|
p->ISO_8859_1 = drvthis->config_get_bool(drvthis->name, "ISO_8859_1", 0, 1);
|
|
|
|
/* Which displaytype */
|
|
tmp = drvthis->config_get_int(drvthis->name, "Type", 0, DEFAULT_DISPLAYTYPE);
|
|
if ((tmp < 0) || (tmp > 3)) {
|
|
report(RPT_WARNING, "%s: Type must be between 0 and 3; using default %d",
|
|
drvthis->name, DEFAULT_DISPLAYTYPE);
|
|
tmp = DEFAULT_DISPLAYTYPE;
|
|
}
|
|
p->display_type = tmp;
|
|
|
|
|
|
/* Which speed */
|
|
tmp = drvthis->config_get_int(drvthis->name, "Speed", 0, DEFAULT_SPEED);
|
|
if ((tmp != 1200) && (tmp != 2400) && (tmp != 9600) && (tmp != 19200) && (tmp != 115200)) {
|
|
report(RPT_WARNING, "%s: Speed must be 1200, 2400, 9600, 19200 or 115200; using default %d",
|
|
drvthis->name, DEFAULT_SPEED);
|
|
tmp = DEFAULT_SPEED;
|
|
}
|
|
if (tmp == 1200) p->speed = B1200;
|
|
else if (tmp == 2400) p->speed = B2400;
|
|
else if (tmp == 9600) p->speed = B9600;
|
|
else if (tmp == 19200) p->speed = B19200;
|
|
else if (tmp == 115200) p->speed = B115200;
|
|
|
|
|
|
|
|
/* Set up io port correctly, and open it...*/
|
|
debug(RPT_DEBUG, "%s: Opening device: %s", __FUNCTION__, p->device);
|
|
p->fd = open(p->device, O_RDWR | O_NOCTTY | O_NDELAY);
|
|
if (p->fd == -1) {
|
|
report(RPT_ERR, "%s: open(%s) failed (%s)", drvthis->name, p->device, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
tcgetattr(p->fd, &portset);
|
|
|
|
// We use RAW mode
|
|
#ifdef HAVE_CFMAKERAW
|
|
// The easy way
|
|
cfmakeraw(&portset);
|
|
#else
|
|
// The hard way
|
|
portset.c_iflag &= ~( IGNBRK | BRKINT | PARMRK | ISTRIP
|
|
| INLCR | IGNCR | ICRNL | IXON );
|
|
portset.c_oflag &= ~OPOST;
|
|
portset.c_lflag &= ~( ECHO | ECHONL | ICANON | ISIG | IEXTEN );
|
|
portset.c_cflag &= ~( CSIZE | PARENB | CRTSCTS );
|
|
portset.c_cflag |= CS8 | CREAD | CLOCAL ;
|
|
#endif
|
|
|
|
// Set port speed
|
|
cfsetospeed(&portset, p->speed);
|
|
cfsetispeed (&portset, B0);
|
|
|
|
// Do it...
|
|
tcsetattr(p->fd, TCSANOW, &portset);
|
|
|
|
/* make sure the frame buffer is there... */
|
|
p->framebuf = (unsigned char *) malloc(p->width * p->height);
|
|
if (p->framebuf == NULL) {
|
|
report(RPT_ERR, "%s: unable to create framebuffer", drvthis->name);
|
|
return -1;
|
|
}
|
|
memset(p->framebuf, ' ', p->width * p->height);
|
|
|
|
/* make sure the framebuffer backing store is there... */
|
|
p->backingstore = (unsigned char *) malloc(p->width * p->height);
|
|
if (p->backingstore == NULL) {
|
|
report(RPT_ERR, "%s: unable to create framebuffer backing store", drvthis->name);
|
|
return -1;
|
|
}
|
|
memset(p->backingstore, ' ', p->width * p->height);
|
|
|
|
//setup displayspecific data
|
|
switch (p->display_type) {
|
|
//nec_fipc
|
|
case 0: //nec_fipc
|
|
p->need_refresh = 0; // 1 if displays need refresh after changing custonchars
|
|
p->customchars = 1; // number of customchaaracters the display provides
|
|
p->vbar_cc_offset = 5; // character offset of the bars
|
|
p->hbar_cc_offset = 12; // character offset of the bars
|
|
|
|
// hardwarespecific commands:
|
|
// hw_cmd[Command][data] = {{commandlength , command 1},
|
|
// .....
|
|
// {commandlength , command N}}
|
|
const char hw_cmd_0[10][4]= {{1 ,0x04}, // dark
|
|
{1 ,0x03},
|
|
{1 ,0x02},
|
|
{1 ,0x01}, // bright
|
|
{1 ,0x0D}, // pos1
|
|
{1 ,0x1B}, // move cursor
|
|
{1 ,0x0C}, // reset
|
|
{2 ,0x14, 0x11}, // init
|
|
{1 ,0x1A}, // set user char
|
|
{1 ,0x09}}; // tab
|
|
for (tmp = 0; tmp < 10; tmp++)
|
|
for (w = 0; w < 4 ;w++)
|
|
p->hw_cmd[tmp][w] = hw_cmd_0[tmp][w];
|
|
|
|
// Translates ISO 8859-1 to display charset.
|
|
const unsigned char charmap_0[] = {
|
|
/* #128 = 0x80 */
|
|
128, 129, 130, 131, 132, 133, 134, 135,
|
|
136, 137, 138, 139, 140, 141, 142, 143,
|
|
144, 145, 146, 147, 148, 149, 150, 151,
|
|
152, 153, 154, 155, 156, 157, 158, 159,
|
|
/* #160 = 0xA0 */
|
|
160, '!', 0xF7, 0xF8, '?', '?', 0x7C, 0xF9,
|
|
'"', '?', '?', '?', '?', '-', '?', '?',
|
|
0x8B, 0xF3, 0x89, 0x8A, 0x27, 0x98, '?', '.',
|
|
',', '?', '?', '?', '?', 0x8C, '?', 0xAA,
|
|
/* #192 = 0xC0 */
|
|
'A', 'A', 'A', 'A', 0xA2, 0xA8, 'A', 'C',
|
|
'E', 'E', 'E', 0xB6, 'I', 'I', 'I', 'I',
|
|
'D', 0xA9, 'O', 'O', 'O', 'O', 0xA3, 0x8D,
|
|
'0', 'U', 'U', 'U', 0xA4, 'Y', 'p', 0x91,
|
|
/* #224 = 0xE0 */
|
|
'a', 'a', 'a', 'a', 0xA5, 'a', 'a', 'c',
|
|
'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i',
|
|
'o', 'n', 'o', 'o', 'o', 'o', 0xA6, 0x8E,
|
|
'0', 'u', 'u', 'u', 0xA7, 'y', 'p', 'y'
|
|
};
|
|
for (tmp = 0; tmp < 128 ; tmp++)
|
|
p->charmap[tmp] = charmap_0[tmp];
|
|
|
|
//{bytes to send, icon bit mapped to bit 0, icon bit mapped to bit 1, ...}
|
|
const int usr_chr_dot_assignment_0[57]={ 7,
|
|
1, 2, 3, 4, 5, 0, 0, 0,
|
|
6, 7, 8, 9,10, 0, 0, 0,
|
|
11,12,13,14,15, 0, 0, 0,
|
|
16,17,18,19,20, 0, 0, 0,
|
|
21,22,23,24,25, 0, 0, 0,
|
|
26,27,28,29,30, 0, 0, 0,
|
|
31,32,33,34,35, 0, 0, 0 };
|
|
for (tmp = 0; tmp < 57 ;tmp++)
|
|
p->usr_chr_dot_assignment[tmp] = usr_chr_dot_assignment_0[tmp];
|
|
|
|
//Where to place the usercharacters (0..30) in the asciicode.
|
|
//Also used to map standardcharacters in the usercharacterspace(0..30)
|
|
//(useful for displays with less then 30 usercharacters and predefined bars)
|
|
const unsigned int usr_chr_mapping_0[31]=
|
|
{0xAF,0,0,0,0,0, 0x5F, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0, 0x5F, 0xE1, 0xE3, 0xE4};
|
|
for (tmp = 0; tmp < 31; tmp++)
|
|
p->usr_chr_mapping[tmp] = usr_chr_mapping_0[tmp];
|
|
break;
|
|
|
|
//KD Rev2.1
|
|
case 1: //KD Rev2.1
|
|
p->need_refresh=1; // 1 if displays need refresh after changing custonchars
|
|
p->customchars=31; // number of customchaaracters the display provides
|
|
p->vbar_cc_offset=0; // character offset of the bars
|
|
p->hbar_cc_offset=0; // character offset of the bars
|
|
|
|
// hardwarespecific commands:
|
|
// hw_cmd[Command][data] = {{commandlength , command 1},
|
|
// .....
|
|
// {commandlength , command N}}
|
|
const char hw_cmd_1[10][4]= {{1 ,0x04}, // dark
|
|
{1 ,0x03},
|
|
{1 ,0x02},
|
|
{1 ,0x01}, // bright
|
|
{1 ,0x0D}, // pos1
|
|
{1 ,0x1B}, // move cursor
|
|
{1 ,0x0C}, // reset
|
|
{2 ,0x14, 0x11}, // init
|
|
{1 ,0x1A}, // set user char
|
|
{1 ,0x09}}; // tab
|
|
for (tmp = 0; tmp < 10; tmp++)
|
|
for (w = 0; w < 4; w++)
|
|
p->hw_cmd[tmp][w] = hw_cmd_1[tmp][w];
|
|
|
|
const unsigned char charmap_1[] = {
|
|
/* #128 = 0x80 */
|
|
128, 129, 130, 131, 132, 133, 134, 135,
|
|
136, 137, 138, 139, 140, 141, 142, 143,
|
|
144, 145, 146, 147, 148, 149, 150, 151,
|
|
152, 153, 154, 155, 156, 157, 158, 159,
|
|
/* #160 = 0xA0 */
|
|
160, '!', 0xF7, 0xF8, '?', '?', 0x7C, 0xF9,
|
|
'"', '?', '?', '?', '?', '-', '?', '?',
|
|
0xA9, 0xBA, '?', '?', 0x27, '?', '?', '.',
|
|
',', '?', '?', '?', '?', '?', '?', '?',
|
|
/* #192 = 0xC0 */
|
|
0xB2, 'A', 'A', 'A', 0xA2, 0xA1, 'A', 'C',
|
|
'E', 'E', 0xA8, 0xB6, 0xAB, 0xAA, 0xAC, 0xA9,
|
|
'D', 0xAE, 0xB1, 0xB0, 0xBF, 'O', 0xA3, 'x',
|
|
0xAF, 0xB7, 0xB6, 0xB8, 0xA4, 'Y', 'p', 0x91,
|
|
/* #224 = 0xE0 */
|
|
0xD2, 0xC2, 0xC4, 'a', 0xA5, 0xC1, 0xC5, 0xC3,
|
|
0xC7, 0xC6, 0xC8, 0xD4, 0xCC, 0xCB, 0xCD, 0xCA,
|
|
'o', 0xCF, 0xD1, 0xD0, 0xCE, 'o', 0xA6, 0xBB,
|
|
0xD0, 0xD7, 0xD6, 0xD8, 0xA7, 'y', 'p', 'y'
|
|
};
|
|
for (tmp = 0; tmp < 128; tmp++)
|
|
p->charmap[tmp] = charmap_1[tmp];
|
|
|
|
|
|
//{bytes to send, icon bit mapped to bit 0, icon bit mapped to bit 1, ...}
|
|
const int usr_chr_dot_assignment_1[57]={ 7,
|
|
1, 2, 3, 4, 5, 0, 0, 0,
|
|
6, 7, 8, 9,10, 0, 0, 0,
|
|
11,12,13,14,15, 0, 0, 0,
|
|
16,17,18,19,20, 0, 0, 0,
|
|
21,22,23,24,25, 0, 0, 0,
|
|
26,27,28,29,30, 0, 0, 0,
|
|
31,32,33,34,35, 0, 0, 0};
|
|
for (tmp = 0; tmp < 57; tmp++)
|
|
p->usr_chr_dot_assignment[tmp] = usr_chr_dot_assignment_1[tmp];
|
|
|
|
//Where to place the usercharacters (0..30) in the asciicode.
|
|
//Also used to map standardcharacters in the usercharacterspace(0..30)
|
|
//(useful for displays with less then 30 usercharacters and predefined bars)
|
|
const unsigned int usr_chr_mapping_1[31]=
|
|
{0xAF};
|
|
for (tmp = 0; tmp < 31; tmp++)
|
|
p->usr_chr_mapping[tmp] = usr_chr_mapping_1[tmp];
|
|
break;
|
|
|
|
//Noritake
|
|
case 2: //Noritake
|
|
p->need_refresh=0; // 1 if displays need refresh after changing custonchars
|
|
p->customchars=16; // number of customchaaracters the display provides
|
|
p->vbar_cc_offset=0; // character offset of the bars
|
|
p->hbar_cc_offset=0; // character offset of the bars
|
|
|
|
// hardwarespecific commands:
|
|
// hw_cmd[Command][data] = {{commandlength , command 1},
|
|
// .....
|
|
// {commandlength , command N}}
|
|
const char hw_cmd_2[10][4]= {{1 ,0x1B, 0x4C, 0x00}, // dark
|
|
{1 ,0x1B, 0x4C, 0x50},
|
|
{2 ,0x1B, 0x4C, 0x90},
|
|
{1 ,0x1B, 0x4C, 0xFF}, // bright
|
|
{1 ,0x0C}, // pos1
|
|
{2 ,0x1B, 0x48}, // move cursor
|
|
{2 ,0x1B, 0x49}, // reset
|
|
{2 ,0x14, 0x11}, // init
|
|
{2 ,0x1B, 0x43}, // set user char
|
|
{1 ,0x09}}; // tab
|
|
for (tmp = 0; tmp < 10; tmp++)
|
|
for (w = 0; w < 4; w++)
|
|
p->hw_cmd[tmp][w] = hw_cmd_2[tmp][w];
|
|
|
|
// no charmap needed
|
|
for (tmp = 128; tmp <= 255; tmp++)
|
|
p->charmap[tmp]=tmp;
|
|
|
|
//{bytes to send, icon bit mapped to bit 0, icon bit mapped to bit 1, ...}
|
|
const int usr_chr_dot_assignment_2[57]={ 5,
|
|
1, 2, 3, 4, 5, 6, 7, 8,
|
|
9,10,11,12,13,14,15,16,
|
|
17,18,19,20,21,22,23,24,
|
|
25,26,27,28,29,30,31,32,
|
|
33,34,35, 0, 0, 0, 0, 0};
|
|
for (tmp=0; tmp < 57; tmp++)
|
|
p->usr_chr_dot_assignment[tmp]=usr_chr_dot_assignment_2[tmp];
|
|
|
|
//Where to place the usercharacters (0..30) in the asciicode.
|
|
//Also used to map standardcharacters in the usercharacterspace(0..30)
|
|
//(useful for displays with less then 30 usercharacters and predefined bars)
|
|
const unsigned int usr_chr_mapping_2[31]=
|
|
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,\
|
|
0x0F, 0x10, 0x13 , 0x14, 0x1C, 0x1D, 0x1E, 0x1F};
|
|
for (tmp=0;tmp < 31 ;tmp++)
|
|
p->usr_chr_mapping[tmp]=usr_chr_mapping_2[tmp];
|
|
break;
|
|
//Futaba
|
|
case 3: //Futaba
|
|
p->need_refresh=0; // 1 if displays need refresh after changing custonchars
|
|
p->customchars=3; // number of customchaaracters the display provides
|
|
p->vbar_cc_offset=0x30; // character offset of the bars
|
|
p->hbar_cc_offset=0x30; // character offset of the bars
|
|
|
|
// hardwarespecific commands:
|
|
// hw_cmd[Command][data] = {{commandlength , command 1},
|
|
// .....
|
|
// {commandlength , command N}}
|
|
const char hw_cmd_3[10][4]= {{1 ,0x04, 0x20}, // dark
|
|
{1 ,0x04, 0x40},
|
|
{2 ,0x04, 0x60},
|
|
{1 ,0x04, 0xFF}, // bright
|
|
{2 ,0x10, 0x00}, // pos1
|
|
{1 ,0x10,}, // move cursor
|
|
{1 ,0x1F}, // reset
|
|
{1 ,0x11}, // init
|
|
{1 ,0x03}, // set user char
|
|
{1 ,0x09}}; // tab
|
|
for (tmp=0;tmp < (10) ;tmp++)
|
|
for (w=0;w < (4) ;w++)
|
|
p->hw_cmd[tmp][w]=hw_cmd_3[tmp][w];
|
|
|
|
// Translates ISO 8859-1 to display charset.
|
|
const unsigned char charmap_3[] = {
|
|
/* #128 = 0x80 */
|
|
128, 129, 130, 131, 132, 133, 134, 135,
|
|
136, 137, 138, 139, 140, 141, 142, 143,
|
|
144, 145, 146, 147, 148, 149, 150, 151,
|
|
152, 153, 154, 155, 156, 157, 158, 159,
|
|
/* #160 = 0xA0 */
|
|
160, 0xAD, 0x9B, 0x9C, 0xC8, 0x9D, 0x7C, 0xC0,
|
|
'"', '?', 0xA6, 0xAE, 0xAA, '-', '?', '?',
|
|
0xEF, 0xCA, 0xC6, 0xC7, 0x27, 0xB8, '?', '.',
|
|
',', '?', 0xA7, 0xAF, 0xAC, 0xAB, '?', 0xA8,
|
|
/* #192 = 0xC0 */
|
|
0xD0, 'A', 0xD5, 'A', 0x8E, 0x8F, 0x92, 0x80,
|
|
0xD1, 0x90, 0xD6, 0xD3, 'I', 'I', 0xD7, 0xD4,
|
|
'D', 0xA5, 'O', 'O', 0xD8, 'O', 0x99, 'x',
|
|
'0', 0xD2, 'U', 0xD9, 0x9A, 'Y', 'p', 0xB1,
|
|
/* #224 = 0xE0 */
|
|
0x85, 0xA0, 0x83, 'a', 0x84, 0x86, 0x91, 0x87,
|
|
0x8A, 0x82, 0x88, 0x89, 0x8D, 0xA1, 0x8C, 0x8B,
|
|
'o', 0xA4, 0x95, 0xA9, 0x93, 'o', 0x94, '/',
|
|
'0', 0x97, 0xA3, 0x96, 0x81, 'y', 'p', 0x89
|
|
};
|
|
for (tmp = 0; tmp < 128; tmp++)
|
|
p->charmap[tmp] = charmap_3[tmp];
|
|
|
|
//{bytes to send, icon bit mapped to bit 0, icon bit mapped to bit 1, ...}
|
|
const int usr_chr_dot_assignment_3[57]={ 5,
|
|
8, 7, 6, 5, 4, 3, 2, 1,
|
|
16,15,14,13,12,11,10, 9,
|
|
24,23,22,21,20,19,18,17,
|
|
32,31,30,29,28,27,26,25,
|
|
0, 0, 0, 0, 0,35,34,33};
|
|
for (tmp=0;tmp < 57 ;tmp++)
|
|
p->usr_chr_dot_assignment[tmp]=usr_chr_dot_assignment_3[tmp];
|
|
|
|
//Where to place the usercharacters (0..30) in the asciicode.
|
|
//Also used to map standardcharacters in the usercharacterspace(0..30)
|
|
//(useful for displays with less then 30 usercharacters and predefined bars)
|
|
const unsigned int usr_chr_mapping_3[31]=
|
|
{0xCD, 0xCE, 0xCF};
|
|
for (tmp = 0; tmp < 31; tmp++)
|
|
p->usr_chr_mapping[tmp] = usr_chr_mapping_3[tmp];
|
|
break;
|
|
}
|
|
|
|
//initialise display
|
|
write(p->fd, &p->hw_cmd[reset][1],p->hw_cmd[reset][0]);
|
|
write(p->fd, &p->hw_cmd[init_cmds][1],p->hw_cmd[init_cmds][0]);
|
|
serialVFD_set_brightness(drvthis, 1, brightness);
|
|
|
|
report(RPT_DEBUG, "%s: init() done", drvthis->name);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Changes screen brightness (0 min ... 255 max ... 1000 also max)
|
|
//
|
|
MODULE_EXPORT void
|
|
serialVFD_set_brightness (Driver *drvthis, int state, int brightness)
|
|
{ // set p->brightness
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
if (brightness > 0) {
|
|
if (brightness > 255)
|
|
brightness = 255;
|
|
if (p->brightness != brightness) {
|
|
p->brightness = brightness;
|
|
serialVFD_put_brightness(drvthis);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
serialVFD_put_brightness (Driver *drvthis)
|
|
{ // set hardware brightness
|
|
PrivateData *p = drvthis->private_data;
|
|
int realbrightness = (int) (p->brightness / 64);
|
|
|
|
//(4 steps 0-64, 65-128, 129-192, 193-1000)
|
|
write(p->fd, &p->hw_cmd[realbrightness][1],\
|
|
p->hw_cmd[realbrightness][0]);
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Draws a vertical bar...
|
|
//
|
|
MODULE_EXPORT void
|
|
serialVFD_vbar (Driver *drvthis, int x, int y, int len, int promille, int options)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
serialVFD_init_vbar(drvthis);
|
|
lib_vbar_static(drvthis, x, y, len, promille, options, p->cellheight, p->vbar_cc_offset);
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Draws a horizontal bar to the right.
|
|
//
|
|
|
|
MODULE_EXPORT void
|
|
serialVFD_hbar (Driver *drvthis, int x, int y, int len, int promille, int options)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
serialVFD_init_hbar(drvthis);
|
|
lib_hbar_static(drvthis, x, y, len, promille, options, p->cellwidth, p->hbar_cc_offset);
|
|
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Sets a custom character from 0-7...
|
|
//
|
|
// For input, value 1 mean "on" and 0 is "off".
|
|
//
|
|
// The input is just an array of characters...
|
|
//
|
|
MODULE_EXPORT void
|
|
serialVFD_set_char (Driver *drvthis, int n, unsigned char *dat)
|
|
{ //set char in p->custom_char
|
|
PrivateData *p = drvthis->private_data;
|
|
unsigned int byte, bit;
|
|
|
|
if ((n < 0) || (n > p->customchars-1))
|
|
return;
|
|
if (!dat)
|
|
return;
|
|
|
|
for (byte = 0; byte < p->usr_chr_dot_assignment[0]; byte++) {
|
|
int letter = 0;
|
|
|
|
for (bit = 0; bit < 8; bit++) {
|
|
int pos = (int) p->usr_chr_dot_assignment[bit+8*byte+1];
|
|
|
|
if (pos > 0) {
|
|
int posbyte = (pos-1) / 5;
|
|
int posbit = 4 - ((pos-1) % 5);
|
|
|
|
letter |= ((dat[posbyte] >> posbit) & 1) << bit;;
|
|
}
|
|
}
|
|
p->custom_char[n][byte] = letter;
|
|
}
|
|
|
|
if (p->display_type != 1) //not KD Rev 2.1
|
|
serialVFD_put_char(drvthis, n);
|
|
|
|
if (p->need_refresh == 1)
|
|
p->custom_char_changed[n] = 1;
|
|
}
|
|
|
|
static void
|
|
serialVFD_put_char (Driver *drvthis, int n)
|
|
{ // put char in display
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
write(p->fd, &p->hw_cmd[set_user_char][1],
|
|
p->hw_cmd[set_user_char][0]);// substitute and select Character to overwrite
|
|
write(p->fd, &p->usr_chr_mapping[n], 1);
|
|
write(p->fd, &p->custom_char[n][0], p->usr_chr_dot_assignment[0]);// overwrite selected Character
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Blasts a single frame onscreen, to the lcd...
|
|
//
|
|
// Input is a character array, sized serialVFD->width*serialVFD->height
|
|
//
|
|
static void
|
|
serialVFD_draw_frame (Driver *drvthis, unsigned char *dat)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
int i, j, last_chr = -10;
|
|
|
|
if (!dat)
|
|
return;
|
|
if (p->refresh_timer > 500) { // Do a full refresh every 500 refreshs.
|
|
// With this it is possible to switch display on and off while lcdproc is running
|
|
write(p->fd, &p->hw_cmd[init_cmds][1],p->hw_cmd[init_cmds][0]);
|
|
serialVFD_put_brightness(drvthis); // restore brightness
|
|
|
|
for (i = 0; i < (p->height * p->width); i++)
|
|
p->backingstore[i] = '\0'; // clear Backing-store
|
|
|
|
if (p->display_type != 1) {//not KD Rev 2.1
|
|
for (i = 0; i < p->customchars; i++) // refresh all customcharacters
|
|
serialVFD_put_char(drvthis, i);
|
|
/* {
|
|
write(p->fd, &p->hw_cmd[set_user_char][1],\
|
|
p->hw_cmd[set_user_char][0]);// substitute and select character to overwrite
|
|
write(p->fd, &p->usr_chr_mapping[i], 1);
|
|
write(p->fd, &p->custom_char[(int)dat[i]][0], p->usr_chr_dot_assignment[0]);// overwrite selected Character
|
|
}*/
|
|
}
|
|
p->refresh_timer = 0;
|
|
}
|
|
p->refresh_timer++;
|
|
|
|
for (i = 0; i < (p->height * p->width); i++) {
|
|
/* Backing-store implementation. If it's already
|
|
* on the screen, don't put it there again
|
|
*/
|
|
|
|
if (dat[i] != p->backingstore[i] || (dat[i] <=30 && p->custom_char_changed[(int)dat[i]]))
|
|
{
|
|
if (last_chr < i-1) { // if not last char written cursor has to be moved.
|
|
if (last_chr < i-2-p->hw_cmd[mv_cursor][0]) {
|
|
write(p->fd, &p->hw_cmd[mv_cursor][1],
|
|
p->hw_cmd[mv_cursor][0]);
|
|
write(p->fd, &i, 1);
|
|
}
|
|
else {
|
|
for (j = last_chr; j < (i-1); j++) {
|
|
write(p->fd, &p->hw_cmd[hor_tab][1], p->hw_cmd[hor_tab][0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dat[i] <= 30) {// custom character
|
|
if (p->display_type == 1) {// KD Rev 2.1 only
|
|
write(p->fd, "\x1A\xDB", 2); // substitute and select character to overwrite(237)
|
|
write(p->fd, &p->custom_char[(int)dat[i]][0], 7);// overwrite selected character
|
|
write(p->fd, "\xDB", 1); // write character
|
|
}
|
|
else { // all other displays
|
|
write(p->fd, &p->usr_chr_mapping[(int)dat[i]], 1);
|
|
}
|
|
}
|
|
else if (dat[i] > 127 && (p->ISO_8859_1 != 0)) { // ISO_8859_1 translation for 129 ... 255
|
|
write(p->fd, &p->charmap[dat[i] + 128], 1);
|
|
}
|
|
else {
|
|
write(p->fd, &dat[i], 1);
|
|
}
|
|
|
|
last_chr = i;
|
|
}
|
|
}
|
|
|
|
if (last_chr != -10) { // update backingstore if something changed
|
|
int i;
|
|
|
|
memcpy(p->backingstore, dat, p->height * p->width);
|
|
|
|
for (i = 0; i < p->customchars; i++)
|
|
p->custom_char_changed[i] = '\0';
|
|
}
|
|
}
|
|
|
|
|
|
MODULE_EXPORT void
|
|
serialVFD_num( Driver * drvthis, int x, int num )
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
int do_init = 0;
|
|
|
|
if (p->ccmode != CCMODE_BIGNUM) { // Are the customcharacters set up correctly? If not:
|
|
do_init = 1; // Lib_adv_bignum has to set the customcharacters.
|
|
p->ccmode = CCMODE_BIGNUM; // Switch customcharactermode to bignum.
|
|
}
|
|
// Lib_adv_bignum does everything needed to show the bignumbers.
|
|
lib_adv_bignum(drvthis, x, num, do_init, p->customchars);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Places an icon on screen
|
|
*/
|
|
MODULE_EXPORT int
|
|
serialVFD_icon (Driver *drvthis, int x, int y, int icon)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
static unsigned char heart_open[] =
|
|
{ b__XXXXX,
|
|
b__X_X_X,
|
|
b_______,
|
|
b_______,
|
|
b_______,
|
|
b__X___X,
|
|
b__XX_XX,
|
|
b__XXXXX };
|
|
static unsigned char heart_filled[] =
|
|
{ b__XXXXX,
|
|
b__X_X_X,
|
|
b___X_X_,
|
|
b___XXX_,
|
|
b___XXX_,
|
|
b__X_X_X,
|
|
b__XX_XX,
|
|
b__XXXXX };
|
|
|
|
// Yes we know, this is a VERY BAD implementation :-)
|
|
switch (icon) {
|
|
case ICON_BLOCK_FILLED:
|
|
serialVFD_chr(drvthis, x, y, 127);
|
|
break;
|
|
case ICON_HEART_FILLED:
|
|
p->ccmode = CCMODE_STANDARD;
|
|
serialVFD_set_char(drvthis, 0, heart_filled);
|
|
serialVFD_chr(drvthis, x, y, 0);
|
|
break;
|
|
case ICON_HEART_OPEN:
|
|
p->ccmode = CCMODE_STANDARD;
|
|
serialVFD_set_char(drvthis, 0, heart_open);
|
|
serialVFD_chr(drvthis, x, y, 0);
|
|
break;
|
|
default:
|
|
return -1; // Let the core do other icons
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Sets up for vertical bars. Call before serialVFD->vbar()
|
|
//
|
|
static void
|
|
serialVFD_init_vbar (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
if (p->ccmode != CCMODE_VBAR) {
|
|
unsigned char vBar[p->cellheight];
|
|
int i;
|
|
|
|
p->ccmode = CCMODE_VBAR;
|
|
|
|
memset(vBar, 0x00, sizeof(vBar));
|
|
|
|
for (i = 1; i < p->cellheight; i++) {
|
|
// add pixel line per pixel line ...
|
|
vBar[p->cellheight - i] = 0xFF;
|
|
serialVFD_set_char(drvthis, i, vBar);
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Inits horizontal bars...
|
|
//
|
|
static void
|
|
serialVFD_init_hbar (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
if (p->ccmode != CCMODE_HBAR) {
|
|
unsigned char hBar[p->cellheight];
|
|
int i;
|
|
|
|
p->ccmode = CCMODE_HBAR;
|
|
|
|
for (i = 1; i < p->cellwidth; i++) {
|
|
// fill pixel columns from left to right.
|
|
memset(hBar, 0xFF & ~((1 << (p->cellwidth - i)) - 1), sizeof(hBar));
|
|
serialVFD_set_char(drvthis, i, hBar);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Clears the LCD screen
|
|
//
|
|
MODULE_EXPORT void
|
|
serialVFD_clear (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
memset(p->framebuf, ' ', p->width * p->height);
|
|
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Prints a string on the lcd display, at position (x,y). The
|
|
// upper-left is (1,1), and the lower right should be (20,4).
|
|
//
|
|
MODULE_EXPORT void
|
|
serialVFD_string (Driver *drvthis, int x, int y, char string[])
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
int i;
|
|
|
|
x--;
|
|
y--;
|
|
for (i = 0; string[i] != '\0'; i++) {
|
|
// Check for buffer overflows...
|
|
if ((y * p->width) + x + i > (p->width * p->height))
|
|
break;
|
|
p->framebuf[(y * p->width) + x + i] = string[i];
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Clean-up
|
|
//
|
|
MODULE_EXPORT void
|
|
serialVFD_close (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
if (p != NULL) {
|
|
close(p->fd);
|
|
|
|
if (p->framebuf)
|
|
free(p->framebuf);
|
|
|
|
if (p->backingstore)
|
|
free(p->backingstore);
|
|
free(p);
|
|
}
|
|
drvthis->store_private_ptr(drvthis, NULL);
|
|
}
|
|
|
|
/*
|
|
* Returns the display width in characters
|
|
*/
|
|
MODULE_EXPORT int
|
|
serialVFD_width (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
return p->width;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the display height in characters
|
|
*/
|
|
MODULE_EXPORT int
|
|
serialVFD_height (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
return p->height;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Returns the maximum number of custom char slots (not how many
|
|
// are free at a moment, maybe this isn't even needed...
|
|
//
|
|
MODULE_EXPORT int
|
|
serialVFD_get_free_chars (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
return p->customchars;
|
|
}
|
|
/*
|
|
* Returns the width of a character in pixels
|
|
*/
|
|
MODULE_EXPORT int
|
|
serialVFD_cellwidth (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
return p->cellwidth;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the height of a character in pixels
|
|
*/
|
|
MODULE_EXPORT int
|
|
serialVFD_cellheight (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
return p->cellheight;
|
|
}
|
|
|
|
|
|
MODULE_EXPORT void
|
|
serialVFD_flush (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
serialVFD_draw_frame(drvthis, p->framebuf);
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// Prints a character on the lcd display, at position (x,y). The
|
|
// upper-left is (1,1), and the lower right should be (20,4).
|
|
//
|
|
MODULE_EXPORT void
|
|
serialVFD_chr (Driver *drvthis, int x, int y, char c)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
y--;
|
|
x--;
|
|
p->framebuf[(y * p->width) + x ] = c;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
//
|
|
// Retrieves brightness
|
|
//
|
|
MODULE_EXPORT int
|
|
serialVFD_get_brightness(Driver *drvthis, int state)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
return p->brightness;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// provides some info about this driver
|
|
//
|
|
MODULE_EXPORT const char *
|
|
serialVFD_get_info (Driver *drvthis)
|
|
{
|
|
PrivateData *p = drvthis->private_data;
|
|
|
|
strcpy(p->info, "Driver for many serialVFDs from NEC(all FIPC based), Noritake, Futaba and the \"KD Rev2.1\"VFD.");
|
|
return p->info;
|
|
}
|