touchRead() but only with bad soldering

Status
Not open for further replies.

pictographer

Well-known member
My latest project is a desk light. It uses a Teensy LC in an electrical junction box 4"x2"x1.5" made of metal that's about a 1/16" thick. While I was refining it, I had a wire soldered from Teensy LC pin 0 to the metal junction box. touchRead(0) worked as expected. After adding a few more components and doing a "better" job of soldering the wire to the metal box, touchRead(0) stopped working and always returned 65535.

Eventually, it dawned on me that if I disconnected the wire from the case, but masking taped it to the case, perhaps touchRead(0) would work again. And it did, but just barely.

I'd like to understand better what's going on and how to make touchRead() work as intended for touches on the metal box. My guess is that the capacitance of the box is too much for the sensor, so the measurement overflows. I'm wondering if I did a more careful job of getting the wire close to the case without being electrically connected if I could make it work better. If that doesn't work, maybe I'll thread the wire through one of the holes so that I can touch it without the case being directly involved, but that probably won't look as nice.

outside.jpginside.jpg

In case anyone is curious, the light has 8 APA102 RGB LEDs, a PIR sensor, a photodiode, a PC piezo speaker, and a push button. It's a testbed for other projects that are less convenient to work on.

The code is probably not needed and is somewhat rough, but here it is.

Code:
#include <Bounce2.h>

#include <cstring>
#include <climits>


#include <InternalTemperature.h>


#include <EEPROM.h>
// TODO https://github.com/pictographer/didah/blob/master/EEPROMAnything.h
#include <Keyboard.h>


// The marvelous FastLED library provides a high-level interface to
// many kinds of RGB LEDs and drives them efficiently. See
// http://fastled.io
#include <FastLED.h>


#if defined(TEENSYDUINO)
#if defined(__MK20DX128__)
#define BOARD "Teensy 3.0"
#elif defined(__MK20DX256__)
#define BOARD "Teensy 3.1/3.2"
#elif defined(__MKL26Z64__)
#define BOARD "Teensy LC"
#elif defined(__MK64FX512__)
#define BOARD "Teensy 3.5"
#elif defined(__MK66FX1M0__)
#define BOARD "Teensy 3.6"
#else
#error "Unknown board"
#endif
#endif


// Desklight
const int PIR_INPUT_YELLOW = 2;


const int MOMENTARY_BUTTON1_WHITE = 9;
const int MOMENTARY_BUTTON2_WHITE = 10;
const int DEBOUNCE_MS = 10;
Bounce momentaryButton = Bounce(MOMENTARY_BUTTON1_WHITE, DEBOUNCE_MS);




// Parameters for our LEDs
#define LED_TYPE APA102
#define COLOR_ORDER BGR
//const unsigned int NUM_LEDS = 96;
const unsigned int NUM_LEDS = 8;
const unsigned int CLOCK_MHZ = 12;


const int TOUCH_CASE_SENSOR_BLUE = 0;
const int TOUCH_CASE_BASELINE = 1613;
const int TOUCH_CASE_MAX = 8258;
const int TOUCH_CASE_MIN = 1388;
const int DATA_PIN = 11;
const int CLOCK_PIN = 14;
const int PHOTODIODE_ANODE_BLACK = 16;
const int PHOTODIODE_CATHODE_RED = 17;
const int PHOTODIODE_MAX = 985;
const int PHOTODIODE_MIN = 25;
const int PHOTODIODE_DAYLIGHT = 818;
const int PHOTODIODE_THRESHOLD = 708;
const int SPEAKER_BLACK = 22;
const int SPEAKER_RED = 23;


// Storage for LED colors.
CRGB leds[NUM_LEDS];


char buffer[64] = { 0 };


// Size of null-terminated command buffer.
const size_t cmax = 256;
static char cmdline[cmax];


// Send Super-L to lock the screen on Ubuntu 20.
// Caution: Do not call this in a tight loop.
void lockScreen() {
  Keyboard.set_modifier(0);
  Keyboard.set_modifier(MODIFIERKEY_GUI);
  Keyboard.set_key1(uint8_t(KEY_L));
  Keyboard.send_now();
  Keyboard.set_modifier(0);
  Keyboard.set_key1(0);
  Keyboard.send_now();
}


