Author Topic: Improved/optimized DS18B20/1Wire read  (Read 21151 times)

ColinR

  • Full Member
  • ***
  • Posts: 176
Improved/optimized DS18B20/1Wire read
« on: October 05, 2014, 04:50:15 PM »
Hello all,

I've been picking apart my code to do some optimization, and an obvious target was the read of 1Wire devices, the DS18B20 in particular. Currently, I use the OneWire library, without the DallasTemperature library, which I didn't much care for.

What all 1Wire read routines have in common is that they issue the Convert command prior to reading a measured value. For the DS18B20, this is the Convert T command, 0x44. Depending on the resolution, the conversion takes from 75-750ms, per the datasheet.

There are three main issues I had with the implementation using OneWire as in the examples:
  • There was no provision for setting the resolution. While this is available in the DallasTemperature library, it just didn't make sense to me to rewrite all of my lean code around this library to get this one feature.
  • Wait times are hardcoded. Per the datasheet, it is possible to check with the device for status after issuing the Convert T command to know exactly when the temperature conversion is complete. In most implementations, however, this wait time is hard-coded, and typically VERY generously. For example, in a read operation at 12 bits that actually takes <600ms, the hardcoded wait time is 1000ms. This is a 40% savings, ladies and gentlemen. Futhermore, most test code does not even adjust the delay time for different resolution. This is just insane. A ready that should take 60-75ms will implement a wait of 1000ms (?!)
  • Everything is done synchronously. Nothing else can happen while we are waiting for this conversion This is just stupid and lazy coding, and the real reason I dug into this. If anything else in your program depends on timing, forget using the default 1Wire read code

So to easily work out the first two items, I created this example (code box 1 below). Run it after substituting your 1Wire pin, and you'll get something like this for reading 9, 10, 11, and 12 bit resolutions:

dsaddress:2838FF400500004E,
Conversion took: 76 ms
Raw Scratchpad Data:
50 1 0 0 1F FF 10 10 21
Temp (C): 21.00

dsaddress:2838FF400500004E,
Conversion took: 150 ms
Raw Scratchpad Data:
50 1 0 0 3F FF 10 10 51
Temp (C): 21.00

dsaddress:2838FF400500004E,
Conversion took: 298 ms
Raw Scratchpad Data:
50 1 0 0 5F FF 10 10 C1
Temp (C): 21.00

dsaddress:2838FF400500004E,
Conversion took: 596 ms
Raw Scratchpad Data:
4F 1 0 0 7F FF 1 10 37
Temp (C): 20.94


A HUGE improvement over stock wait times. With the accuracy of the DS18B20, it really doesn't make much sense to use 12 bits, so 10 bits saves me loads in timing. I left in a bunch of original code that's been commented out so you can see how it was done previously.

Now, we can also separate conversion commands and reading the data back. If you have multiple sensors, you actually want to use the Skip ROM and Convert T commands to tell all devices on the bus to convert simultaneously, but that's another topic. In the meantime while conversion is taking place, we can do other stuff.

So we separate our read DS18B20 routine into find, set resolution, send conversion command, and finally read temperature. Between the last two, we just continually check in our loop to see that data is ready, and when it is, we read it. Pretty simple, but SUPER EFFECTIVE. You can see we lose a little due to overhead, but still, plenty fast, and we can do other stuff at the same time.

Enjoy!
C

Temp (C): 21.50
Elapsed time (ms): 99
Temp (C): 21.50
Elapsed time (ms): 170
Temp (C): 21.62
Elapsed time (ms): 321
Temp (C): 21.56
Elapsed time (ms): 618



Code box 1:
Code: [Select]
#include <OneWire.h>

#define LED 9
#define SERIAL_BAUD   115200

void setup(void) {
  Serial.begin(SERIAL_BAUD);
}

void loop(void) {
  for (int i=9;i<13;i++){
    handleOWIO(6,i);
    Serial.println();
  }
 
  delay(1000); 
  Blink(LED,3);
}

