// vrpn_OmegaTemperature.C // This is a driver for the OmegaTemperature temperature controller. // It was written in April 2014 by Russ Taylor. // INFO about how the device communicates, taken from the user manual: // This information comes from the Bulletin E-90-OCN publication, which is // the manual for the Omega Series CN7200, CN7600, CN7800, CN7500 microprocessor // based temperature process control. It was developed for a CN7800. // It communicates over RS-485 using the MODBUS ASCII/RTU communications // protocol. The driver is written for the ASCII protocol. // A Windows application to test communication with the device can be found // at http://www.omega.com/Software/CN7-a_r.html // A Modbus ASCII frame is as follows (Wikipedia) (big-endian): // Colon ':' character. // 2-character station address // 2-character function code // (0x03 read register, 0x02 read data bits, 0x06 write register, 0x05 write bit) // n-character data + length // 2-character checksum // 2-character CR/LF (0x0d 0x0a) // Modbus application protocol specification: // http://www.modbus.com/docs/Modbus_Application_Protocol_V1_1b.pdf // Modbus over serial line document: // http://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf // libmodbus: libmodbus.org has a Modbus library for various operating // systems. It has ominous statements in the release notes about Windows // support being broken even in the latest version (3.1.1 on 2013-10-06). // The latest stable version is 3.0.5; it does not say that it has broken // support on Windows. It seems to support the serial RTU protocol. // However, the configure script reports that VC++ support has not been // tested in a long time; it can be configured and built under Cygwin. /* XXX The code has not been changed from the Biosciences code yet, only the names have been changed. */ /* Using a standard DB-9 cable (female-female connectors on both ends with straight-through connections from each pin) connect the controller (middle DB-9 connector) to a serial port of your computer. Set the serial port at 115,200 speed, 8 bits, 1 stop bit, NONE parity, and Hardware flow control. The following is the list of text commands supported. NOTE: Each command should follow by \r <CR> code: (The following notes LIE: There is no space before the . or before the C, and sometimes the C is an E. Also, the order is incorrect. The actual order is stage 1, bath 1, external 1, stage 2, bath 2, external 2.) T1<CR> returns temperature readings from STAGE1 sensor: 37 .1 C T2<CR> returns temperature readings from BATH1 sensor: 36 .9 C T5<CR> returns SET temperature: 37 .0 C T3<CR> returns temperature readings from STAGE2 sensor: 37 .1 C T4<CR> returns temperature readings from BATH2 sensor: 36 .9 C T6<CR> returns SET temperature: 37 .0 C CTn<CR> returns readings from n (n=1 - STAGE1, 2 - BATH1, 3 - STAGE2, 4 - BATH2) sensor: 37 .1 C ON<CR> turns temperature control ON OFF<CR> turns temperature control OFF S1 037 0<CR> sets reference temperature for channel I (NOTE: all four digits should be sent to the controller) S2 037 0<CR> sets reference temperature for channel II */ #include <stddef.h> // for size_t #include <stdio.h> // for sprintf, fprintf, stderr, etc #include <string.h> // for strlen, NULL #include "vrpn_BaseClass.h" // for ::vrpn_TEXT_ERROR, etc #include "vrpn_OmegaTemperature.h" #include "vrpn_Serial.h" // for vrpn_write_characters, etc #include "vrpn_Shared.h" // for vrpn_unbuffer, timeval, etc #include "vrpn_MessageMacros.h" // for VRPN_MSG_INFO, VRPN_MSG_WARNING, VRPN_MSG_ERROR VRPN_SUPPRESS_EMPTY_OBJECT_WARNING() #if defined(VRPN_USE_MODBUS) && defined(VRPN_USE_WINSOCK2) #undef VERBOSE // Defines the modes in which the device can find itself. #define STATUS_RESETTING (-1) // Resetting the device #define STATUS_SYNCING (0) // Looking for the first character of report #define STATUS_READING (1) // Looking for the rest of the report #define TIMEOUT_TIME_INTERVAL (2000000L) // max time between reports (usec) // This creates a vrpn_OmegaTemperature. It opens // the serial device using the code in the vrpn_Serial_Analog constructor. // It uses hardware flow control. vrpn_OmegaTemperature::vrpn_OmegaTemperature (const char * name, vrpn_Connection * c, const char * port, float temp1, float temp2, bool control_on): vrpn_Serial_Analog(name, c, port, 115200, 8, vrpn_SER_PARITY_NONE, true), vrpn_Analog_Output(name, c), vrpn_Button_Filter(name, c) { // XXX Make this configurable? int baud = 38400; char parity = 'n'; // XXX What should this be? int stop_bits = 1; d_modbus = modbus_new_rtu(port, baud, parity, 8, stop_bits); // XXX No code below has been changed yet. num_channel = 6; o_num_channel = 3; num_buttons = 1; buttons[0] = control_on; // Fill in the arguments to send to the device at reset time. o_channel[0] = temp1; o_channel[1] = temp2; o_channel[2] = control_on; // Set the mode to reset status = STATUS_RESETTING; // Register to receive the message to request changes and to receive connection // messages. if (d_connection != NULL) { if (register_autodeleted_handler(request_m_id, handle_request_message, this, d_sender_id)) { fprintf(stderr,"vrpn_OmegaTemperature: can't register handler\n"); d_connection = NULL; } if (register_autodeleted_handler(request_channels_m_id, handle_request_channels_message, this, d_sender_id)) { fprintf(stderr,"vrpn_OmegaTemperature: can't register handler\n"); d_connection = NULL; } if (register_autodeleted_handler(d_ping_message_id, handle_connect_message, this, d_sender_id)) { fprintf(stderr,"vrpn_OmegaTemperature: can't register handler\n"); d_connection = NULL; } } else { fprintf(stderr,"vrpn_OmegaTemperature: Can't get connection!\n"); } } // Command format described in document: // S1 037 0<CR> sets reference temperature for channel 1 // (NOTE: all four digits should be sent to the controller) // Actual command format: // S1 0370<CR> Sets reference temperature for channel 1 to 37.0 deg C // S2 0421<CR> Sets reference temperature for channel 2 to 42.1 deg C bool vrpn_OmegaTemperature::set_reference_temperature(unsigned channel, float value) { char command[128]; // Fill in the command with the zero-padded integer output for // above the decimal and then a single value for the first point // past the decimal. int whole = static_cast<int>(value); int dec = static_cast<int>(value*10) - whole*10; sprintf(command, "S%d %03d%d\r", channel+1, whole,dec); // Send the command to the serial port return (vrpn_write_characters(serial_fd, (unsigned char *)(command), strlen(command)) == strlen(command)); } // Command format: // ON<CR> sets control on // OFF<CR> sets control off bool vrpn_OmegaTemperature::set_control_status(bool on) { char command[128]; if (on) { sprintf(command, "ON\r"); } else { sprintf(command, "OFF\r"); } // Send the command to the serial port return (vrpn_write_characters(serial_fd, (unsigned char *)(command), strlen(command)) == strlen(command)); } // Command format: // T1<CR> returns temperature readings from STAGE1 sensor: 37.1C // T2<CR> returns temperature readings from BATH1 sensor: 36.9C // T5<CR> returns SET temperature: 37.0C // T3<CR> returns temperature readings from STAGE2 sensor: 37.1C // T4<CR> returns temperature readings from BATH2 sensor: 36.9C // T6<CR> returns SET temperature: 37.0C // NOTE: Sometimes the C is an E when there is no reading. bool vrpn_OmegaTemperature::request_temperature(unsigned channel) { char command[128]; sprintf(command, "T%d\r", channel+1); #ifdef VERBOSE printf("Sending command: %s", command); #endif // Send the command to the serial port return (vrpn_write_characters(serial_fd, (unsigned char *)(command), strlen(command)) == strlen(command)); } // Convert the four bytes that have been read into a signed integer value. // The format (no quotes) looks like: "- 37.1C\r" or " 000.00E\r". // I don't think that the - means a minus sign, and it has a space // between it and the number. // Returns -1000 if there is an error. float vrpn_OmegaTemperature::convert_bytes_to_reading(const char *buf) { float val; char c; // Skip any leading minus sign. if (*buf == '-') { buf++; } // Read a fractional number. if (sscanf(buf, "%f%c", &val, &c) != 2) { return -1000; } // See if we get and E or C after the number, // or (since E can be part of a floating-point // number) if we get \r. if ( (c != 'E') && (c != 'C') && (c != '\r') ) { return -1000; } return val; } int vrpn_OmegaTemperature::reset(void) { //----------------------------------------------------------------------- // Sleep less thana second and then drain the input buffer to make sure we start // with a fresh slate. vrpn_SleepMsecs(200); vrpn_flush_input_buffer(serial_fd); //----------------------------------------------------------------------- // Set the temperatures for channel 1 and 2 and then set the temperature // control to be on or off depending on what we've been asked to do. if (!set_reference_temperature(0, static_cast<float>(o_channel[0]))) { fprintf(stderr,"vrpn_OmegaTemperature::reset(): Cannot send set ref temp 0, trying again\n"); return -1; } if (!set_reference_temperature(1, static_cast<float>(o_channel[1]))) { fprintf(stderr,"vrpn_OmegaTemperature::reset(): Cannot send set ref temp 1, trying again\n"); return -1; } if (!set_control_status(o_channel[0] != 0)) { fprintf(stderr,"vrpn_OmegaTemperature::reset(): Cannot send set control status, trying again\n"); return -1; } //----------------------------------------------------------------------- // Send the command to request input from the first channel, and set up // the finite-state machine so we know which thing to request next. d_next_channel_to_read = 0; if (!request_temperature(d_next_channel_to_read)) { fprintf(stderr,"vrpn_OmegaTemperature::reset(): Cannot request temperature, trying again\n"); return -1; } // We're now waiting for any responses from devices status = STATUS_SYNCING; VRPN_MSG_WARNING("reset complete (this is normal)"); vrpn_gettimeofday(×tamp, NULL); // Set watchdog now return 0; } // This function will read characters until it has a full report, then // put that report into analog fields and call the report methods on these. // The time stored is that of the first character received as part of the // report. int vrpn_OmegaTemperature::get_report(void) { int ret; // Return value from function call to be checked //-------------------------------------------------------------------- // If we're SYNCing, then the next character we get should be the start // of a report. If we recognize it, go into READing mode and tell how // many characters we expect total. If we don't recognize it, then we // must have misinterpreted a command or something; reset // and start over //-------------------------------------------------------------------- if (status == STATUS_SYNCING) { // Try to get a character. If none, just return. if (vrpn_read_available_characters(serial_fd, (unsigned char *)(d_buffer), 1) != 1) { return 0; } // Got the first character of a report -- go into READING mode // and record that we got one character at this time. Clear the // rest of the buffer to 0's so that we won't be looking at old // data when we parse. // The time stored here is as close as possible to when the // report was generated. d_bufcount = 1; vrpn_gettimeofday(×tamp, NULL); status = STATUS_READING; size_t i; for (i = 1; i < sizeof(d_buffer); i++) { d_buffer[i] = 0; } #ifdef VERBOSE printf("... Got the 1st char\n"); #endif } //-------------------------------------------------------------------- // Read as many bytes of this report as we can, storing them // in the buffer. //-------------------------------------------------------------------- while ( 1 == (ret = vrpn_read_available_characters(serial_fd, (unsigned char *)(&d_buffer[d_bufcount]), 1))) { d_bufcount++; } if (ret == -1) { VRPN_MSG_ERROR("Error reading"); status = STATUS_RESETTING; return 0; } #ifdef VERBOSE if (ret != 0) printf("... got %d total characters\n", d_bufcount); #endif if (d_buffer[d_bufcount-1] != '\r') { // Not done -- go back for more return 0; } //-------------------------------------------------------------------- // We now have enough characters to make a full report. Check to make // sure that its format matches what we expect. If it does, the next // section will parse it. // Store the report into the appropriate analog channel. //-------------------------------------------------------------------- #ifdef VERBOSE printf(" Complete report: \n%s\n",d_buffer); #endif float value = convert_bytes_to_reading(d_buffer); if (value == -1000) { char msg[256]; sprintf(msg,"Invalid report, channel %d, resetting", d_next_channel_to_read); VRPN_MSG_ERROR(msg); status = STATUS_RESETTING; } channel[d_next_channel_to_read] = value; #ifdef VERBOSE printf("got a complete report (%d chars)!\n", d_bufcount); #endif //-------------------------------------------------------------------- // Request a reading from the next channe. //-------------------------------------------------------------------- d_next_channel_to_read = (d_next_channel_to_read + 1) % 6; if (!request_temperature(d_next_channel_to_read)) { char msg[256]; sprintf(msg,"Can't request reading, channel %d, resetting", d_next_channel_to_read); VRPN_MSG_ERROR(msg); status = STATUS_RESETTING; } //-------------------------------------------------------------------- // Done with the decoding, send the reports and go back to syncing //-------------------------------------------------------------------- report_changes(); status = STATUS_SYNCING; d_bufcount = 0; return 1; } bool vrpn_OmegaTemperature::set_specified_channel(unsigned channel, vrpn_float64 value) { // XXX Check return status of the set commands? switch (channel) { case 0: // Reference temperature for channels 1 and 2 case 1: // Reference temperature for channels 1 and 2 set_reference_temperature(channel, static_cast<float>(value)); o_channel[channel] = value; break; case 2: // Turn on temperature control if this is nonzero. o_channel[2] = value; buttons[0] = ( value != 0 ); set_control_status( value != 0); break; default: return false; } return true; } int vrpn_OmegaTemperature::handle_request_message(void *userdata, vrpn_HANDLERPARAM p) { const char *bufptr = p.buffer; vrpn_int32 chan_num; vrpn_int32 pad; vrpn_float64 value; vrpn_OmegaTemperature *me = (vrpn_OmegaTemperature *)userdata; // Read the parameters from the buffer vrpn_unbuffer(&bufptr, &chan_num); vrpn_unbuffer(&bufptr, &pad); vrpn_unbuffer(&bufptr, &value); // Set the appropriate value, if the channel number is in the // range of the ones we have. if ( (chan_num < 0) || (chan_num >= me->o_num_channel) ) { char msg[1024]; sprintf(msg,"vrpn_OmegaTemperature::handle_request_message(): Index out of bounds (%d of %d), value %lg\n", chan_num, me->num_channel, value); me->send_text_message(msg, me->timestamp, vrpn_TEXT_ERROR); return 0; } me->set_specified_channel(chan_num, value); return 0; } int vrpn_OmegaTemperature::handle_request_channels_message(void* userdata, vrpn_HANDLERPARAM p) { int i; const char* bufptr = p.buffer; vrpn_int32 num; vrpn_int32 pad; vrpn_OmegaTemperature* me = (vrpn_OmegaTemperature *)userdata; // Read the values from the buffer vrpn_unbuffer(&bufptr, &num); vrpn_unbuffer(&bufptr, &pad); if (num > me->o_num_channel) { char msg[1024]; sprintf(msg,"vrpn_OmegaTemperature::handle_request_channels_message(): Index out of bounds (%d of %d), clipping\n", num, me->o_num_channel); me->send_text_message(msg, me->timestamp, vrpn_TEXT_ERROR); num = me->o_num_channel; } for (i = 0; i < num; i++) { vrpn_unbuffer(&bufptr, &(me->o_channel[i])); me->set_specified_channel(i, me->o_channel[i]); } return 0; } /** When we get a connection request from a remote object, send our state so they will know it to start with. */ int vrpn_OmegaTemperature::handle_connect_message(void *userdata, vrpn_HANDLERPARAM) { vrpn_OmegaTemperature *me = (vrpn_OmegaTemperature *)userdata; me->report(vrpn_CONNECTION_RELIABLE); return 0; } void vrpn_OmegaTemperature::report_changes(vrpn_uint32 class_of_service) { vrpn_Analog::timestamp = timestamp; vrpn_Analog::report_changes(class_of_service); vrpn_Button::report_changes(); } void vrpn_OmegaTemperature::report(vrpn_uint32 class_of_service) { vrpn_Analog::timestamp = timestamp; vrpn_Analog::report(class_of_service); vrpn_Button::report_changes(); } /** This routine is called each time through the server's main loop. It will take a course of action depending on the current status of the device, either trying to reset it or trying to get a reading from it. It will try to reset the device if no data has come from it for a couple of seconds */ void vrpn_OmegaTemperature::mainloop() { char errmsg[256]; server_mainloop(); switch(status) { case STATUS_RESETTING: reset(); break; case STATUS_SYNCING: case STATUS_READING: { // It turns out to be important to get the report before checking // to see if it has been too long since the last report. This is // because there is the possibility that some other device running // in the same server may have taken a long time on its last pass // through mainloop(). Trackers that are resetting do this. When // this happens, you can get an infinite loop -- where one tracker // resets and causes the other to timeout, and then it returns the // favor. By checking for the report here, we reset the timestamp // if there is a report ready (ie, if THIS device is still operating). while (get_report()) {}; // Keep getting reports so long as there are more struct timeval current_time; vrpn_gettimeofday(¤t_time, NULL); if ( vrpn_TimevalDuration(current_time,timestamp) > TIMEOUT_TIME_INTERVAL) { sprintf(errmsg,"Timeout... current_time=%ld:%ld, timestamp=%ld:%ld", current_time.tv_sec, static_cast<long>(current_time.tv_usec), timestamp.tv_sec, static_cast<long>(timestamp.tv_usec)); VRPN_MSG_ERROR(errmsg); status = STATUS_RESETTING; } } break; default: VRPN_MSG_ERROR("Unknown mode (internal error)"); break; } } #endif