int readLight() {
  const auto count = 1024L;
  digitalWriteFast(PHOTODIODE_ANODE_BLACK, 1);
  auto sum = 0L;
  for (auto i = 0L; i < count; ++i) {
    sum += analogRead(PHOTODIODE_CATHODE_RED);
  }
  digitalWriteFast(PHOTODIODE_ANODE_BLACK, 0);
  return sum / count;
}


float readTemperatureF() {
  const auto count = 16L;
  auto sum = 0.0f;
  for (auto i = 0L; i < count; ++i) {
    sum += InternalTemperature.readUncalibratedTemperatureF();
  }
  return sum / count;
}


void rotate(int k) {
  auto c = leds[0];
  for (auto i = 0U; i < NUM_LEDS; ++i) {
    leds[i] = leds[(i + k) % NUM_LEDS];
  }
  leds[NUM_LEDS - 1] = c;
}


bool pollTouch() {
  int touch_reading = touchRead(TOUCH_CASE_SENSOR_BLUE);
  return TOUCH_CASE_BASELINE + (TOUCH_CASE_MAX - TOUCH_CASE_BASELINE) / 4 < touch_reading;
}


void basicBeep() {
  analogWriteFrequency(SPEAKER_RED, 440);
  analogWrite(SPEAKER_RED, 127);
  delay(100);
  analogWrite(SPEAKER_RED, 0);
}


// Read a line into cmdline. Complain and discard if longer than cmax - 1.
void pollCommand(char cmdline[], size_t cmax) {
  size_t cindex = 0;
  while (Serial.available()) {
    unsigned int r = Serial.read();
    if (!cindex && isspace(r)) continue; // Discard leading spaces.
    if (isprint(r)) {
      cmdline[cindex++] = r;
      cmdline[cindex] = 0;
      if (cindex == cmax - 1) {
        // The input is too long, thus invalid. Discard it.
        cindex = 0;
        cmdline[cindex] = 0;
        while (Serial.available()) Serial.read();
        Serial.println("Command buffer overflow!");
        basicBeep();
        break;
      }
    }
  }
  // Trim trailing spaces.
  while (cindex && isspace(cmdline[cindex - 1])) {
    --cindex;
  }
  cmdline[cindex] = 0;
}


bool isEqual(const char* a, const char* b) {
  return !strcmp(a, b);
}


// Is a a prefix of abc?
bool isPrefix(const char* a, const char* abc) {
  while (*a && *abc) {
    if (*a != *abc) {
      return false;
    }
    ++a;
    ++abc;
  }
  return *a == 0;
}


// TODO - With the better implementation, we could put these back.
// Duplicates
//    case CRGB::Aqua: return "Aqua";
//    case CRGB::DarkGrey: return "DarkGrey";
//    case CRGB::DarkSlateGrey: return "DarkSlateGrey";
//    case CRGB::DimGrey: return "DimGrey";
//    case CRGB::Fuchsia: return "Fuchsia";
//    case CRGB::Grey: return "Grey";
//    case CRGB::LightGrey: return "LightGrey";
//    case CRGB::LightSlateGrey: return "LightSlateGrey";
//    case CRGB::SlateGrey: return "SlateGrey";


// TODO - Remove useless color names.
// Many of these colors are obscure or don't look different than more common colors.