void handleOWIO(byte pin, byte resolution) {
  int owpin = pin;
 
  // Device identifier
  byte dsaddr[8];
  char dscharaddr[16];
  OneWire myds(owpin);
  getfirstdsadd(myds,dsaddr);
 
  Serial.print(F("dsaddress:"));
  int j;
  for (j=0;j<8;j++) {
    if (dsaddr[j] < 16) {
      Serial.print('0');
    }
    Serial.print(dsaddr[j], HEX);
  }
  sprintf(dscharaddr,"%02x%02x%02x%02x%02x%02x%02x%02x",dsaddr[0],dsaddr[1],dsaddr[2],dsaddr[3],dsaddr[4],dsaddr[5],dsaddr[6],dsaddr[7]);
  Serial.println(',');
 
  // Data

  Serial.println(getdstemp(myds, dsaddr, resolution));
 
} // run OW sequence

void getfirstdsadd(OneWire myds, byte firstadd[]){
  byte i;
  byte present = 0;
  byte addr[8];
  float celsius, fahrenheit;
 
  int length = 8;
 
  //Serial.print("Looking for 1-Wire devices...\n\r");
  while(myds.search(addr)) {
    //Serial.print("\n\rFound \'1-Wire\' device with address:\n\r");
    for( i = 0; i < 8; i++) {
      firstadd[i]=addr[i];
      //Serial.print("0x");
      if (addr[i] < 16) {
        //Serial.print('0');
      }
      //Serial.print(addr[i], HEX);
      if (i < 7) {
        //Serial.print(", ");
      }
    }
    if ( OneWire::crc8( addr, 7) != addr[7]) {
        //Serial.print("CRC is not valid!\n");
        return;
    }
     // the first ROM byte indicates which chip

    //Serial.print("\n\raddress:");
    //Serial.print(addr[0]);
   
    return;
  }
}


float getdstemp(OneWire myds, byte addr[8], byte resolution) {
  byte present = 0;
  int i;
  byte data[12];
  byte type_s;
  float celsius;
  float fahrenheit;
 
  switch (addr[0]) {
    case 0x10:
      //Serial.println(F("  Chip = DS18S20"));  // or old DS1820
      type_s = 1;
      break;
    case 0x28:
      //Serial.println(F("  Chip = DS18B20"));
      type_s = 0;
      break;
    case 0x22:
      //Serial.println(F("  Chip = DS1822"));
      type_s = 0;
      break;
    default:
      Serial.println(F("Device is not a DS18x20 family device."));
  }
 
  // Get byte for desired resolution
  byte resbyte = 0x1F;
  if (resolution == 12){
    resbyte = 0x7F;
  }
  else if (resolution == 11) {
    resbyte = 0x5F;
  }
  else if (resolution == 10) {
    resbyte = 0x3F;
  }
 
  // Set configuration
  myds.reset();
  myds.select(addr);
  myds.write(0x4E);         // Write scratchpad
  myds.write(0);            // TL
  myds.write(0);            // TH
  myds.write(resbyte);         // Configuration Register
 
  myds.write(0x48);         // Copy Scratchpad
 
 
  myds.reset();
  myds.select(addr);
 
  long starttime = millis();
  myds.write(0x44,1);         // start conversion, with parasite power on at the end
  while (!myds.read()) {
    // do nothing
  }
  Serial.print("Conversion took: ");
  Serial.print(millis() - starttime);
  Serial.println(" ms");

  //delay(1000);     // maybe 750ms is enough, maybe not
  // we might do a ds.depower() here, but the reset will take care of it.
 
  present = myds.reset();
  myds.select(addr);   
  myds.write(0xBE);         // Read Scratchpad

  //Serial.print("  Data = ");
  //Serial.print(present,HEX);
  Serial.println("Raw Scratchpad Data: ");
  for ( i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = myds.read();
    Serial.print(data[i], HEX);
    Serial.print(" ");
  }
  //Serial.print(" CRC=");
  //Serial.print(OneWire::crc8(data, 8), HEX);
  Serial.println();

  // convert the data to actual temperature

  unsigned int raw = (data[1] << 8) | data[0];
  if (type_s) {
    raw = raw << 3; // 9 bit resolution default
    if (data[7] == 0x10) {
      // count remain gives full 12 bit resolution
      raw = (raw & 0xFFF0) + 12 - data[6];
    } else {
      byte cfg = (data[4] & 0x60);
      if (cfg == 0x00) raw = raw << 3;  // 9 bit resolution, 93.75 ms
        else if (cfg == 0x20) raw = raw << 2; // 10 bit res, 187.5 ms
        else if (cfg == 0x40) raw = raw << 1; // 11 bit res, 375 ms
        // default is 12 bit resolution, 750 ms conversion time
    }
  }
  celsius = (float)raw / 16.0;
  fahrenheit = celsius * 1.8 + 32.0;
  Serial.print("Temp (C): ");
  //Serial.println(celsius);
  return celsius;
}

