/* @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 #include #include #include #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 1; } 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 1; } if (DS_AllocFailed || transByteFailed) { a->state = AsconReadDone; a->readState = 0; data->first = 1; AsconError(a, errMsg, errNum); return 1; } if (a->timeout > 0) { if (DoubleTime() - a->start > a->timeout) { AsconError(a, "PROTEK608: read timeout", 0); a->state = AsconTimeout; } } return 1; } /* * @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); }