const struct {
  const CRGB colorCode;
  const char* colorName;
} colorName[] = {
  { CRGB::AliceBlue, "AliceBlue" },
  { CRGB::Amethyst, "Amethyst" },
  { CRGB::AntiqueWhite, "AntiqueWhite" },
  { CRGB::Aquamarine, "Aquamarine" },
  { CRGB::Azure, "Azure" },
  { CRGB::Beige, "Beige" },
  { CRGB::Bisque, "Bisque" },
  { CRGB::Black, "Black" },
  { CRGB::BlanchedAlmond, "BlanchedAlmond" },
  { CRGB::Blue, "Blue" },
  { CRGB::BlueViolet, "BlueViolet" },
  { CRGB::Brown, "Brown" },
  { CRGB::BurlyWood, "BurlyWood" },
  { CRGB::CadetBlue, "CadetBlue" },
  { CRGB::Chartreuse, "Chartreuse" },
  { CRGB::Chocolate, "Chocolate" },
  { CRGB::Coral, "Coral" },
  { CRGB::CornflowerBlue, "CornflowerBlue" },
  { CRGB::Cornsilk, "Cornsilk" },
  { CRGB::Crimson, "Crimson" },
  { CRGB::Cyan, "Cyan" },
  { CRGB::DarkBlue, "DarkBlue" },
  { CRGB::DarkCyan, "DarkCyan" },
  { CRGB::DarkGoldenrod, "DarkGoldenrod" },
  { CRGB::DarkGray, "DarkGray" },
  { CRGB::DarkGreen, "DarkGreen" },
  { CRGB::DarkKhaki, "DarkKhaki" },
  { CRGB::DarkMagenta, "DarkMagenta" },
  { CRGB::DarkOliveGreen, "DarkOliveGreen" },
  { CRGB::DarkOrange, "DarkOrange" },
  { CRGB::DarkOrchid, "DarkOrchid" },
  { CRGB::DarkRed, "DarkRed" },
  { CRGB::DarkSalmon, "DarkSalmon" },
  { CRGB::DarkSeaGreen, "DarkSeaGreen" },
  { CRGB::DarkSlateBlue, "DarkSlateBlue" },
  { CRGB::DarkSlateGray, "DarkSlateGray" },
  { CRGB::DarkTurquoise, "DarkTurquoise" },
  { CRGB::DarkViolet, "DarkViolet" },
  { CRGB::DeepPink, "DeepPink" },
  { CRGB::DeepSkyBlue, "DeepSkyBlue" },
  { CRGB::DimGray, "DimGray" },
  { CRGB::DodgerBlue, "DodgerBlue" },
  { CRGB::FireBrick, "FireBrick" },
  { CRGB::FloralWhite, "FloralWhite" },
  { CRGB::ForestGreen, "ForestGreen" },
  { CRGB::Gainsboro, "Gainsboro" },
  { CRGB::GhostWhite, "GhostWhite" },
  { CRGB::Gold, "Gold" },
  { CRGB::Goldenrod, "Goldenrod" },
  { CRGB::Gray, "Gray" },
  { CRGB::Green, "Green" },
  { CRGB::GreenYellow, "GreenYellow" },
  { CRGB::Honeydew, "Honeydew" },
  { CRGB::HotPink, "HotPink" },
  { CRGB::IndianRed, "IndianRed" },
  { CRGB::Indigo, "Indigo" },
  { CRGB::Ivory, "Ivory" },
  { CRGB::Khaki, "Khaki" },
  { CRGB::Lavender, "Lavender" },
  { CRGB::LavenderBlush, "LavenderBlush" },
  { CRGB::LawnGreen, "LawnGreen" },
  { CRGB::LemonChiffon, "LemonChiffon" },
  { CRGB::LightBlue, "LightBlue" },
  { CRGB::LightCoral, "LightCoral" },
  { CRGB::LightCyan, "LightCyan" },
  { CRGB::LightGoldenrodYellow, "LightGoldenrodYellow" },
  { CRGB::LightGreen, "LightGreen" },
  { CRGB::LightPink, "LightPink" },
  { CRGB::LightSalmon, "LightSalmon" },
  { CRGB::LightSeaGreen, "LightSeaGreen" },
  { CRGB::LightSkyBlue, "LightSkyBlue" },
  { CRGB::LightSlateGray, "LightSlateGray" },
  { CRGB::LightSteelBlue, "LightSteelBlue" },
  { CRGB::LightYellow, "LightYellow" },
  { CRGB::Lime, "Lime" },
  { CRGB::LimeGreen, "LimeGreen" },
  { CRGB::Linen, "Linen" },
  { CRGB::Magenta, "Magenta" },
  { CRGB::Maroon, "Maroon" },
  { CRGB::MediumAquamarine, "MediumAquamarine" },
  { CRGB::MediumBlue, "MediumBlue" },
  { CRGB::MediumOrchid, "MediumOrchid" },
  { CRGB::MediumPurple, "MediumPurple" },
  { CRGB::MediumSeaGreen, "MediumSeaGreen" },
  { CRGB::MediumSlateBlue, "MediumSlateBlue" },
  { CRGB::MediumSpringGreen, "MediumSpringGreen" },
  { CRGB::MediumTurquoise, "MediumTurquoise" },
  { CRGB::MediumVioletRed, "MediumVioletRed" },
  { CRGB::MidnightBlue, "MidnightBlue" },
  { CRGB::MintCream, "MintCream" },
  { CRGB::MistyRose, "MistyRose" },
  { CRGB::Moccasin, "Moccasin" },
  { CRGB::NavajoWhite, "NavajoWhite" },
  { CRGB::Navy, "Navy" },
  { CRGB::OldLace, "OldLace" },
  { CRGB::Olive, "Olive" },
  { CRGB::OliveDrab, "OliveDrab" },
  { CRGB::Orange, "Orange" },
  { CRGB::OrangeRed, "OrangeRed" },
  { CRGB::Orchid, "Orchid" },
  { CRGB::PaleGoldenrod, "PaleGoldenrod" },
  { CRGB::PaleGreen, "PaleGreen" },
  { CRGB::PaleTurquoise, "PaleTurquoise" },
  { CRGB::PaleVioletRed, "PaleVioletRed" },
  { CRGB::PapayaWhip, "PapayaWhip" },
  { CRGB::PeachPuff, "PeachPuff" },
  { CRGB::Peru, "Peru" },
  { CRGB::Pink, "Pink" },
  { CRGB::Plaid, "Plaid" },
  { CRGB::Plum, "Plum" },
  { CRGB::PowderBlue, "PowderBlue" },
  { CRGB::Purple, "Purple" },
  { CRGB::Red, "Red" },
  { CRGB::RosyBrown, "RosyBrown" },
  { CRGB::RoyalBlue, "RoyalBlue" },
  { CRGB::SaddleBrown, "SaddleBrown" },
  { CRGB::Salmon, "Salmon" },
  { CRGB::SandyBrown, "SandyBrown" },
  { CRGB::SeaGreen, "SeaGreen" },
  { CRGB::Seashell, "Seashell" },
  { CRGB::Sienna, "Sienna" },
  { CRGB::Silver, "Silver" },
  { CRGB::SkyBlue, "SkyBlue" },
  { CRGB::SlateBlue, "SlateBlue" },
  { CRGB::SlateGray, "SlateGray" },
  { CRGB::Snow, "Snow" },
  { CRGB::SpringGreen, "SpringGreen" },
  { CRGB::SteelBlue, "SteelBlue" },
  { CRGB::Tan, "Tan" },
  { CRGB::Teal, "Teal" },
  { CRGB::Thistle, "Thistle" },
  { CRGB::Tomato, "Tomato" },
  { CRGB::Turquoise, "Turquoise" },
  { CRGB::Violet, "Violet" },
  { CRGB::Wheat, "Wheat" },
  { CRGB::White, "White" },
  { CRGB::WhiteSmoke, "WhiteSmoke" },
  { CRGB::Yellow, "Yellow" },
  { CRGB::YellowGreen, "YellowGreen" },
  { CRGB::FairyLight, "FairyLight" },
  { CRGB::FairyLightNCC, "FairyLightNCC" }
};
const size_t colorNameCount = sizeof colorName / sizeof colorName[0];