void Blink(byte PIN, int DELAY_MS)
{
  pinMode(PIN, OUTPUT);
  digitalWrite(PIN,HIGH);
  delay(DELAY_MS);
  digitalWrite(PIN,LOW);
}

Code Block 2:
Code: [Select]
#include <OneWire.h>

#define LED 9
#define SERIAL_BAUD   115200

OneWire myds(6);
byte readstage;
byte resolution;
unsigned long starttime;
unsigned long elapsedtime;
byte dsaddr[8];

void setup(void) {
  Serial.begin(SERIAL_BAUD);
  readstage = 0;
  resolution = 12;
}

void loop(void) {
 
  if (readstage == 0){
      getfirstdsadd(myds,dsaddr);
      dssetresolution(myds,dsaddr,resolution);
      starttime = millis();
      dsconvertcommand(myds,dsaddr);
      readstage++;
  }
  else {
      if (myds.read()) {
        Serial.println(dsreadtemp(myds,dsaddr, resolution));
       
        Serial.print("Elapsed time (ms): ");
        elapsedtime = millis() - starttime;
        Serial.println(elapsedtime);
        readstage=0;
        if (resolution == 12){
          resolution = 9;
        }
        else {
          resolution ++;
        }
      }
  }
 
  Blink(LED,5);
}

void getfirstdsadd(OneWire myds, byte firstadd[]){
  byte i;
  byte present = 0;
  byte addr[8];
  float celsius, fahrenheit;
 
  int length = 8;
 
  //Serial.print("Looking for 1-Wire devices...\n\r");
  while(myds.search(addr)) {
    //Serial.print("\n\rFound \'1-Wire\' device with address:\n\r");
    for( i = 0; i < 8; i++) {
      firstadd[i]=addr[i];
      //Serial.print("0x");
      if (addr[i] < 16) {
//        Serial.print('0');
      }
//      Serial.print(addr[i], HEX);
      if (i < 7) {
        //Serial.print(", ");
      }
    }
    if ( OneWire::crc8( addr, 7) != addr[7]) {
        Serial.print("CRC is not valid!\n");
        return;
    }
     // the first ROM byte indicates which chip

    //Serial.print("\n\raddress:");
    //Serial.print(addr[0]);
   
    return;
  }
}

void dssetresolution(OneWire myds, byte addr[8], byte resolution) {
   
  // Get byte for desired resolution
  byte resbyte = 0x1F;
  if (resolution == 12){
    resbyte = 0x7F;
  }
  else if (resolution == 11) {
    resbyte = 0x5F;
  }
  else if (resolution == 10) {
    resbyte = 0x3F;
  }
 
  // Set configuration
  myds.reset();
  myds.select(addr);
  myds.write(0x4E);         // Write scratchpad
  myds.write(0);            // TL
  myds.write(0);            // TH
  myds.write(resbyte);         // Configuration Register
 
  myds.write(0x48);         // Copy Scratchpad
}

void dsconvertcommand(OneWire myds, byte addr[8]){
  myds.reset();
  myds.select(addr);
  myds.write(0x44,1);         // start conversion, with parasite power on at the end
 
}

