/** \file server/drivers/sdeclcd.c * This is the LCDproc driver for SDEC LCD Devices. * They are found in the Watchguard FireBox firewall appliances. * They are interfaced through the parallel port. * * The code is based on the spec file LMC-S2D20-01.pdf, * a technical hardware and programming guide for this LCD. */ /*- * Copyright(C) 2011, Francois Mertz * * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #ifdef HAVE_STDLIB_H # include #endif #ifdef HAVE_STRING_H # include #endif #include "port.h" #include "lpt-port.h" #include "timing.h" #include "lcd.h" #include "report.h" #include "adv_bignum.h" #include "lcd_lib.h" #include "sdeclcd.h" /* * The following definitions are specific for the SDEC LCD device, * and the way it is wired in the Watchguard Firebox. There is a * mapping between the parallel port Control register and the * LCD control lines, as follows: (see lpt-port.h for defs) */ #define SDEC_BACKLIGHT STRB /* Strobe, bit 0 */ #define SDEC_ENABLE LF /* Linefeed, bit */ #define SDEC_CONTRAST INIT /* Init, bit 2 <--FIXME, Untested */ #define SDEC_REG_SEL SEL /* Select Printer, bit 3 */ /* * The following definitions are specific for the SDEC LCD device */ #define SDEC_DISP_W 20 /* Display Width */ #define SDEC_DISP_H 2 /* Display Height */ #define SDEC_ADD_LINE1 0x80 /* Address of beginning of Line 1 */ #define SDEC_ADD_LINE2 0xC0 /* Address of beginning of Line 2 */ #define SDEC_CELL_W 5 /* Character cell Width */ #define SDEC_CELL_H 8 /* Character cell Height */ #define SDEC_NUM_CC 8 /* Number of Custom Characters */ #define SDEC_IDENT "SDEC LCD Driver with Keyboard, Version " VERSION /* * Instruction Set for the SDEC LCD device */ #define SDEC_FN_CLEAR_DISPLAY 0x01 /* Clears the LCD */ #define SDEC_FN_RETURN_HOME 0x02 /* Returns cursor home */ #define SDEC_FN_SET_ENTRY_MODE 0x04 /* Sets Entry Mode */ #define SDEC_OPT_INCREMENT 0x02 /* Cursor moves right */ #define SDEC_FN_DISPLAY 0x08 /* Display: */ #define SDEC_OPT_DISPLAY_ON 0x04 /* Display ON option */ #define SDEC_OPT_CURSOR_ON 0x02 /* Cursor ON option */ #define SDEC_OPT_BLINK_ON 0x01 /* Blinking Cursor option */ #define SDEC_FN_CURSOR_R 0x14 /* Move cursor right 1 position */ #define SDEC_FN_CURSOR_L 0x10 /* Move cursor left 1 position */ #define SDEC_FN_FUNCTION_SET 0x20 /* Function Set: */ #define SDEC_OPT_8_BIT 0x10 /* 8bit option (vs 4 bit) */ #define SDEC_OPT_2_LINES 0x08 /* 2 Lines (vs 1 Line) */ #define SDEC_OPT_5X10 0x04 /* Each char is 5x10 (vs 5x7) */ #define SDEC_FN_CG_ADD 0x40 /* Set Custom Char address */ #define SDEC_FN_DD_ADD 0x80 /* Set Display address (cursor) */ #define SDEC_DATA SDEC_REG_SEL /* For data, RS is ON */ #define SDEC_EXEC 0x00 /* For exec, RS is OFF */ #define SDEC_HOLD 40 /* Min exec time in micro sec */ #define SDEC_HOLD_ENABLE 1 /* Min time to hold ENABLE in micro * sec, should be 450 nano sec */ /* * Basic PC parallel port constants. At this point, the driver only supports a * SDEC LCD device on port 0, as all Fireboxes seem to be wired that way. * * http://en.wikipedia.org/wiki/Parallel_port */ #define LPT_PORT_0 0x0378 #define LPT_PORT_1 0x0278 #define LPT_PORT_2 0x03BC #define LPT_DEFAULT LPT_PORT_0 #define LPT_STATUS (LPT_DEFAULT+1) #define LPT_CONTROL (LPT_DEFAULT+2) /* Control port hardware inversion */ #define LPT_CTRL_MASK OUTMASK /* Bits actually used by Status port */ #define LPT_STUS_MASK (SELIN|PAPEREND|ACK|BUSY|FAULT) /* * Driver implementation choices. * * Heart beat location on screen, and corresponding address, char, * and backlight timeout */ #define SDEC_HB_LOC (SDEC_DISP_W-1) #define SDEC_HB_ADD (SDEC_ADD_LINE1+SDEC_HB_LOC) #define SDEC_HB_CHAR ':' #define SDEC_BKLT_DFT 30 /** * This structure will hold data private to this Driver * As the driver is implemented as a Shared Library, persistent data needs to * be put in a dynamically allocated struct. Otherwise, all instances of the * library would share the same values. */ typedef struct driver_private_data { CGmode ccmode; /* Current custom character mode */ unsigned char bklgt; /* Backlight status */ unsigned char lastkbd; /* Last keyboard status */ unsigned char hb_stus; /* Heart Beat status */ unsigned bklgt_timer; /* Seconds to keep the backlight on */ time_t bklgt_lasttime; /* When back light was turned on */ time_t hrbt_lasttime; /* When heart beat was output */ char *framebuf; /* Frame buffer */ char *framelcd; /* Frame buffer on the LCD */ char *vbar_cg; /* Vertical Bar bitmaps */ char *hbar_cg; /* Horizontal Bar bitmaps */ char *bignum_cg; /* Big Numbers bitmaps */ } PrivateData; /* Variables assisting the driver loader */ MODULE_EXPORT char *api_version = API_VERSION; MODULE_EXPORT int stay_in_foreground = 0; /* Foreground is not required */ MODULE_EXPORT int supports_multiple = 0; /* No support for mult. LCDs */ MODULE_EXPORT char *symbol_prefix = "sdeclcd_"; /* Prefix for API entries */ /** * Main LCD control routine * \param register_select What to set the Register Select line to * \param backlight What to set the Backlight line to * \param data Data to send to LCD (char or instruction) * \param usec Wait time in micro seconds * * This is the basic routine that controls the LCD. It interfaces with the * parallel ports and contains the logic for flipping and holding the lines * as required. It tries and be comprehensive enough to avoid spreading * LPT port logic all over the code. * * The Register Select line determines if an output is to be understood as an * instruction, or as data to be displayed. * The Backlight line controls the backlight in a direct manner. * * The sequence is: * Bring Enable up * Output the data (command or character) * Hold Enable for 450 nano sec (.45 micro sec) * Bring Enable down * Wait for the LCD to execute the command, typically 40 micro sec * * Take into account the hardware inversion of some control port bits. */ static inline void _sdec_control_wait(unsigned char register_select, unsigned char backlight, unsigned char data, int usec) { port_out(LPT_CONTROL, (register_select | backlight | SDEC_ENABLE) ^ LPT_CTRL_MASK); port_out(LPT_DEFAULT, data); timing_uPause(SDEC_HOLD_ENABLE); port_out(LPT_CONTROL, (register_select | backlight) ^ LPT_CTRL_MASK); timing_uPause(usec); } /** * These macros are meant to be used by the driver code. They are wrappers * around the low level routine, and capture the Register Select logic. */ #define sdec_write(a,b) _sdec_control_wait(SDEC_DATA, b, a, SDEC_HOLD) #define sdec_exec(a,b) _sdec_control_wait(SDEC_EXEC, b, a, SDEC_HOLD) #define sdec_exec_wait(a,b) _sdec_control_wait(SDEC_EXEC, 0, a,b) /** * This is the mandatory initialization sequence for the LCD, as per the SDEC * programming guide. Note the required execution times. */ static void sdec_init() { /* Mandatory reset sequence */ sdec_exec_wait(SDEC_FN_FUNCTION_SET | SDEC_OPT_8_BIT, 15000); sdec_exec_wait(SDEC_FN_FUNCTION_SET | SDEC_OPT_8_BIT, 4100); sdec_exec_wait(SDEC_FN_FUNCTION_SET | SDEC_OPT_8_BIT, 100); sdec_exec_wait(SDEC_FN_FUNCTION_SET | SDEC_OPT_8_BIT | SDEC_OPT_2_LINES, 100); sdec_exec_wait(SDEC_FN_DISPLAY, 40); sdec_exec_wait(SDEC_FN_CLEAR_DISPLAY, 1640); sdec_exec_wait(SDEC_FN_SET_ENTRY_MODE | SDEC_OPT_INCREMENT, 40); /* Additional command */ sdec_exec(SDEC_FN_DISPLAY | SDEC_OPT_DISPLAY_ON, 0); } /** * API: Initialize the driver. */ MODULE_EXPORT int sdeclcd_init(Driver *drvthis) { PrivateData *p; int i, j; /* * Compact version of big num character bitmaps. Stack each line * separately to visualize each char. */ static char bignum_bitmaps[] = { b___XXXX, b__XX___, b__XX___, b__XX___, b__XX___, b__XX___, b__XX___, b__XX___, b__XXXX_, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b__XX___, b__XX___, b__XX___, b__XX___, b__XX___, b__XX___, b__XX___, b___XXXX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b__XXXX_, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b__XXXXX, b_______, b_______, b_______, b_______, b_______, b_______, b__XXXXX, b__XXXX_, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b_____XX, b__XXXX_, b___XXXX, b__XX___, b__XX___, b__XX___, b__XX___, b__XX___, b__XX___, b___XXXX }; /* Allocate and store private data */ p = (PrivateData *) malloc(sizeof(PrivateData)); if (p == NULL) return -1; if (drvthis->store_private_ptr(drvthis, p)) return -1; /* Initialize private data and read config */ p->ccmode = standard; p->bklgt = BACKLIGHT_ON; p->bklgt_timer = SDEC_BKLT_DFT; p->bklgt_lasttime = time(NULL); p->hrbt_lasttime = time(NULL); p->hb_stus = HEARTBEAT_OFF; p->framebuf = (char *)malloc(SDEC_DISP_W * SDEC_DISP_H); p->framelcd = (char *)malloc(SDEC_DISP_W * SDEC_DISP_H); p->vbar_cg = (char *)malloc(SDEC_CELL_H * SDEC_NUM_CC); p->hbar_cg = (char *)malloc(SDEC_CELL_H * SDEC_NUM_CC); p->bignum_cg = (char *)bignum_bitmaps; if (NULL == p->framebuf || NULL == p->framelcd || NULL == p->vbar_cg || NULL == p->hbar_cg) { report(RPT_ERR, "%s: unable to allocate framebuffer", drvthis->name); return -1; } memset(p->framebuf, ' ', SDEC_DISP_W * SDEC_DISP_H); memset(p->framelcd, ' ', SDEC_DISP_W * SDEC_DISP_H); /* Prepare custom character bitmaps:vbar, hbar */ for (i = 0; i < SDEC_NUM_CC; i++) { for (j = 0; j < SDEC_CELL_H; j++) { /* Visualize this: */ p->vbar_cg[i * SDEC_CELL_H + SDEC_CELL_H - j - 1] = (j <= i) ? 0xFF : 0; p->hbar_cg[i * SDEC_CELL_H + j] = (~(0x0F >> i)) & 0x1F; } } p->bignum_cg = bignum_bitmaps; /* Initializes the scheduler, see timing.h */ timing_init(); /* Initialize access to the ports */ if (port_access_multiple(LPT_DEFAULT, 3)) { report(RPT_ERR, "%s: cannot get IO-permission for 0x%03X! Are we root?", drvthis->name, LPT_DEFAULT); return -1; } sdec_init(); return 0; } /** * API: Close the driver (do necessary clean-up). */ MODULE_EXPORT void sdeclcd_close(Driver *drvthis) { PrivateData *p = (PrivateData *) drvthis->private_data; if (p) { if (p->framebuf) free(p->framebuf); if (p->framelcd) free(p->framelcd); if (p->vbar_cg) free(p->vbar_cg); if (p->hbar_cg) free(p->hbar_cg); free(p); } if (!port_deny_multiple(LPT_DEFAULT, 3)) { report(RPT_WARNING, "%s: cannot release IO-permission for 0x%03X!", drvthis->name, LPT_DEFAULT); } drvthis->store_private_ptr(drvthis, NULL); } /** * API: Return the display width in characters. */ MODULE_EXPORT int sdeclcd_width(Driver *drvthis) { return SDEC_DISP_W; } /** * API: Return the display height in characters. */ MODULE_EXPORT int sdeclcd_height(Driver *drvthis) { return SDEC_DISP_H; } /** * API: Flush the contents of the framebuffer out to the LCD * * The idea is that we only write to the LCD what we must. Therefore we compare * the content of the framebuffer to the content of the LCD as we last wrote it * and write when there is a diff. Writing involves setting the cursor (at a * cost of 40 micro sec), and then write the framebuffer char (another 40 micro * sec). As the Entry Mode was set to auto-increment the cursor on every write, * we only set the cursor when we must. */ MODULE_EXPORT void sdeclcd_flush(Driver *drvthis) { PrivateData *p = (PrivateData *)drvthis->private_data; int i, c; unsigned char addr; for (i = 0, c = -1; i < SDEC_DISP_H * SDEC_DISP_W; i++) { if (p->framelcd[i] == p->framebuf[i]) continue; /* Found diff: cursor ok ? */ if (c != i) { /* Set cursor address to where the change is */ addr = (i < SDEC_DISP_W) ? (SDEC_ADD_LINE1 + i) : (SDEC_ADD_LINE2 + i - SDEC_DISP_W); /* This costs 40 micro sec. */ sdec_exec(SDEC_FN_DD_ADD | addr, p->bklgt); c = i; } /* Write to LCD, 40 micro sec */ sdec_write(p->framebuf[i], p->bklgt); /* LCD Entry Mode auto increments cursor, so keep track */ if (SDEC_DISP_W - 1 == c++) c = -1; /* Force cursor set */ p->framelcd[i] = p->framebuf[i]; } } /** * API: Print a string on the screen at position (x,y). */ MODULE_EXPORT void sdeclcd_string(Driver *drvthis, int x, int y, char string[]) { PrivateData *p = (PrivateData *) drvthis->private_data; int len; if (y < 1 || y > SDEC_DISP_H || x < 1 || x > SDEC_DISP_W) return; x--; /* convert coordinates to 0-base */ y--; len = strlen(string); memcpy(p->framebuf + x + y * SDEC_DISP_W, string, (x + len > SDEC_DISP_W) ? SDEC_DISP_W - x : len); } /** * API: Print a character on the screen at position (x,y). */ MODULE_EXPORT void sdeclcd_chr(Driver *drvthis, int x, int y, char c) { PrivateData *p = (PrivateData *) drvthis->private_data; if (y < 1 || y > SDEC_DISP_H || x < 1 || x > SDEC_DISP_W) return; p->framebuf[(y - 1) * SDEC_DISP_W + x - 1] = c; } /** * API: Clear the screen. */ MODULE_EXPORT void sdeclcd_clear(Driver *drvthis) { PrivateData *p = (PrivateData *) drvthis->private_data; memset(p->framebuf, ' ', SDEC_DISP_W * SDEC_DISP_H); } /** * API: Return the width of a character in pixels. */ MODULE_EXPORT int sdeclcd_cellwidth(Driver *drvthis) { return SDEC_CELL_W; } /** * API: Return the height of a character in pixels. */ MODULE_EXPORT int sdeclcd_cellheight(Driver *drvthis) { return SDEC_CELL_H; } /** * API: Get total number of custom characters available. */ MODULE_EXPORT int sdeclcd_get_free_char(Driver *drvthis) { return SDEC_NUM_CC; } /** * API: * Implementation routine for Icons. The server asks for an icon by code, and * this routine does the job. Simply updates the frame buffer with a suitable * char based on the icon code requested. As the LCD has its own character * map with latin, chinese, greek and extra symbols, it is only a matter of * mapping to some character on the LCD that (loosely) resembles the * requested code. This implementation does NOT use custom characters. */ MODULE_EXPORT int sdeclcd_icon(Driver *drvthis, int x, int y, int icon_code) { char icon_char; switch (icon_code) { case ICON_BLOCK_FILLED: icon_char = 0xFF; break; case ICON_ARROW_UP: icon_char = '|'; break; case ICON_ARROW_DOWN: icon_char = '|'; break; case ICON_ARROW_LEFT: icon_char = 0x7F; break; case ICON_ARROW_RIGHT: icon_char = 0x7E; break; case ICON_CHECKBOX_OFF: icon_char = 0xDB; break; case ICON_CHECKBOX_ON: icon_char = 0xE8; break; case ICON_CHECKBOX_GRAY: icon_char = 0xA5; break; case ICON_SELECTOR_AT_LEFT: icon_char = 0x7E; break; case ICON_SELECTOR_AT_RIGHT: icon_char = 0x7F; break; case ICON_ELLIPSIS: icon_char = 0xD0; break; default: return -1; } /* Update frame buffer */ sdeclcd_chr(drvthis, x, y, icon_char); return 0; } /** * API: * Implements heart beat. This routine implements a visual cue that the whole * LCDproc server/driver and host are still alive. We could update the frame * buffer, but we chose to go straight to the hardware instead. * We do NOT rely on custom characters for this. Instead, we simply alternate * the frame buffer value with a special char in the corner. This way, we keep * the character under the heart beat somewhat visible. After all, this is only * 20x2. This is called by the server very often, so we pace ourselves, and * beat the heart on our schedule. * OPTIONAL */ MODULE_EXPORT void sdeclcd_heartbeat(Driver *drvthis, int type) { PrivateData *p = drvthis->private_data; if (time(NULL) <= p->hrbt_lasttime) return; /* Move cursor */ sdec_exec(SDEC_FN_DD_ADD | SDEC_HB_ADD, p->bklgt); /* Write */ if (HEARTBEAT_ON == type && HEARTBEAT_OFF == p->hb_stus) sdec_write(SDEC_HB_CHAR, p->bklgt); else sdec_write(p->framelcd[SDEC_HB_LOC], p->bklgt); /* Toggle status */ p->hb_stus = (HEARTBEAT_ON == p->hb_stus) ? HEARTBEAT_OFF : HEARTBEAT_ON; /* Save time */ p->hrbt_lasttime = time(NULL); } /** * API: Draw a horizontal bar to the right. */ MODULE_EXPORT void sdeclcd_hbar(Driver *drvthis, int x, int y, int len, int promille, int options) { PrivateData *p = drvthis->private_data; int i, j; if (hbar != p->ccmode) { for (i = 0; i < SDEC_CELL_W; i++) { for (j = 0; j < SDEC_CELL_H; j++) { /* Set Custom Char Address */ sdec_exec(SDEC_FN_CG_ADD | (i * SDEC_CELL_H + j), p->bklgt); /* Write bitmap at Address */ sdec_write(p->hbar_cg[i * SDEC_CELL_H + j], p->bklgt); } } p->ccmode = hbar; } /* 1 bar is at char 0 or 8, so offset is 7 (avoiding offset of -1) */ lib_hbar_static(drvthis, x, y, len, promille, options, SDEC_CELL_W, 7); } /** * API: Draw a vertical bar bottom-up. */ MODULE_EXPORT void sdeclcd_vbar(Driver *drvthis, int x, int y, int len, int promille, int options) { PrivateData *p = drvthis->private_data; int i, j; if (vbar != p->ccmode) { for (i = 0; i < SDEC_NUM_CC; i++) { for (j = 0; j < SDEC_CELL_H; j++) { /* Set Custom Char Address */ sdec_exec(SDEC_FN_CG_ADD | (i * SDEC_CELL_H + j), p->bklgt); /* Write bitmap at Address */ sdec_write(p->vbar_cg[i * SDEC_CELL_H + j], p->bklgt); } } p->ccmode = vbar; } /* see note in hbar */ lib_vbar_static(drvthis, x, y, len, promille, options, SDEC_CELL_H, 7); } /** * API: Write a big number to the screen. */ MODULE_EXPORT void sdeclcd_num(Driver *drvthis, int x, int num) { PrivateData *p = (PrivateData *)drvthis->private_data; int i, j; /* Big Nums are made of these chunks: */ static char bignum_map[11][2][2] = { {{0, 1}, {2, 3}}, /* 0 */ {{32, 4}, {32, 4}}, /* 1 */ {{5, 6}, {7, 5}}, {{5, 6}, {5, 6}}, {{2, 3}, {32, 4}}, {{7, 5}, {5, 6}}, {{7, 5}, {7, 6}}, {{0, 1}, {32, 4}}, {{7, 6}, {7, 6}}, {{7, 6}, {5, 6}}, /* 9 */ {{0xA1, 32}, {0xDF, 32}} /* : */ }; if (num < 0 || num > 10) return; /* Check custom char in LCD memory */ if (bignum != p->ccmode) { for (i = 0; i < SDEC_NUM_CC; i++) { for (j = 0; j < SDEC_CELL_H; j++) { /* Set Custom Char Address */ sdec_exec(SDEC_FN_CG_ADD | (i * SDEC_CELL_H + j), p->bklgt); /* Write bitmap at Address */ sdec_write(p->bignum_cg[i * SDEC_CELL_H + j], p->bklgt); } } p->ccmode = bignum; } /* Update the frame buffer */ for (i = 0; i < 2; i++) for (j = 0; j < 2; j++) if (bignum_map[num][j][i] != ' ') sdeclcd_chr(drvthis, x + i, j + 1, bignum_map[num][j][i]); } /** * API: * This routine implements the control of the back light. Here, * we set the back light status variable only, and let the next update take * care of the actual light control line. * Try and keep the light off, unless someone pushed a button not too long ago. * The light has a poor half life, so try and preserve it. */ MODULE_EXPORT void sdeclcd_backlight(Driver *drvthis, int level) { PrivateData *p = (PrivateData *) drvthis->private_data; if (time(NULL) - p->bklgt_lasttime >= p->bklgt_timer || 0 == level) p->bklgt = BACKLIGHT_OFF; else p->bklgt = BACKLIGHT_ON; } /** * API: Get key from keypad. This routine implements the polling of the * keypad. This is not part of the SDEC LCD, but is wired through the * parallel port as well. It seems natural to include it in this driver. * Different boxes have different mapping to keys, but all codes are * fortunately unique. As this routine is called periodically by the server, * this is where we added the logic to turn off the backlight after some * time. */ MODULE_EXPORT const char * sdeclcd_get_key(Driver *drvthis) { PrivateData *p = (PrivateData *)drvthis->private_data; unsigned kbd; /* * check backlight timer. It is important to make sure we turn the * light off when there is no activity. With a half life of only * 3,000 hours, we need to preserve it. */ if (time(NULL) - p->bklgt_lasttime >= p->bklgt_timer) p->bklgt = BACKLIGHT_OFF; else p->bklgt = BACKLIGHT_ON; /* read keyboard status */ kbd = port_in(LPT_STATUS) & LPT_STUS_MASK; /* check if keyboard changed */ if (kbd == p->lastkbd) return NULL; /* reset backlight counter */ p->bklgt_lasttime = time(NULL); p->lastkbd = kbd; /*- * Return key text according to status reg mapping: * X-e U:70 R:F8 D:68 L:58 Rel:78 * X-peak U:C8 R:E0 D:C0 L:E8 Rel: 88,80,A8,A0 */ switch (kbd) { case 0x70: case 0xC8: return ("Up"); case 0xF8: case 0xE0: return ("Right"); case 0x68: case 0xC0: return ("Down"); case 0x58: case 0xE8: return ("Left"); case 0x78: /* button Releases */ case 0x88: case 0x80: case 0xA8: case 0xA0: return (NULL); default: /* Code not mapped: */ report(RPT_DEBUG, "LCDd sdeclcd.c/sdeclcd_get_key() %2x unmapped", kbd); return (NULL); } } /** * API: Return info string about this driver. */ MODULE_EXPORT const char * sdeclcd_get_info(Driver *drvthis) { return SDEC_IDENT; }