const char* colorToString(const CRGB color) {
  for (auto i = 0U; i < colorNameCount; ++i) {
    if (colorName[i].colorCode == color) return colorName[i].colorName;
  }
  return NULL;
}


const CRGB colorNameToCode(const char* name) {
  for (auto i = 0U; i < colorNameCount; ++i) {
    if (isEqual(name, colorName[i].colorName)) return colorName[i].colorCode;
  }
  return CRGB::Black;
}


void setup() {
  while (!Serial && millis() < 2000) ;
  pinMode(MOMENTARY_BUTTON1_WHITE, INPUT_PULLDOWN);
  pinMode(MOMENTARY_BUTTON2_WHITE, OUTPUT);


  pinMode(PIR_INPUT_YELLOW, INPUT_PULLDOWN);


  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(SPEAKER_BLACK, OUTPUT);
  pinMode(SPEAKER_RED, OUTPUT);
  digitalWriteFast(SPEAKER_BLACK, 0);
  digitalWriteFast(SPEAKER_RED, 0);
  pinMode(PHOTODIODE_CATHODE_RED, INPUT_PULLDOWN);
  pinMode(PHOTODIODE_ANODE_BLACK, OUTPUT);
  digitalWriteFast(PHOTODIODE_ANODE_BLACK, 0);


  InternalTemperature.begin(/* TEMPERATURE_NO_ADC_SETTING_CHANGES */);
  {
    //  bool isOkay = InternalTemperature.singlePointCalibrationF(80.0f, 80.0f);
    bool isOkay = InternalTemperature.dualPointCalibrationC(73.9f, 74.6f,
                  80.0f, 84.0f);
    if (!isOkay && Serial) {
      Serial.printf("Temperature calibration failed.\n");
    }
  }


  // And now the star of the show. Initialize the LEDs.
  FastLED.addLeds<LED_TYPE, DATA_PIN, CLOCK_PIN,
                  COLOR_ORDER, DATA_RATE_MHZ(CLOCK_MHZ)>(leds, NUM_LEDS);


  if (Serial) {
    Serial.printf("Serial connection time (ms): %d\n", millis());
    Serial.printf("Teensy board type: %s\n", BOARD);
    Serial.printf("sizeof(long) = %d; sizeof(int) = %d\n", sizeof(long), sizeof(int));
    // SIM_UIDMH is x55 on two Teensy LCs
    // SIM_UIDML is 0x262014 on my desk lamp, 0x2b2014 on a spare Teensy LC
    // SIM_UIDL ia x33974e45 on two Teensy LCs
    char id[36];
#if defined(__MKL26Z64__)
    sprintf(id, "%08lX-%08lX-%08lX", SIM_UIDMH, SIM_UIDML, SIM_UIDL);
#else
    sprintf(id, "%08lX-%08lX-%08lX-%08lX", SIM_UIDH, SIM_UIDMH, SIM_UIDML, SIM_UIDL);
#endif
    Serial.printf("CPU ID: %s\n", id);
    Serial.printf("CPU Frequency: %d\n", F_CPU);
    Serial.printf("CPU Bus Frequency: %d\n", F_BUS);
    Serial.printf("CPU Memory Frequency: %d\n", F_MEM);
    Serial.printf("Build time: %s\n", __DATE__ " " __TIME__);
    Serial.printf("Arduino version: %d\n", ARDUINO);
    Serial.printf("Main source file: %s\n", __FILE__);
    Serial.printf("Touch Read: %d\n", touchRead(TOUCH_CASE_SENSOR_BLUE));
    Serial.printf("Light Read: %d\n", readLight());
    Serial.printf("Temperature (°F): ");
    Serial.println(readTemperatureF(), 1);
    Serial.printf("Temperature uncalibrated (°F): ");
    Serial.println(InternalTemperature.readUncalibratedTemperatureF(), 1);
    basicBeep();
  }


  fill_solid(leds, NUM_LEDS, CRGB::White);
  FastLED.setBrightness(8);
  FastLED.show();
}