float dsreadtemp(OneWire myds, byte addr[8], byte resolution) {
  byte present = 0;
  int i;
  byte data[12];
  byte type_s;
  float celsius;
  float fahrenheit;
 
  switch (addr[0]) {
    case 0x10:
      //Serial.println(F("  Chip = DS18S20"));  // or old DS1820
      type_s = 1;
      break;
    case 0x28:
      //Serial.println(F("  Chip = DS18B20"));
      type_s = 0;
      break;
    case 0x22:
      //Serial.println(F("  Chip = DS1822"));
      type_s = 0;
      break;
    default:
      Serial.println(F("Device is not a DS18x20 family device."));
  }
 
  present = myds.reset();
  myds.select(addr);   
  myds.write(0xBE);         // Read Scratchpad

  //Serial.print("  Data = ");
  //Serial.print(present,HEX);
//  Serial.println("Raw Scratchpad Data: ");
  for ( i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = myds.read();
//    Serial.print(data[i], HEX);
//    Serial.print(" ");
  }
  //Serial.print(" CRC=");
  //Serial.print(OneWire::crc8(data, 8), HEX);
//  Serial.println();

  // convert the data to actual temperature

  unsigned int raw = (data[1] << 8) | data[0];
  if (type_s) {
    raw = raw << 3; // 9 bit resolution default
    if (data[7] == 0x10) {
      // count remain gives full 12 bit resolution
      raw = (raw & 0xFFF0) + 12 - data[6];
    } else {
      byte cfg = (data[4] & 0x60);
      if (cfg == 0x00) raw = raw << 3;  // 9 bit resolution, 93.75 ms
        else if (cfg == 0x20) raw = raw << 2; // 10 bit res, 187.5 ms
        else if (cfg == 0x40) raw = raw << 1; // 11 bit res, 375 ms
        // default is 12 bit resolution, 750 ms conversion time
    }
  }
  celsius = (float)raw / 16.0;
  fahrenheit = celsius * 1.8 + 32.0;
  Serial.print("Temp (C): ");
  //Serial.println(celsius);
  return celsius;
}

void Blink(byte PIN, int DELAY_MS)
{
  pinMode(PIN, OUTPUT);
  digitalWrite(PIN,HIGH);
  delay(DELAY_MS);
  digitalWrite(PIN,LOW);
}
« Last Edit: October 05, 2014, 05:08:56 PM by ColinR »
CuPID Controls :: Open Source browser-based sensor and device control
Interfaceinnovations.org/cupidcontrols.html
cupidcontrols.com

Felix

  • Administrator
  • Hero Member
  • *****
  • Posts: 5850
  • Country: us
    • LowPowerLab
Re: Improved/optimized DS18B20/1Wire read
« Reply #1 on: October 05, 2014, 10:43:06 PM »
Awesome, very nice work sir. And if you couple that DS temp reading with John's equation for temperature compensation (and lowering the bitrate and bandwidth) you will get a highly accurate temperature compensated transceiver which will yield a multi fold increase in range.
Details here:
https://lowpowerlab.com/forum/index.php/topic,357.0.html

ColinR

  • Full Member
  • ***
  • Posts: 176
Re: Improved/optimized DS18B20/1Wire read
« Reply #2 on: October 06, 2014, 12:04:00 AM »
Rad!

I just can't believe that this code doesn't exist already. Hanging your micro for a full second to read temperature just seems nutso, especially when a huge point of using a micro is that it's fast!

I updated the Arduino site: http://playground.arduino.cc/Learning/OneWire

And added an article on my blog: http://www.cupidcontrols.com/2014/10/moteino-arduino-and-1wire-optimize-your-read-for-speed/

Cheers,
C
CuPID Controls :: Open Source browser-based sensor and device control
Interfaceinnovations.org/cupidcontrols.html
cupidcontrols.com

kiwisincebirth

  • Jr. Member
  • **
  • Posts: 69
Re: Improved/optimized DS18B20/1Wire read
« Reply #3 on: October 06, 2014, 01:12:56 AM »
Thanks for the good work Colin, two questions

1. Would it be possible to initiate a reading, then put the Moteino into a deep sleep for a few seconds (to a few minutes),  wake up and retrieve the reading, transmit over RFM radio, then sleep again. I am thinking of conserving battery power while the measurement is being taken.

2. Do you intend to turn your code into a library? If not do you object to someone else doing it for you?

Thanks.

ColinR

  • Full Member
  • ***
  • Posts: 176
Re: Improved/optimized DS18B20/1Wire read
« Reply #4 on: October 06, 2014, 02:20:43 AM »
Thanks.

Powering down would not work, as the DS18B20 pulls about 1mA during the conversion process. This is the main caveat of issuing a simultaneous conversion for all devices on the network - if you tell 100 devices to convert simultaneously, for example, you can end up with supply issues.

I don't have plans to put this into a library, but have no objections to anybody else doing so. The resolution and write alarm registers are critically important commands that should be accessible, as well as the separation of the convert and read functions. I know of folks who use the two alarm bytes to store data, as they stick around in nonvolatile memory.

