471 lines
13 KiB
C
471 lines
13 KiB
C
/*
|
|
@file sct_protek.c
|
|
@brief Protocol handler for Protek 608 multimeter
|
|
|
|
Reads the display bitmap and generates an ASCII string similar to the
|
|
following,
|
|
SD3:0.|SD4:1|SD5: |DUTY2:0|HOLD2:0|RANGE2:0|ZD2:0|DC2:0|SIGN2:0|AC2:0|CONT2:0|PW1:0|X1:0|B1:1|B0:1|B16:0|B8:0|B4:0|B2:0|DC1:1|SIGN1:1|AC1:0|LOBAT:0|MD5:0.|MD4:0|MD3:0|X2:0|X3:0|PLUS:0|MINUS:0|RS232C:1|AUTOOFF:0|X4:0|PULSE:0|STORE:0|AVG:0|MINUSPEAK:0|MIN:0|RECALL:0|REL:0|PLUSPEAK:0|MAX:0|GO/NG:0|REF:0|MINUSPER:0|PLUSPER:0|MD2:0|MD1:2|s1:0|DEGF1:0|Hz1:0|X5:0|F1:0|A1:0|OHM1:0|X6:0|DEGC1:0|S1:0|V1:1|k1:0|n1:0|m1:0|u1:0|M1:0|B8K:0|B16K:0|X7:0|X8:0|B4K:0|B2K:0|B1K:0|B512:0|B32:0|B64:0|B128:0|B256:0|M2:1|G2:0|m2:0|PERCENT2:0|k2:0|OHM2:1|V2:0|dBm2:0|X9:0|Hz2:0|A2:0|DEGK2:0|SD1:0|SD2:5|ROTSWITCH:0
|
|
|
|
NOTE: Multimeter must be connected via a moxa box
|
|
|
|
Author: Ferdi Franceschini (ffr@ansto.gov.au)
|
|
TODO The letters I, S, and O are currently being returned as 1, 5, and 0.
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <ascon.h>
|
|
#include <ascon.i>
|
|
#include <dynstring.h>
|
|
|
|
#define ERRLEN 128
|
|
#define CMDLEN 12
|
|
#define LASTBYTENUM 42
|
|
|
|
/* @brief Returns '1' if bit is set otherwise '0' */
|
|
#define GETBIT(MASK, BYTE) (MASK & BYTE ? '1' : '0')
|
|
|
|
/* @brief Interprets BYTE and returns a string of fields and values, eg "F1:v1|F2:v2" */
|
|
#define SETFIELD(L1, L2, L3, L4, BYTE) (sprintf(field, "%s:%c|%s:%c|%s:%c|%s:%c", L1, GETBIT(0x8, BYTE), L2, GETBIT(0x4, BYTE), L3, GETBIT(0x2, BYTE), L4, GETBIT(0x1, BYTE)))
|
|
|
|
struct protekData {
|
|
unsigned char defa, xcgb;
|
|
int first;
|
|
};
|
|
|
|
void protKill(void *private)
|
|
{
|
|
struct protekData *data = private;
|
|
free(data);
|
|
}
|
|
|
|
/*
|
|
* @brief Decodes a seven segment display bit pattern into an ASCII character
|
|
* The point bit, x in xCGB, is ignored in an 8 segment bit pattern.
|
|
*
|
|
* @param DEFA bit pattern for the left half of a seven segment display
|
|
* @param xCGB bit pattern fot the right half of the display, x is the point bit for an 8 segment pattern.
|
|
* @return ASCII numeral '0' - '9', ' ' for a blank character, 'L' (eg in OL)
|
|
* 'A', 'd', 'E', 'F', 'h', 'I', 'L', 'n', 'P', 'r', 'S', 't', or 'U' for Unknown
|
|
*
|
|
*/
|
|
unsigned char segChar(unsigned char DEFA, unsigned char xCGB)
|
|
{
|
|
unsigned char num, DEFA0CGB;
|
|
|
|
DEFA0CGB = (DEFA << 4) | (0x7 & xCGB);
|
|
switch (DEFA0CGB) {
|
|
case 0x0:
|
|
num = ' ';
|
|
break;
|
|
case 0xF5:
|
|
//TODO O or zero
|
|
num = '0';
|
|
break;
|
|
case 0x05:
|
|
//TODO I or 1
|
|
num = '1';
|
|
break;
|
|
case 0xD3:
|
|
num = '2';
|
|
break;
|
|
case 0x97:
|
|
num = '3';
|
|
break;
|
|
case 0x27:
|
|
num = '4';
|
|
break;
|
|
case 0xB6:
|
|
//TODO 5 or S
|
|
num = '5';
|
|
break;
|
|
case 0xF6:
|
|
num = '6';
|
|
break;
|
|
case 0x35:
|
|
num = '7';
|
|
break;
|
|
case 0xF7:
|
|
num = '8';
|
|
break;
|
|
case 0xB7:
|
|
num = '9';
|
|
break;
|
|
case 0x77:
|
|
num = 'A';
|
|
break;
|
|
case 0xC2:
|
|
num = 'c';
|
|
break;
|
|
case 0xD7:
|
|
num = 'd';
|
|
break;
|
|
case 0xF4:
|
|
num = 'E';
|
|
break;
|
|
case 0x72:
|
|
num = 'F';
|
|
break;
|
|
case 0x66:
|
|
num = 'h';
|
|
break;
|
|
case 0xE0:
|
|
num = 'L';
|
|
break;
|
|
case 0x46:
|
|
num = 'n';
|
|
break;
|
|
case 0x73:
|
|
num = 'P';
|
|
break;
|
|
case 0x42:
|
|
num = 'r';
|
|
break;
|
|
case 0xE2:
|
|
num = 't';
|
|
break;
|
|
default:
|
|
num = 'U';
|
|
break;
|
|
}
|
|
return num;
|
|
}
|
|
/*
|
|
* @brief Translates incoming bytes and generates the corresponding field of the status string
|
|
*
|
|
* @param byte, a character read from the protek multimeter
|
|
* @param byteNum, the position of the character in the protek protocol
|
|
* @param *str, translation of the byte into a "field:value" string where "value" is either an ASCII
|
|
* character or a boolean represented as a '1' or '0' character and "field" is a label.
|
|
* @return -1=ERROR, 0=NOTDONE, 1=DONE and str is set to translated bits
|
|
*
|
|
* The "switch" statement corresponds to the protocol table in the Protek 608 manual pg44
|
|
* with the labels for the bits written in reverse order so that they match
|
|
* the bit patterns we read from the multimeter
|
|
*/
|
|
int translateByte(struct protekData *data, char byte, int byteNum, char *str)
|
|
{
|
|
unsigned char num;
|
|
char field[64]="";
|
|
int gotSegChar=0;
|
|
|
|
switch (byteNum) {
|
|
case 1: /* SD3 p7,C,G,B */
|
|
data->xcgb = byte;
|
|
break;
|
|
case 2: /* SD3 D,E,F,A */
|
|
data->defa= byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "SD3");
|
|
break;
|
|
case 3: /* SD4 p8,C,G,B */
|
|
data->xcgb = byte;
|
|
break;
|
|
case 4: /* SD4 D,E,F,A */
|
|
data->defa= byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "SD4");
|
|
break;
|
|
case 5: /* SD5 p9,C,G,B */
|
|
data->xcgb = byte;
|
|
break;
|
|
case 6: /* SD5 D,E,F,A */
|
|
data->defa= byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "SD5");
|
|
break;
|
|
case 7: /* bits 25-32 SUB UNIT DUTY2,HOLD2,RANGE2,ZD2 (ZENERDIODE) */
|
|
SETFIELD("DUTY2", "HOLD2", "RANGE2", "ZD2", byte);
|
|
break;
|
|
case 8: /* bits 25-32 SUB UNIT DC2,SIGN2,AC2,CONT2 */
|
|
SETFIELD("DC2", "SIGN2", "AC2", "CONT2", byte);
|
|
break;
|
|
case 9: /* BAR GRAPH PW1,x,B1,B0 */
|
|
SETFIELD("PW1", "X1", "B1", "B0", byte);
|
|
break;
|
|
case 10: /* BAR GRAPH B16,B8,B4,B2 */
|
|
SETFIELD("B16", "B8", "B4", "B2", byte);
|
|
break;
|
|
case 11: /* DC1,SIGN1,AC1,LOBAT */
|
|
SETFIELD("DC1", "SIGN1", "AC1", "LOBAT", byte);
|
|
break;
|
|
case 12: /* MD5 D,E,F,A */
|
|
data->defa = byte;
|
|
break;
|
|
case 13: /* MD5 p4,C,G,B */
|
|
data->xcgb = byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "MD5");
|
|
break;
|
|
case 14: /* MD4 D,E,F,A */
|
|
data->defa = byte;
|
|
break;
|
|
case 15: /* MD4 p3,C,G,B */
|
|
data->xcgb = byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "MD4");
|
|
break;
|
|
case 16: /* MD3 D,E,F,A */
|
|
data->defa = byte;
|
|
break;
|
|
case 17: /* MD3 p2,C,G,B */
|
|
data->xcgb = byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "MD3");
|
|
break;
|
|
case 18: /* x,x,+,- */
|
|
SETFIELD("X2", "X3", "PLUS", "MINUS", byte);
|
|
break;
|
|
case 19: /* RS232C,AUTOOFF,x,PULSE */
|
|
SETFIELD("RS232C", "AUTOOFF", "X4", "PULSE", byte);
|
|
break;
|
|
case 20: /* STORE,AVG,MINUSPEAK,MIN */
|
|
SETFIELD("STORE", "AVG", "MINUSPEAK", "MIN", byte);
|
|
break;
|
|
case 21: /* RECALL,REL,PLUSPEAK,MAX */
|
|
SETFIELD("RECALL", "REL", "PLUSPEAK", "MAX", byte);
|
|
break;
|
|
case 22: /* GO/NG,REF,MINUSPER,PLUSPER */
|
|
SETFIELD("GO/NG", "REF", "MINUSPER", "PLUSPER", byte);
|
|
break;
|
|
case 23: /* MD2 D,E,F,A */
|
|
data->defa = byte;
|
|
break;
|
|
case 24: /* MD2 p1,C,G,B */
|
|
data->xcgb = byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "MD2");
|
|
break;
|
|
case 25: /* MD1 D,E,F,A*/
|
|
data->defa = byte;
|
|
break;
|
|
case 26: /* MD1 x,C,G,B */
|
|
data->xcgb = byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "MD1");
|
|
break;
|
|
case 27: /* s1,DEGF1,Hz1,x */
|
|
SETFIELD("s1", "DEGF1", "Hz1", "X5", byte);
|
|
break;
|
|
case 28: /* F1,A1,OHM1,x */
|
|
SETFIELD("F1", "A1", "OHM1", "X6", byte);
|
|
break;
|
|
case 29: /* DEGC1,S1,V1,k1 */
|
|
SETFIELD("DEGC1", "S1", "V1", "k1", byte);
|
|
break;
|
|
case 30: /* n1,m1,u1,M1 */
|
|
SETFIELD("n1", "m1", "u1", "M1", byte);
|
|
break;
|
|
case 31: /* BAR GRAPH B8K,B16K,x,x */
|
|
SETFIELD("B8K", "B16K", "X7", "X8", byte);
|
|
break;
|
|
case 32: /* BAR GRAPH B4K,B2K,B1K,B512 */
|
|
SETFIELD("B4K", "B2K", "B1K", "B512", byte);
|
|
break;
|
|
case 33: /* BAR GRAPH B32,B64,B128,B256 */
|
|
SETFIELD("B32", "B64", "B128", "B256", byte);
|
|
break;
|
|
case 34: /* BAR GRAPH M2,G2,m2,PERCENT2 */
|
|
SETFIELD("M2", "G2", "m2", "PERCENT2", byte);
|
|
break;
|
|
case 35: /* bits 133-143 SUB UNIT k2,OHM2,V2,dBm2 */
|
|
SETFIELD("k2", "OHM2", "V2", "dBm2", byte);
|
|
break;
|
|
case 36: /* bits 133-143 SUB UNIT x,Hz2,A2,DEGK2 */
|
|
SETFIELD("X9", "Hz2", "A2", "DEGK2", byte);
|
|
break;
|
|
case 37: /* SD1 x,C,G,B */
|
|
data->xcgb = byte;
|
|
break;
|
|
case 38: /* SD1 D,E,F,A */
|
|
data->defa= byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "SD1");
|
|
break;
|
|
case 39: /* SD2 p6,C,G,B */
|
|
data->xcgb = byte;
|
|
break;
|
|
case 40: /* SD2 D,E,F,A */
|
|
data->defa= byte;
|
|
gotSegChar=1;
|
|
strcpy(field, "SD2");
|
|
break;
|
|
case 41:
|
|
/* Last byte is undocumented but it seems to be the rotary switch position
|
|
Positions range from 0 (V) to 8 (A).
|
|
*/
|
|
sprintf(field, "ROTSWITCH:%X", byte);
|
|
break;
|
|
default:
|
|
return -1;
|
|
break;
|
|
}
|
|
|
|
if (field[0] != '\0') {
|
|
if (gotSegChar) {
|
|
num = segChar(data->defa, data->xcgb);
|
|
if (num == 'U') {
|
|
return -1;
|
|
}
|
|
if ((010 & data->xcgb) == 010) {
|
|
sprintf(str, "%s:%c.", field, num);
|
|
} else {
|
|
sprintf(str, "%s:%c", field, num);
|
|
}
|
|
} else {
|
|
strcpy(str,field);
|
|
}
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* @brief Constructs a status string from the display map sent by the Protek
|
|
*
|
|
* The multimeter sends two data packets per second bounded by '['(0x5B) and ']'(0x5D)
|
|
* NOTE: Sometimes it sends 0x1D instead of 0x5D at the end of a packet
|
|
*/
|
|
int Protek608Reading(Ascon *a)
|
|
{
|
|
int ret, DS_AllocFailed = 0, transByteStatus = 0, transByteFailed = 0;
|
|
int errNum = 0;
|
|
char chr=0, field[64], errMsg[ERRLEN];
|
|
struct protekData *data = a->private;
|
|
|
|
/* a->readState == 0 means we haven't yet seen the start of a packet (ie the '[')
|
|
* a->readState > 0 is the current byte number
|
|
*/
|
|
while ( ( ret = AsconReadChar(a->fd, &chr) ) > 0) {
|
|
a->start = DoubleTime();
|
|
if (a->readState) {
|
|
if (a->readState >= LASTBYTENUM) {
|
|
a->readState = 0;
|
|
a->state = AsconReadDone;
|
|
data->first = 1;
|
|
if (0 == DynStringConcatChar(a->rdBuffer, '\0')) {
|
|
strcpy(errMsg, "PROTEK608: DynStringConcat or DynStringConcatChar failed:");
|
|
errNum = ENOMEM;
|
|
DS_AllocFailed = 1;
|
|
break;
|
|
}
|
|
if (chr != ']' && chr != 0x1D) {
|
|
snprintf(errMsg, ERRLEN, "PROTEK608: Unexpected value %X for last byte:", chr);
|
|
AsconError(a, errMsg, 0);
|
|
}
|
|
return 0;
|
|
}
|
|
transByteStatus = translateByte(a->private, chr, (a->readState)++, field);
|
|
if (transByteStatus == 1) {
|
|
if (data->first) {
|
|
if ( 0== DynStringConcat(a->rdBuffer, field) ) {
|
|
strcpy(errMsg, "PROTEK608: DynStringConcat or DynStringConcatChar failed:");
|
|
errNum = ENOMEM;
|
|
DS_AllocFailed = 1;
|
|
break;
|
|
}
|
|
data->first=0;
|
|
} else {
|
|
if ( 0 == DynStringConcatChar(a->rdBuffer, '|') ) {
|
|
strcpy(errMsg, "PROTEK608: DynStringConcat or DynStringConcatChar failed:");
|
|
errNum = ENOMEM;
|
|
DS_AllocFailed = 1;
|
|
break;
|
|
}
|
|
if ( 0 == DynStringConcat(a->rdBuffer, field) ) {
|
|
strcpy(errMsg, "PROTEK608: DynStringConcat or DynStringConcatChar failed:");
|
|
errNum = ENOMEM;
|
|
DS_AllocFailed = 1;
|
|
break;
|
|
}
|
|
}
|
|
} else if (transByteStatus == -1) {
|
|
snprintf(errMsg, ERRLEN, "PROTEK608: Failed to translate %X in byte number %d:", chr, a->readState-1);
|
|
errNum = 0;
|
|
transByteFailed = 1;
|
|
break;
|
|
}
|
|
} else if (chr == '[') {
|
|
a->readState = 1;
|
|
}
|
|
}
|
|
|
|
if (ret < 0) {
|
|
AsconError(a, "PROTEK608: AsconReadChar failed:", errno);
|
|
return 0;
|
|
}
|
|
if (DS_AllocFailed || transByteFailed) {
|
|
a->state = AsconReadDone;
|
|
a->readState = 0;
|
|
data->first = 1;
|
|
AsconError(a, errMsg, errNum);
|
|
return 0;
|
|
}
|
|
if (a->timeout > 0) {
|
|
if (DoubleTime() - a->start > a->timeout) {
|
|
AsconError(a, "PROTEK608: read timeout", 0);
|
|
/* a->state = AsconTimeout; */
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* @brief Protocol handler for Protek 608 multimeter
|
|
*
|
|
* It overrides the AsconWriteStart and AsconReading states of the standard handler.
|
|
* Available commands,
|
|
* "STATE": Requests a state report
|
|
*
|
|
* NOTE: The multimeter is always sending a display map at 0.5 second intervals, the "STATE" command
|
|
* simply forces a transition from the AsconWriteStart state to the AsconReadStart state.
|
|
*/
|
|
int Protek608ProtHandler(Ascon *a)
|
|
{
|
|
char *wrBuffArr, errMsg[ERRLEN];
|
|
switch(a->state) {
|
|
case AsconWriteStart:
|
|
wrBuffArr = GetCharArray(a->wrBuffer);
|
|
if (strncasecmp(wrBuffArr, "STATE", CMDLEN) != 0) {
|
|
a->state = AsconIdle;
|
|
snprintf(errMsg, ERRLEN, "PROTEK608: Unknown command %s", wrBuffArr);
|
|
AsconError(a, errMsg, 0);
|
|
return 0;
|
|
}
|
|
a->wrPos = 0;
|
|
AsconReadGarbage(a->fd);
|
|
a->state = AsconReadStart;
|
|
return 1;
|
|
break;
|
|
case AsconReading:
|
|
return Protek608Reading(a);
|
|
break;
|
|
default:
|
|
return AsconStdHandler(a);
|
|
}
|
|
}
|
|
|
|
int AsconProtek608Init(Ascon *a, SConnection *con, int argc, char *argv[])
|
|
{
|
|
/* greater than one when reading data */
|
|
a->readState = 0;
|
|
struct protekData *data;
|
|
a->private = data = (struct protekData *) malloc(sizeof(struct protekData));
|
|
a->killPrivate = protKill;
|
|
data->defa = 0;
|
|
data->xcgb = 0;
|
|
data->first = 1;
|
|
return AsconStdInit(a,con, argc, argv);
|
|
}
|
|
|
|
void AddProtek608Protocol()
|
|
{
|
|
AsconProtocol *prot = NULL;
|
|
|
|
prot = calloc(sizeof(AsconProtocol), 1);
|
|
prot->name = strdup("protek608");
|
|
prot->init = AsconProtek608Init;
|
|
prot->handler = Protek608ProtHandler;
|
|
AsconInsertProtocol(prot);
|
|
}
|