elapsedMillis sincePIR;


void loop() {
  if (pollTouch()) {
    //basicBeep();
  }


  if (digitalReadFast(PIR_INPUT_YELLOW)) {
    fill_rainbow(leds, NUM_LEDS, sincePIR, 8);
    FastLED.show();
    // Motion was detected. The detector goes to sleep for 2.5 seconds after a detection.
    if (2500 < sincePIR) sincePIR = 0;
  }


  if (1000 * 60 * 29 < sincePIR) {
    // 1 minute until locking the screen
    fill_solid(leds, NUM_LEDS, CRGB::Red);
    FastLED.show();
  }


  if (1000 * 60 * 30 < sincePIR) {
    sincePIR = 0;
    // Lock the screen; it's been 30 minutes since the PIR sensor triggered.
    delay(1000); // Delay a second just in case.
    lockScreen();
  }


  digitalWriteFast(MOMENTARY_BUTTON2_WHITE, 1);
  if (momentaryButton.update() && momentaryButton.fallingEdge()) {
    if (strlen(buffer)) {
      Keyboard.print(buffer);
    } else {
      Serial.printf("Enter 'macro <text>' to assign a macro.\n");
    }
    basicBeep();
  }
  digitalWriteFast(MOMENTARY_BUTTON2_WHITE, 0);


#if 0
  auto light_level = readLight();
  if (light_level < PHOTODIODE_MIN) {
    Serial.printf("Light = %d\n", light_level);
  }
#endif


  // Handle USB-serial commands.


  pollCommand(cmdline, cmax);
  if (cmdline[0]) {
    if (strcmp(cmdline, "help") == 0) {
      Serial.printf("No help yet!\n");
    } else if (isPrefix("macro", cmdline)) {
      sscanf(cmdline, "macro %s", &buffer); // TODO - input length limit
      Serial.printf("Got '%s'.\n", buffer);
    } else if (isEqual("pir", cmdline)) {
      Serial.printf("Time since PIR (seconds): %ld\n", sincePIR / 1000);
    } else if (isPrefix("rgb", cmdline)) {
      unsigned int r, g, b;
      int count = sscanf(cmdline, "rgb %u %u %u", &r, &g, &b);
      if (count != 3) {
        Serial.printf("Expecting red, green, and blue values in the range 0..255.\n");
      }
      fill_solid(leds, NUM_LEDS, CRGB(r, g, b));
      FastLED.show();
    } else if (isEqual(cmdline, "ramp")) {
      fill_gradient_RGB(leds, 0, CRGB::White, NUM_LEDS - 1, CRGB(1, 1, 1));
      FastLED.show();
    } else if (colorNameToCode(cmdline) != (CRGB) CRGB::Black) {
      fill_solid(leds, NUM_LEDS, colorNameToCode(cmdline));
      FastLED.show();
    } else if (isEqual(cmdline, "Black")) {
      fill_solid(leds, NUM_LEDS, CRGB::Black);
      FastLED.show();
    } else if (isPrefix("dither", cmdline)) {
      bool isEnabled = strstr(cmdline, "on") || strstr(cmdline, "1");
      bool isDisabled = strstr(cmdline, "off") || strstr(cmdline, "0");
      if (isEnabled != isDisabled) {
        FastLED.setDither(isEnabled);
        Serial.printf("Set dither %s\n", isEnabled ? "on" : "off");
      } else {
        Serial.printf("Expecting dither setting of 'on' or 'off'\n");
      }
    } else if (isEqual(cmdline, "rainbow")) {
      int hue_span = 128 / NUM_LEDS;
      if (hue_span == 0) hue_span = 1;
      for (auto i = 0U; i < (128 + 32) * 4; ++i) {
        fill_rainbow(leds, NUM_LEDS, i, hue_span);
        FastLED.delay(10);
      }
    } else if (isEqual(cmdline, "rotate")) {
      rotate(1);
    } else if (isPrefix("brightness", cmdline)) {
      // Note: sizeof below includes the terminating null.
      const auto arg = cmdline + sizeof("brightness");
      const auto level = atoi(arg);
      const auto arglen = strlen(arg);
      if (0 < arglen && arglen < 4 && level < 256) {
        FastLED.setBrightness(level);
      } else if (arglen) {
        Serial.printf("Expecting brightness from 0..255. Got '%s'.\n", arg);
      }
      Serial.printf("Brightness: %u\n", (unsigned int) FastLED.getBrightness());
    } else if (isEqual(cmdline, "light")) {
      Serial.printf("Light: %d\n", readLight());
    } else if (isEqual(cmdline, "temperature")) {
      Serial.print("Temperature (°F): ");
      Serial.println(readTemperatureF(), 1);
      Serial.print("Uncalibrated: ");
      Serial.println(InternalTemperature.readUncalibratedTemperatureF(), 1);
    } else if (isEqual(cmdline, "colors")) {
      for (auto i = 0U; i < NUM_LEDS; ++i) {
        auto name = colorToString(leds[i]);
        if (!name) name = "";
        Serial.printf("%u: rgb(%3u, %3u, %3u) %s\n", i, leds[i].red, leds[i].green, leds[i].blue, name);
      }
    } else if (isEqual(cmdline, "touch")) {
      Serial.printf("Touch case: %u\n", touchRead(TOUCH_CASE_SENSOR_BLUE));
      Serial.printf("Touch 1: %u\n", touchRead(1));
      Serial.printf("Touch 3: %u\n", touchRead(3));
      Serial.printf("Touch 4: %u\n", touchRead(4));
    }
    cmdline[0] = 0;
    FastLED.show();
  }
}
 
With the touchRead sense wire just pressing against the metal case, the difference between touch and no touch was about two counts. Now with a bit of magnet wire taped to the inside of the case over the length of the long side, the counts differ by about 20 from 739 to 759. Seems to work reliably after a couple of minutes of testing.
 
Status
Not open for further replies.
Back
Top