Colin
CuPID Controls :: Open Source browser-based sensor and device control
Interfaceinnovations.org/cupidcontrols.html
cupidcontrols.com

ColinR

  • Full Member
  • ***
  • Posts: 176
Re: Improved/optimized DS18B20/1Wire read
« Reply #5 on: October 06, 2014, 05:12:01 PM »
Ok, here is an interesting wrinkle that is relevant for me:

Unless you retain the ds object and do not issue another reset or select command on the bus, you can not successfully continue to poll for data readiness status. Let me break this down:

When you read on a 1Wire bus, you do the following:
  • Issue a reset pulse to a bus designated by a pin. All devices 'listen up'. According to the data sheet, this does not interrupt a temperature conversion command
  • Issue a select/match command with the ROM.
  • Issue other commands

What I've discovered, however, is the following:
  • If I issue a conversion command and continue to check status (using the ds.read() as mentioned in the first post), I can tell when the conversion is complete.
  • If I reinstantiate the OneWire object, issue a reset and a select/match command and resume checking for data using ds.read(), I immediately get a response that indicates data is ready, when it clearly cannot be (for example, 10ms into a 600ms conversion time)


What this seems to indicate to me is that you can only use the ds.read() to check for data status before issuing another reset and match on the bus. The reason this matters to me is that I have the possibility of multiple OneWire buses, and so don't want to use a single, global OneWire object, or allocate memory space for a whole array of OneWire objects. I would like to be able to reinstantiate the object and keep the scope to within the subfunctions.

For now, the solution appears to be enforcing a resolution-dependent gap in time between convert and read. This of course would not use a delay function, but would use other trickery to notify the read scratchpad function when data are ready. The key point is that the conversion and read are totally independent, in that they each reinstantiate a new OneWire object.

Colin
CuPID Controls :: Open Source browser-based sensor and device control
Interfaceinnovations.org/cupidcontrols.html
cupidcontrols.com

ColinR

  • Full Member
  • ***
  • Posts: 176
Re: Improved/optimized DS18B20/1Wire read
« Reply #6 on: October 08, 2014, 04:39:48 PM »
Thanks for the good work Colin, two questions

1. Would it be possible to initiate a reading, then put the Moteino into a deep sleep for a few seconds (to a few minutes),  wake up and retrieve the reading, transmit over RFM radio, then sleep again. I am thinking of conserving battery power while the measurement is being taken.

Thanks.

Now that I think more about this, it is certainly possible to sleep the MCU and Radio, as long as the DS18B20 is not parasite-powered. The datasheet says "Zero standby power required", so as soon as it is done with its conversion process, you could certainly wake back up and read it.

By default, this is the way my mote code is set up. It sleeps everything at the end of each loop for some set time. On the first loop, it would run the conversion process, and by the time it ran the loop again, it would be time to read the scratchpad. That should work well!

C
CuPID Controls :: Open Source browser-based sensor and device control
Interfaceinnovations.org/cupidcontrols.html
cupidcontrols.com

tve

  • Newbie
  • *
  • Posts: 14
Re: Improved/optimized DS18B20/1Wire read
« Reply #7 on: October 09, 2014, 02:14:48 AM »
I similarly didn't like the OneWire library for temperatures and derived my own, which uses 10-bit resolution and initiates a conversion on all sensors at the same time and then returns so the arduino loop can proceed. I use a fixed conversion timeout, but I don't think it matters that much if you return control anyway. It sucks if the processor is busy-waiting for 1000ms instead 600ms, but with 10 bits you're talking ~180ms plus/minus a few and there's no busy-wait anyway.
The library is most likely too idiosyncratic for others, but maybe someone else can derive some inspiration (I also looked at a bunch of others to arrive at my code). I noticed that I need to fix the indentation, ooops.
The code is at https://github.com/tve/widuino/tree/master/nodes/OwTemp

scottpenrose

  • Newbie
  • *
  • Posts: 13
Re: Improved/optimized DS18B20/1Wire read
« Reply #8 on: January 05, 2015, 06:16:44 PM »
Great code. Thanks. I wrote a state machine 2 years ago for DS18B20, but never maintained it very well. Better to start again.

There is code out there worth a look at as well - I used for another project DallasTemperature - http://milesburton.com/Dallas_Temperature_Control_Library

