coin-cell (CR2032) powered temperature sensor



  • Hi!

    I would like to share my experience with coin-cell (CR2032) powered temperature sensors:

    DSC_0970.jpg

    DSC_0971.jpg

    The reason for my use of a CR2032 coin cell is that I am really bad at building cases. So I wanted a sensor that will fit into a kinder-surprise egg.

    To maximize battery life I did some modifications to the pro micro clones:

    • I removed all the leds
    • and also the 3.3v-regulator
    • I used a digital pin for the voltage divider so I can turn the divider off
    • I changed the efuse value to 0x07 (BOD off)

    For easier connections I use digital pins to provide the DS18B20 temperature sensor with VCC and GND. I also got rid of the 4k7 resistor by using a modified version of the onewire.h library which uses the internal pullup instead.

    Temperature is transmitted every 5 minutes and battery level every hour.

    For the battery level I used software bounderies: 3.3V is 100%, 2.6V is 0%.

    If anyone is interested, here is the code. It is based on the old example sketch.

    #include <MySensor.h>
    #include <SPI.h>
    #include <DallasTemperature.h>
    #include <OneWire.h>
    
    #define ONE_WIRE_BUS 4 // Pin where dallase sensor is connected
    #define ONE_WIRE_GND 5
    #define ONE_WIRE_VCC 3
    #define BATT_IN_PIN A0
    #define BATT_VCC_PIN 6
    #define MAX_ATTACHED_DS18B20 16
    
    #define SLEEP_TIME 300000  // Sleep time between reads (in milliseconds)
    #define BATT_TIME 12  //when also BATT-LEVEL is reported
    
    #define BATT_100 3.3
    #define BATT_0 2.6
    
    OneWire oneWire(ONE_WIRE_BUS);
    DallasTemperature sensors(&oneWire);
    MySensor gw;
    float lastTemperature[MAX_ATTACHED_DS18B20];
    uint8_t numSensors = 0;
    boolean receivedConfig = false;
    boolean metric = true;
    // Initialize temperature message
    MyMessage msg(0, V_TEMP);
    uint8_t battReport = 0;
    int oldvalue = 0;
    
    void setup()
    {
      // Startup OneWire
    #ifdef ONE_WIRE_VCC
      pinMode(ONE_WIRE_VCC, OUTPUT);
      digitalWrite(ONE_WIRE_VCC, HIGH);
    #endif
    #ifdef ONE_WIRE_GND
      pinMode(ONE_WIRE_GND, OUTPUT);
      digitalWrite(ONE_WIRE_GND, LOW);
    #endif
    
      analogReference(INTERNAL);
      pinMode(BATT_IN_PIN, INPUT);
      pinMode(BATT_VCC_PIN, OUTPUT);
      digitalWrite(BATT_VCC_PIN, LOW);
    
      sensors.begin();
    
      // Startup and initialize MySensors library. Set callback for incoming messages.
      gw.begin();
    
      // Send the sketch version information to the gateway and Controller
      gw.sendSketchInfo("Temperature Sensor", "1.0");
    
      // Fetch the number of attached temperature sensors
      numSensors = sensors.getDeviceCount();
    
      sensors.setWaitForConversion(false);//saves a few mAs per read :-) to debug
    
      // Present all sensors to controller
      for (int i = 0; i < numSensors && i < MAX_ATTACHED_DS18B20; i++) {
        gw.present(i, S_TEMP);
      }
    }
    
    
    void loop()
    {
      // Process incoming messages (like config from server)
      //gw.process();
    
      // Fetch temperatures from Dallas sensors
      sensors.requestTemperatures();
    
      gw.sleep(750);//wait for conversion in sleep mode
    
      // Read temperatures and send them to controller
      for (int i = 0; i < numSensors && i < MAX_ATTACHED_DS18B20; i++) {
    
        // Fetch and round temperature to one decimal
        float temperature = static_cast<float>(static_cast<int>((gw.getConfig().isMetric ? sensors.getTempCByIndex(i) : sensors.getTempFByIndex(i)) * 10.)) / 10.;
    
        // Only send data if temperature has changed and no error
        if (lastTemperature[i] != temperature && temperature != -127.00) {
    
          // Send in the new temperature
          gw.send(msg.setSensor(i).set(temperature, 1));
          lastTemperature[i] = temperature;
        }
      }
      if (++battReport == BATT_TIME) {
        digitalWrite(BATT_VCC_PIN, HIGH);
        delay(1);
        int value = analogRead(BATT_IN_PIN);
        digitalWrite(BATT_VCC_PIN, LOW);
        if (value != oldvalue)
        {
          int percent = (( value * 3.36e-3) - 2.6) / (3.3 - 2.6) * 100;
          percent = (percent > 100) ? 100 : percent;
          percent = (percent < 0) ? 0 : percent;
          gw.sendBatteryLevel(percent);
        }
        oldvalue = value;
        battReport = 0;
      }
      gw.sleep(SLEEP_TIME);//wake on interrupt or after sleep-time
    }
    

    My experience so far:
    On the good side: the first sensor I built started operation on May 23 (about 3 months ago) and is still working great with the same coin cell. I first hoped for a month or two because of the small capacity of a CR2032 coin cell, so my expectation is already exceeded.

    On the bad side: Some other sensors I built later had problems. They worked well with the fresh battery but stopped working after a day or two. After this time they went into a boot loop (serial monitor reads "sensor started, id ..." over and over again) so apparently the startup of the radio somehow triggers a reboot.
    As a modification to prevent this I resoldered the input capacitor of the raw-pin (which is unused) directly to the radio.
    For most sensors this worked fine.
    But one sensor did keep rebooting. I had to change it to a 2xAA-powered sensor. I later used the same CR2032 coin cell, which powered a reboot loop for about 24 hours to power another, newly built sensor and it worked on that one.

    I don't know what the reason for this is, but it has to have something to do with the voltage drop made by the radio. Either some radios are bad and drawing a higher current or some of the pro micro clones are bad and maybe not accepting the changed efuse values.

    Has anyone had similar issues? Are there counterfeit atmega328 around which do not work up to specifications?


  • Hero Member

    @fleinze said:

    For easier connections I use digital pins to provide the DS18B20 temperature sensor with VCC and GND.

    Sounds like a clever idea.

    Measuring your currents would be the best way to narrow down where the problem is.

    Maybe some of your radios are using different counterfeit NRF chips, and the radio chip in your boot-looping sensor may, for example, be drawing more current during transmit, or perhaps drawing too much current during sleep or just generally,, causing voltage to drop because of internal resistance, and the pro mini to drop out. After radio drops out, voltage rises again, and pro mini reboots.

    Or, perhaps some other component in your boot-looping sensor is drawing higher current, resulting in the same cause-effect outcome after your battery drains enough.

    Or maybe your boot-loop battery is just weak to begin with as compared to your other batteries.

    Make sense?

    Eventually, I would guess most coin cell sensors are going to boot-loop, triggered by sending a packet, whenever their batteries get drained enough, because of the high internal resistance in the coin cell battery. So, just measuring battery voltage, by itself, without a peak current to drop it, isn't going to tell you how close your sensor is to cutting out.



  • Just a quick Update on my coin-cell sensors:
    I still did not figure out why one sensor did not work because I lack of the needed measurement-equipment 😞

    But I figured out something else: The voltage of a CR2032 battery has a very strong temperature-dependence. So for calculating the battery-level I probably need to calculate a temperature-normalized voltage-value.


  • Contest Winner

    I'm following this topics with much interest. It would be really great if it's possible to power a sensor with a CR2032 cell.

    For my two AA powered Soil Moisture sensor, I just put a transistor (BC547b I think, they came with the Arduino starter kit) between the soil moisture sensor circuit and one in the circuit for battery power measurement. As I only have basic knowledge of electronics, I'm not sure if this will save battery power. But I switch on the transistors whenever I have to read the sensors. Wait a little bit so that the sensors have enough time to give a good reading. And then I switch the transistors off. I only need two extra outputs this way and I'm using A0 and A1 as digital outputs.

    My guessing is, that I will save battery power by turning the power on the sensors on, only when I need to read them.



  • I think you can just use a arduino-pin to power the soil sensor and the battery power measurement.
    I did this with the battery power measurement even if it only saves about 2uA, probably in the magnitude of the self-discharge rate.


  • Contest Winner

    @fleinze That makes actually more sense. But if I'm using a 3.3V step-up regulator I won't be able to feed the measuerment circuit from a digital pin right? Would a transistor be a good idea?



  • That depends: Does the step-up just feed the measurement or also the arduino?
    Can you post schematics?

    As long as you stay below 20mA you can use the digital pins to power everything.

    Be aware that a step-up regulator has low efficency at low currents, so it is possible that it actually shortens battery life instead of increasing it.


  • Contest Winner

    The step-up feeds the mini pro 3.3V. I measure the battery level directly from the battery.


  • Hero Member

    Did you ever solve your boot-loop problem?



  • Kind of. I used a different Arduino and now it works.



  • @TheoL: The step-up regulator will most likely drain your battery very quickly. It uses a lot of current in "standby-mode" (when Arduino is powered down)!

    You can power an Arduino and most sensors directly from battery but you need to disable BOD (brown-out-detection). To do this you need an programmer ("Arduino as ISP"-sketch is sufficient) and set the extended-fuse to 0x07. This can be done with the Arduino-IDE, but you need to edit the boards.txt file.



  • what about your nodes after several months ?



  • @carmelo42 I just changed coin-cell on one of my sensors. It lasted since for 10 months, this is ok for me.



  • Measuring the voltage proofed to be worthless for this kind of battery. It is more dependent of the temperature than from anything else.



  • @fleinze said:

    @carmelo42 I just changed coin-cell on one of my sensors. It lasted since for 10 months, this is ok for me.

    10 months ? it's perfect 🙂

    Can you provide the modified version of the library to avoid using 4.7k resistor for the Dallas sensor ?

    What is for the resistor on the pic ? for the voltage mesurement ?



  • @carmelo42

    The no-resistor-library can be found here:
    https://wp.josh.com/2014/06/23/no-external-pull-up-needed-for-ds18b20-temp-sensor/

    The resistors (there are two but the other one is barely visible) are for voltage-measurement. In a later version I got rid of them using this resistor-less method of measurement:
    http://provideyourown.com/2012/secret-arduino-voltmeter-measure-battery-voltage/



  • Just found this post https://www.hackster.io/Talk2/temp-and-humidity-sensor-with-a-cr2032-for-over-1-year-580114 showing some details about using a CR2032 to power a sensor node. By my calculations, if the author removed the LED at all the solution would last for over 2 years!



  • @fleinze said:

    @carmelo42

    The no-resistor-library can be found here:
    https://wp.josh.com/2014/06/23/no-external-pull-up-needed-for-ds18b20-temp-sensor/

    The resistors (there are two but the other one is barely visible) are for voltage-measurement. In a later version I got rid of them using this resistor-less method of measurement:
    http://provideyourown.com/2012/secret-arduino-voltmeter-measure-battery-voltage/

    great 🙂

    Did you change the bootloader ? Which one did you use ?


  • Hardware Contributor

    Hello,

    there are a few improvements you can do to make your CR2032 nodes last longer:

    • add a capacitor of 100-200µF in parallel with your battery. Ceramic is better, but I have no problem with nodes using electrolytic capacitors. This will help when there is a peak power consumption from the radio. If you do not put one, voltage will drop quickly and that's probably what is triggering reboot loop on one of your nodes: maybe radio is less efficient and needs to resend more messages. Or maybe your BOD is not updated so it resets when voltage drops at 2.7V...
    • in your code, add a sleep command between message sending to give time for your cell to rest, and for the capacitor to recharge. Do it also at the beginning of presentation method and between each message sending in presentation.
    • use a better sensor that can accept a lower voltage, they are more expensive that DS18 but they can work down to 2V and use much less current: BMP180/280 if you want to measure only temperature (with barometer as extra), SI7021 for temp/hum, BME280 for temp/hum more expensive but better.
    • do not use voltage divider at all, you don't need one as you can just get Vcc from the A0 pin (with it I have some change of voltage related to temperature but not as wild as you seem to get. Maybe it's related to the DS18 measurement also ?)

    With these changes and a si7021 breakout board from which I removed the voltage regulator (it's not consuming much, but without it is even better), I can send every minute, flash a led and I hardly see any drop in voltage after a few weeks of running.



  • @Nca78 said:

    add a capacitor of 100-200µF

    I will try this, thanks! Currently I use the 10uF capacitor which is on the arduino pro minis on the raw pin side.

    in your code, add a sleep command between message sending

    How long do you sleep? In normal operation there is only one send command per loop, I only send battery level once every hour. I try to read vcc after sending the temperature so the battery is under some load when measuring.

    use a better sensor that can accept a lower voltage

    I already built one with a Si7021 sensor. But I ran out of CR2032 so I powered it using two AA cells. I should solder it back to coin cell now I got some.

    you can just get Vcc from the A0 pin

    I don't know this method do you have a link or can you explain it?



  • @carmelo42 sorry I somehow missed your post. I use the standard-bootloader as I did not get Optiboot to run on the 3.3V/8MHz pro minis. I set the extended fuse to 0x07 (BOD disabled) by editing boards.txt.



  • @fleinze said:

    @carmelo42 sorry I somehow missed your post. I use the standard-bootloader as I did not get Optiboot to run on the 3.3V/8MHz pro minis. I set the extended fuse to 0x07 (BOD disabled) by editing boards.txt.
    thanks !

    is is a bit confusing for me :

    • we can burn the bootloader from the Arduino IDE : are the fuses written at this moment ?
    • we can upload a sketch with the arduino IDE : are the fuses written at this moment ?
    • with my researches, I found that for disabling BOD was possible with 0xFF value for efuse ?

    I have some lifetime issues with my CR2032 sensor .. and I suspect the fuses are not correctly set ...

    If you can light up my mind it will be perfect 🙂



  • @carmelo42 The fuses only are written when you burn the bootloader, not when uploading the sketch. The Arduino IDE takes the fuse bytes from the boards.txt files.
    It would also be possible to just set the fuse bytes by using the avrdude-program from a command line, but you will still need to use a programmer.
    The extended-fuse setting 0xFF is the same as 0x07 as only the last 3 bits of this bytes are used. Both values will give you BOD disabled.



  • I just added an 100uF capacitor to one of my sensors and put an old battery in. Let's see how much more life I can get out of this battery now.



  • @fleinze
    Hi, do you think it's possible to connect de Dallas sensor to pin 7, 8 and 9 of the arduino ?

    Like this :

    #define ONE_WIRE_BUS 8 // Pin where dallase sensor is connected
    #define ONE_WIRE_GND 9
    #define ONE_WIRE_VCC 7
    

    I have tried, and the temperature is always 85°C ...


  • Hardware Contributor

    Hello @carmelo42, pin 9 is supposed to be connected to the NRF24 (CE / Chip Enable) so it cannot work as ground for the sensor as it will be high (and so = to Vcc) most if not all of the time.



  • @fleinze said:

    I just added an 100uF capacitor to one of my sensors and put an old battery in. Let's see how much more life I can get out of this battery now.

    Very interest work.
    Is it still working?



  • There is a TI paper about adding a big capacitor in series with the coin-cell battery to minimize the voltage drop during high-current peaks: http://www.ti.com/lit/wp/swra349/swra349.pdf, also another interesting article related to low-power and CR2032: http://www.low-powerdesign.com/121312-article-extending-battery-life.htm



  • @flopp I added the capacitor and put the old battery in. By old battery I mean the one I had replaced a week earlier for a new one. Sensor running smooth since, so great success!





  • The DS18B20+ is not a great sensor for the battery powered nodes. It has a huge conversion time comparing to others.
    9-bit resolution | 93.75 ms | 0.5°C
    10-bit resolution | 187.5 ms | 0.25°C
    11-bit resolution | 375 ms | 0.125°C
    12-bit resolution | 750 ms | 0.0625°C
    The last column is the sensor's precision. So unless you're OK with a 0.5°C steps, the simple usage of DS18B20+ will kill your battery a lot faster.
    Don't use DHT22, that one is very slow too. BME280 does have a barometric pressure sensor but it's not very fast either.
    The best sensor for battery powered nodes is the the Si7021 which is rather old, but is the fastest.



  • I put the arduino to sleep during the conversion time, so this is not a big issue. But I agree that the DS18B20 is not the best, but it is widely available. For newer nodes I try to use the Si7021 where possible.



  • @fleinze said:

    I put the arduino to sleep during the conversion time, so this is not a big issue. But I agree that the DS18B20 is not the best, but it is widely available. For newer nodes I try to use the Si7021 where possible.

    It goes without saying that the nodes are in sleep mode between reports. However, the conversion time has a great impact in the cycle math. I won't say that pulling the readings 10 times faster will give you 10 times more time on battery but if you'll get 5 times more time then it's something. Not to mention that the Si7021 works down to 1.9V unlike the DS18B20+ which works down to only 3.0V.



  • @fleinze Great work with the CR2032 coin cell temp. sensor. I am trying to do a similar exercise with Si7021 sensors but getting into issues while coding. The examples from other posts around Si7021 adapted for latest MySensor lib seem to get stuck in my IDE giving lot of errors. I have tried getting all the required Header files but still get stuck due to some random error. Is it possible for you to share your code based on CR2032 temp node running on Si7021? Thanks....



  • @tango156157, I sometimes get similar errors while compiling the sketches. Mostly because I am still using the MySensors 1.5 library for my legacy network, 2.1.0b for the RFM69 network (working RSSI report) and 2.1.1 for the LoRa network.
    Mostly when dealing with MySensors 1.5 and/or older sensors libraries, I find the newer Arduino version to pop all sort of errors. But then I fire up one of the older Arduino IDE versions I have, 1.6.5, 1.6.9 and so on. Eventually one of them compiles the sketch without errors while the newer version don't work. This is far from being a solid solution for these errors, but it gets the job done.
    You should try that too, especially if you're using older libraries for your sensors.
    You can find them here: https://www.arduino.cc/en/Main/OldSoftwareReleases#previous



  • Hi,

    this is my code working with 2.1 version. The code is not tidied up, please excuse.

    /*
       MySensors-Node for DS18B20 Temperature-Sensors.
       Mysensors.Library-Version 1.6
    */
    
    #define MY_RADIO_NRF24
    
    #define MY_TRANSPORT_UPLINK_CHECK_DISABLED
    
    //#define MY_DEBUG
    
    
    #include <MySensors.h>
    #include <SPI.h>
    #include <Wire.h>
    #include <SI7021.h>
    
    #define BATT_SENSOR
    //#define VCCGND_PINS
    
    #ifdef VCCGND_PINS
    const uint8_t GND = A2;
    const uint8_t VCC = A3;
    #endif
    
    #ifdef BATT_SENSOR
    #define REPORT_VOLTAGE
    #endif
    
    const unsigned long SLEEP_TIME = 300000; // Sleep time between reads (in milliseconds)
    
    const uint8_t TEMP_TIME = 12; //at least every nth time battery is reported
    const uint8_t HUM_TIME = 12;
    const uint8_t BATT_TIME = 12; //when also BATT-LEVEL is reportet
    const float BATT_100 = 3; //3.3V for CR2032, 3V for 2xAA
    const float BATT_0 = 2.2;
    
    SI7021 sensor;
    
    float lastTemperature, lastHum;
    uint8_t lastTempSent = 0;//, lastHumSent = 0;
    uint8_t numSensors = 0;
    boolean receivedConfig = false;
    boolean metric = true;
    // Initialize temperature message
    MyMessage msgTemp(0, V_TEMP);
    MyMessage msgHum(0, V_HUM);
    #ifdef REPORT_VOLTAGE
    MyMessage msgBatt(1, V_VOLTAGE);
    #endif
    #ifdef BATT_SENSOR
    uint8_t battReport = BATT_TIME - 1; //First report at startup
    long oldvalue = 0;
    #endif
    
    
    void setup()
    {
    
      #ifdef VCCGND_PINS
      pinMode(VCC, OUTPUT);
      digitalWrite(VCC, HIGH);
      pinMode(GND, OUTPUT);
      digitalWrite(GND, LOW);
      #endif
      
      if (!sensor.begin()) {
        Serial.println("No Sensor found!");
        while (true);
      }
      
    
    }
    
    void presentation() {
    
      // Send the sketch version information to the gateway and Controller
      sendSketchInfo("TempHumSi7021", "0.1a");
    
      // Present all sensors to controller
      present(0, S_HUM);
      //present(1, S_HUM);
    #ifdef REPORT_VOLTAGE
      present(1, S_MULTIMETER);
    #endif
    }
    
    
    void loop()
    {
      //delay(2000);//for sensor to start up
      boolean tempsent = false;
    
      // Fetch temperatures from Dallas sensors
      si7021_thc temphum = sensor.getTempAndRH();
    
      // Read temperatures and send them to controller
      // Fetch and round temperature to one decimal
      float temperature = (float)(temphum.celsiusHundredths) / 100.0;
      float humidity = (float)(temphum.humidityPercent);
      // Only send data if temperature has changed and no error
      if ((lastTemperature != temperature) || lastHum != humidity || (++lastTempSent >= TEMP_TIME)) {
        // Send in the new temperature
        send(msgTemp.set(temperature, 1));
        send(msgHum.set(humidity, 1));
        lastHum = humidity;
        lastTemperature = temperature;
        lastTempSent = 0;
        tempsent = true;
      }
    
    
    #ifdef BATT_SENSOR
      if (++battReport >= BATT_TIME && tempsent) {
    
        //gw.sleep(10);
        long value = readVcc();
    
    
        if (value != oldvalue) {
          int percent = (( (float)(value) / 1000 ) - BATT_0) / (BATT_100 - BATT_0) * 100;
          percent = (percent > 100) ? 100 : percent;
          percent = (percent < 0) ? 0 : percent;
          sendBatteryLevel(percent);
    #ifdef REPORT_VOLTAGE
          send(msgBatt.set((float)(value) / 1000, 2));
    #endif
        }
        oldvalue = value;
        battReport = 0;
      }
    #endif
      sleep(SLEEP_TIME);//wake on interrupt or after sleep-time
      //delay(2000);//for sensor to start up
    }
    
    long readVcc() {
      // Read 1.1V reference against AVcc
      // set the reference to Vcc and the measurement to the internal 1.1V reference
    #if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
      ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
    #elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
      ADMUX = _BV(MUX5) | _BV(MUX0);
    #elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
      ADMUX = _BV(MUX3) | _BV(MUX2);
    #else
      ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
    #endif
    
      delay(2); // Wait for Vref to settle
      ADCSRA |= _BV(ADSC); // Start conversion
      while (bit_is_set(ADCSRA, ADSC)); // measuring
    
      uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
      uint8_t high = ADCH; // unlocks both
    
      long result = (high << 8) | low;
    
      result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
      return result; // Vcc in millivolts
    }```


  • @fleinze thanks for sharing your code for Si7021 sensor. I tried compiling but getting below error. Not sure if I am using incorrect header file for Si7021 sensor. I am using MySensors ver 2.1.0 for compiling. Any view? Thanks

    C:\Users\abc\AppData\Local\Temp\arduino_modified_sketch_641038\sketch_may14a.ino: In function 'void loop()':
    
    sketch_may14a:91: error: 'si7021_thc' was not declared in this scope
    
       si7021_thc temphum = sensor.getTempAndRH();
    
       ^
    
    sketch_may14a:91: error: expected ';' before 'temphum'
    
       si7021_thc temphum = sensor.getTempAndRH();
    
                  ^
    
    sketch_may14a:95: error: 'temphum' was not declared in this scope
    
       float temperature = (float)(temphum.celsiusHundredths) / 100.0;
    
                                   ^
    
    Multiple libraries were found for "SI7021.h"
     Used: C:\Users\abc\Documents\Arduino\libraries\SI7021
     Not used: C:\Program Files (x86)\Arduino\libraries\SI7021
    exit status 1
    'si7021_thc' was not declared in this scope
    
    Insert Code Here
    


  • @tango156157
    Look you have more than one library for si7021.
    Move one library and then try again.
    If that doesn't help.

    Move back the first library and move the second one, try again and see if that helped


 

449
Online

7.9k
Users

8.7k
Topics

93.7k
Posts