// vrpn_CerealBox.C // This is a driver for the BG Systems CerealBox controller. // This box is a serial-line device that allows the user to connect // analog inputs, digital inputs, digital outputs, and digital // encoders and read from them over RS-232. You can find out more // at www.bgsystems.com. This code is written for version 3.07 of // the EEPROM code. // This code is based on their driver code, which was posted // on their web site as of the summer of 1999. This code reads the // characters as they arrive, rather than waiting "long enough" for // them to get here; this should allow the CerealBox to be used at // the same time as a tracking device without slowing the tracker // down. #include <stdio.h> // for fprintf, stderr, perror, etc #include <string.h> // for NULL, strlen, strncmp #include "vrpn_BaseClass.h" // for ::vrpn_TEXT_ERROR #include "vrpn_CerealBox.h" #include "vrpn_Serial.h" #include "vrpn_Shared.h" // for timeval, vrpn_gettimeofday #undef VERBOSE static const char offset = 0x21; // Offset added to some characters to avoid ctl chars static const double REV_PER_TICK = 1.0/4096; // How many revolutions per encoder tick? // Defines the modes in which the box can find itself. #define STATUS_RESETTING (-1) // Resetting the box #define STATUS_SYNCING (0) // Looking for the first character of report #define STATUS_READING (1) // Looking for the rest of the report #define MAX_TIME_INTERVAL (2000000) // max time between reports (usec) // This creates a vrpn_CerealBox and sets it to reset mode. It opens // the serial device using the code in the vrpn_Serial_Analog constructor. // The box seems to autodetect the baud rate when the "T" command is sent // to it. vrpn_CerealBox::vrpn_CerealBox (const char * name, vrpn_Connection * c, const char * port, int baud, const int numbuttons, const int numchannels, const int numencoders): vrpn_Serial_Analog(name, c, port, baud), vrpn_Button_Filter(name, c), vrpn_Dial(name, c), _numbuttons(numbuttons), _numchannels(numchannels), _numencoders(numencoders) { // Verify the validity of the parameters if (_numbuttons > 24) { fprintf(stderr,"vrpn_CerealBox: Can only support 24 buttons, not %d\n", _numbuttons); _numbuttons = 24; } if (_numchannels > 8) { fprintf(stderr,"vrpn_CerealBox: Can only support 8 analogs, not %d\n", _numchannels); _numchannels = 8; } if (_numencoders > 8) { fprintf(stderr,"vrpn_CerealBox: Can only support 8 encoders, not %d\n", _numencoders); _numencoders = 8; } // Set the parameters in the parent classes vrpn_Button::num_buttons = _numbuttons; vrpn_Analog::num_channel = _numchannels; vrpn_Dial::num_dials = _numencoders; // Set the status of the buttons, analogs and encoders to 0 to start clear_values(); // Set the mode to reset _status = STATUS_RESETTING; } void vrpn_CerealBox::clear_values(void) { int i; for (i = 0; i < _numbuttons; i++) { vrpn_Button::buttons[i] = vrpn_Button::lastbuttons[i] = 0; } for (i = 0; i < _numchannels; i++) { vrpn_Analog::channel[i] = vrpn_Analog::last[i] = 0; } for (i = 0; i < _numencoders; i++) { vrpn_Dial::dials[i] = 0.0; } } // This routine will reset the CerealBox, asking it to send the requested number // of analogs, buttons and encoders. It verifies that it can communicate with the // device and checks the version of the EPROMs in the device is 3.07. int vrpn_CerealBox::reset(void) { struct timeval timeout; unsigned char inbuf[45]; const char *Cpy = "Copyright (c), BG Systems"; int major, minor, bug; // Version of the EEPROM unsigned char reset_str[32]; // Reset string sent to box int ret; //----------------------------------------------------------------------- // Set the values back to zero for all buttons, analogs and encoders clear_values(); //----------------------------------------------------------------------- // Check that the box exists and has the correct EEPROM version. The // "T" command to the box should cause the 44-byte EEPROM string to be // returned. This string defines the version and type of the box. // Give it a reasonable amount of time to finish (2 seconds), then timeout vrpn_flush_input_buffer(serial_fd); vrpn_write_characters(serial_fd, (const unsigned char *)"T", 1); timeout.tv_sec = 2; timeout.tv_usec = 0; ret = vrpn_read_available_characters(serial_fd, inbuf, 44, &timeout); inbuf[44] = 0; // Make sure string is NULL-terminated if (ret < 0) { perror("vrpn_CerealBox: Error reading from box\n"); return -1; } if (ret == 0) { fprintf(stderr,"vrpn_CerealBox: No response from box\n"); return -1; } if (ret != 44) { fprintf(stderr,"vrpn_CerealBox: Got %d of 44 expected characters\n",ret); return -1; } // Parse the copyright string for the version and other information // Code here is similar to check_rev() function in the BG example code. if (strncmp((char *)inbuf, Cpy, strlen(Cpy))) { fprintf(stderr,"vrpn_CerealBox: Copyright string mismatch: %s\n",inbuf); return -1; } major = inbuf[38] - '0'; minor = inbuf[40] - '0'; bug = inbuf[41] - '0'; if ( (3 != major) || (0 != minor) || (7 != bug) ) { fprintf(stderr, "vrpn_CerealBox: Bad EEPROM version (want 3.07, got %d.%d%d)\n", major, minor, bug); return -1; } printf("vrpn_CerealBox: Box of type %c found\n",inbuf[42]); //----------------------------------------------------------------------- // Compute the proper string to initialize the device based on how many // of each type of input/output that is selected. This follows init_cereal() // in BG example code. { int i; char ana1_4 = 0; // Bits 1-4 do analog channels 1-4 char ana5_8 = 0; // Bits 1-4 do analog channels 5-8 char dig1_3 = 0; // Bits 1-3 enable groups of 8 inputs char enc1_4 = 0; // Bits 1-4 enable encoders 1-4 char enc5_8 = 0; // Bits 1-4 enable encoders 5-8 // Figure out which analog channels to use and set them for (i = 0; i < 4; i++) { if (i < _numchannels) { ana1_4 |= (1<<i); } } for (i = 0; i < 4; i++) { if (i+4 < _numchannels) { ana5_8 |= (1<<i); } } // Figure out which banks of digital inputs to use and set them for (i = 0; i < _numbuttons; i++) { dig1_3 |= (1 << (i/8)); } // Figure out which encoder channels to use and set them for (i = 0; i < 4; i++) { if (i < _numencoders) { enc1_4 |= (1<<i); } } for (i = 0; i < 4; i++) { if (i+4 < _numencoders) { enc5_8 |= (1<<i); } } reset_str[0] = 'c'; reset_str[1] = (unsigned char)(ana1_4 + offset); // Hope we don't need to set baud rate reset_str[2] = (unsigned char)((ana5_8 | (dig1_3 << 4)) + offset); reset_str[3] = (unsigned char)(0 + offset); reset_str[4] = (unsigned char)(0 + offset); reset_str[5] = (unsigned char)(enc1_4 + offset); reset_str[6] = (unsigned char)(enc1_4 + offset); // Set encoders 1-4 for incremental reset_str[7] = (unsigned char)(enc5_8 + offset); reset_str[8] = (unsigned char)(enc5_8 + offset); // Set encoders 5-8 for incremental reset_str[9] = '\n'; reset_str[10] = 0; } // Send the string and then wait for an acknowledgement from the box. vrpn_write_characters(serial_fd, reset_str, 10); timeout.tv_sec = 2; timeout.tv_usec = 0; ret = vrpn_read_available_characters(serial_fd, inbuf, 2, &timeout); if (ret < 0) { perror("vrpn_CerealBox: Error reading ack from box\n"); return -1; } if (ret == 0) { fprintf(stderr,"vrpn_CerealBox: No ack from box\n"); return -1; } if (ret != 2) { fprintf(stderr,"vrpn_CerealBox: Got %d of 2 expected ack characters\n",ret); return -1; } if (inbuf[0] != 'a') { fprintf(stderr,"vrpn_CerealBox: Bad ack: wanted 'a', got '%c'\n",inbuf[0]); return -1; } //----------------------------------------------------------------------- // Ask the box to send a report, and go into SYNCING mode to get it. vrpn_write_characters(serial_fd, (unsigned char *)"pE", 2); status = STATUS_SYNCING; printf("CerealBox reset complete.\n"); //----------------------------------------------------------------------- // Figure out how many characters to expect in each report from the device // There is a 'p' to start and '\n' to finish, two bytes for each analog // value, 4 bytes for each encoder. Buttons are enabled in banks of 8, // but each bank of 8 is returned in 2 bytes (4 bits each). _expected_chars = 2 + 2*_numchannels + _numencoders*4 + ((_numbuttons+7) / 8) * 2; 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 the time, analog, button and encoder fields and call // the report methods on these. The time stored is that of // the first character received as part of the report. Reports start with // the header "p" and end with "\n", and is of the length _expected_chars. // If we get a report that is not valid, we assume that we have lost a // character or something and re-synchronize by waiting // until the start-of-report character ('p') comes around again. // The routine that calls this one // makes sure we get a full reading often enough (ie, it is responsible // for doing the watchdog timing to make sure the box hasn't simply // stopped sending characters). // Returns 1 if got a complete report, 0 otherwise. int vrpn_CerealBox::get_report(void) { int ret; // Return value from function call to be checked int i; // Loop counter int nextchar = 1; // Index of the next character to read //-------------------------------------------------------------------- // The reports are each _expected_chars characters long, and each start with an // ASCII 'p' character. If we're synching, read a byte at a time until we // find a 'p' character. //-------------------------------------------------------------------- if (status == STATUS_SYNCING) { // Try to get a character. If none, just return. if (vrpn_read_available_characters(serial_fd, _buffer, 1) != 1) { return 0; } // If it is not a 'p', we don't want it but we // need to look at the next one, so just return and stay // in Syncing mode so that we will try again next time through. if ( _buffer[0] != 'p') { fprintf(stderr,"vrpn_CerealBox: Syncing (looking for 'p', " "got '%c')\n", _buffer[0]); return 0; } // Got the first character of a report -- go into READING mode // and record that we got one character at this time. The next // bit of code will attempt to read the rest of the report. // The time stored here is as close as possible to when the // report was generated. _bufcount = 1; vrpn_gettimeofday(×tamp, NULL); status = STATUS_READING; #ifdef VERBOSE printf("... Got the 1st char\n"); #endif } //-------------------------------------------------------------------- // Read as many bytes of this report as we can, storing them // in the buffer. We keep track of how many have been read so far // and only try to read the rest. The routine that calls this one // makes sure we get a full reading often enough (ie, it is responsible // for doing the watchdog timing to make sure the device hasn't simply // stopped sending characters). //-------------------------------------------------------------------- ret = vrpn_read_available_characters(serial_fd, &_buffer[_bufcount], _expected_chars-_bufcount); if (ret == -1) { send_text_message("vrpn_CerealBox: Error reading", timestamp, vrpn_TEXT_ERROR); status = STATUS_RESETTING; return 0; } _bufcount += ret; #ifdef VERBOSE if (ret != 0) printf("... got %d characters (%d total)\n",ret, _bufcount); #endif if (_bufcount < _expected_chars) { // 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. If it does not, we need to go back into // synch mode and ignore this report. A well-formed report has the // first character 'p', and the last character is '\n'. //-------------------------------------------------------------------- if (_buffer[0] != 'p') { status = STATUS_SYNCING; fprintf(stderr,"vrpn_CerealBox: Not 'p' in record\n"); return 0; } if (_buffer[_expected_chars-1] != '\n') { status = STATUS_SYNCING; fprintf(stderr,"vrpn_CerealBox: No carriage return in record\n"); return 0; } #ifdef VERBOSE printf("got a complete report (%d of %d)!\n", _bufcount, _expected_chars); #endif //-------------------------------------------------------------------- // Ask the device to send us another report. Ideally, this would be // sent earlier so that we can overlap with the previous report. However, // when we ask after the first character we start losing parts of the // reports when we've turned on a lot of inputs. So, we do it here // after the report has come in. //-------------------------------------------------------------------- vrpn_write_characters(serial_fd, (unsigned char *)"pE", 2); //-------------------------------------------------------------------- // Decode the report and store the values in it into the parent classes // (analog, button and encoder). This code is modelled on the routines // convert_serial() and unpack_encoders() in the BG systems code. //-------------------------------------------------------------------- { // Digital code. There appear to be 4 bits (four buttons) stored // in each byte, in the low-order 4 bits after the offset of 0x21 // has been removed from each byte. They seem to come in highest- // buttons first, with the highest within each bank in the leftmost // bit. This assumes we are not using MP for digital inputs. int i; int numbuttonchars; // Read two characters for each eight buttons that are on, from the // highest bank down. numbuttonchars = ((_numbuttons+7) / 8) * 2; for (i = numbuttonchars-1; i >= 0; i--) { // Find the four bits for these buttons by subtracting // the offset to get them into the low-order 4 bits char bits = (char)(_buffer[nextchar++] - offset); // Set the buttons for each bit buttons[ i*4 + 3 ] = ( (bits & 0x08) != 0); buttons[ i*4 + 2 ] = ( (bits & 0x04) != 0); buttons[ i*4 + 1 ] = ( (bits & 0x02) != 0); buttons[ i*4 + 0 ] = ( (bits & 0x01) != 0); } } {// Analog code. Looks like there are two characters for each // analog value; this conversion code grabbed right from the // BG code. They seem to come in lowest-numbered first. int intval, i; double realval; for (i = 0; i < _numchannels; i++) { intval = (0x3f & (_buffer[nextchar++]-offset)) << 6; intval |= (0x3f & (_buffer[nextchar++]-offset)); realval = -1.0 + (2.0 * intval/4095.0); channel[i] = realval; } } { // Encoders. They come packed as 24-bit values with 6 bits in // each byte (offset by 0x21). They seem to come least-significant // part first. This decoding is valid only for incremental // encoders. Remember to convert the encoder values into fractions // of a revolution. for (i = 0; i < _numencoders; i++) { int enc0, enc1, enc2, enc3; long increment; enc0 = (_buffer[nextchar++]-offset) & 0x3f; increment = enc0; enc1 = (_buffer[nextchar++]-offset) & 0x3f; increment |= enc1 << 6; enc2 = (_buffer[nextchar++]-offset) & 0x3f; increment |= enc2 << 12; enc3 = (_buffer[nextchar++]-offset) & 0x3f; increment |= enc3 << 18; if ( increment & 0x800000 ) { dials[i] = (int)(increment - 16777216) * REV_PER_TICK; } else { dials[i] = (int)(increment) * REV_PER_TICK; } } } //-------------------------------------------------------------------- // Done with the decoding, send the reports and go back to syncing //-------------------------------------------------------------------- report_changes(); status = STATUS_SYNCING; _bufcount = 0; return 1; } void vrpn_CerealBox::report_changes(vrpn_uint32 class_of_service) { vrpn_Analog::timestamp = timestamp; vrpn_Button::timestamp = timestamp; vrpn_Dial::timestamp = timestamp; vrpn_Analog::report_changes(class_of_service); vrpn_Button::report_changes(); vrpn_Dial::report_changes(); } void vrpn_CerealBox::report(vrpn_uint32 class_of_service) { vrpn_Analog::timestamp = timestamp; vrpn_Button::timestamp = timestamp; vrpn_Dial::timestamp = timestamp; vrpn_Analog::report(class_of_service); vrpn_Button::report_changes(); vrpn_Dial::report(); } // 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 cerealbox, // either trying to reset it or trying to get a reading from it. void vrpn_CerealBox::mainloop() { // Call the generic server mainloop, since we are a server 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 as long as they come struct timeval current_time; vrpn_gettimeofday(¤t_time, NULL); if ( vrpn_TimevalDuration(current_time,timestamp) > MAX_TIME_INTERVAL) { fprintf(stderr,"CerealBox failed to read... current_time=%ld:%ld, timestamp=%ld:%ld\n", current_time.tv_sec, static_cast<long>(current_time.tv_usec), timestamp.tv_sec, static_cast<long>(timestamp.tv_usec)); send_text_message("Too long since last report, resetting", current_time, vrpn_TEXT_ERROR); status = STATUS_RESETTING; } } break; default: fprintf(stderr,"vrpn_CerealBox: Unknown mode (internal error)\n"); break; } }