It does non blocking reads like this and also allows setResolution etc built in. Has been around for about 5 years.

Although it uses no delays - non blocking, the OneWire both these are dependent on, still use delays. They are small but have caused me problems in the past when doing very accurate time based readings.

ColinR

  • Full Member
  • ***
  • Posts: 176
Re: Improved/optimized DS18B20/1Wire read
« Reply #9 on: January 05, 2015, 06:28:36 PM »
I looked at the DallasTemperature code and mentioned it on my blog where I talk about this topic. I came to the conclusion that it was too much overhead for the tiny extra features I wanted out of it. I didn't realize there were additional delays elsewhere in OneWire though, so I'll take a look at that. It's been satisfactory running it non-blocking as I did in the first post.

I'll probably end up writing my own when I get around to using a bunch of the random 1Wire devices I have around here with micros.

C
CuPID Controls :: Open Source browser-based sensor and device control
Interfaceinnovations.org/cupidcontrols.html
cupidcontrols.com

scottpenrose

  • Newbie
  • *
  • Posts: 13
Re: Improved/optimized DS18B20/1Wire read
« Reply #10 on: January 07, 2015, 08:19:42 PM »
Yes that library is quite large. Flexible, but large. I think I will try your code out as an experiment as it is better fitness for purpose and the 328s are so small (I have been working a lot lately with ARMs with 64K, so back to 2K and 328s has been a reminder of small systems). Thanks again.

Scott

obstler42

  • Newbie
  • *
  • Posts: 12
Re: Improved/optimized DS18B20/1Wire read
« Reply #11 on: January 21, 2015, 03:30:44 PM »
hi,

I'm trying to get your improved code (code1 on top) running, but can't seem to get it to work. It gets the address just fine but is stuck in the while loop forever. Last serial output is:

dsaddress:28FF04B275040020,

and from adding more serial debug output I know for sure that it is stuck in the while loop:

Code: [Select]
	myds.write(0x44, 1);         // start conversion, with parasite power on at the end
while (!myds.read()) {
// do nothing
}

Using the <DallasTemperature.h> lib and the sample code on http://www.hobbytronics.co.uk/ds18b20-arduino works just fine (but inefficient)... do you have any ideas what could go wrong here?

thanks.

ColinR

  • Full Member
  • ***
  • Posts: 176
Re: Improved/optimized DS18B20/1Wire read
« Reply #12 on: January 26, 2015, 11:20:16 PM »
Not sure. Does the other code sample work? You probably want to use it anyway, since it's non-blocking. Let me have a think on that.

C
CuPID Controls :: Open Source browser-based sensor and device control
Interfaceinnovations.org/cupidcontrols.html
cupidcontrols.com

hexium

  • Newbie
  • *
  • Posts: 3
Re: Improved/optimized DS18B20/1Wire read
« Reply #13 on: February 17, 2015, 03:10:24 AM »
Hi and thanks for this great work. I have a simple (possibly noob) question:

- If the "raw" variable is declared as an (unsigned int) type, does this mean we cannot get negative values from this code?

Thanks!

ColinR

  • Full Member
  • ***
  • Posts: 176
Re: Improved/optimized DS18B20/1Wire read
« Reply #14 on: February 17, 2015, 09:52:30 PM »
Good question. The simple answer is no. From page four here in the datasheet, you can see that the temperature is contained in two bytes of twos complement: http://datasheets.maximintegrated.com/en/ds/DS18B20.pdf

Looking at the code here, however, I can see that there is not provision for negative temperatures, and you would indeed get strange results. I know I've written this code somewhere else before, but apparently it didn't make it over here. Oops, just found it. Here you go:

Code: [Select]
float calctemp(byte data[]) {
  float celsius;
 
  unsigned int TReading = (data[1] << 8) + data[0];
  unsigned int SignBit = TReading & 0x8000;  // test most sig bit
  if (SignBit) // negative
  {
    TReading = (TReading ^ 0xffff) + 1; // 2's comp
  }
  celsius = float(TReading)/16;
 
  if (SignBit){
    celsius = celsius * -1;
  }
  return celsius;
}
« Last Edit: February 18, 2015, 02:11:55 AM by ColinR »
CuPID Controls :: Open Source browser-based sensor and device control
Interfaceinnovations.org/cupidcontrols.html
cupidcontrols.com