Oregon V2.1 Sensor Emulation that works with Oregon Base Station? [SOLVED]

I am trying to create a Uno-based project to replace a dead Oregon Scientific temperature and humidity sensor (THGR122NX). There are a number of sketches out there for doing this emulation--but none that I've found that can actually transmit data to an Oregon Scientific weather station that uses the version 2.1 protocol (in my case a BAR608HGA).

I can use the WlessOregonV2.h library (and others) and can transmit data to a 433 Mhz receiver fine. But it doesn't register on the weather station.

Is anyone aware of any sketches or libraries that can do this? The closest I've found is this:Oregon Scientific Protocol v3 Wave Height and Water Temperature Sensor But it is based on V3 of the Oregon Scientific protocol, and I need Version 2.1.

I worked with the Oregon V2.1 sensors a number of years ago and found that the weather station receiver demanded very precise bit timing, as well as all message bits and checksum set correctly. I finally decided that it was not worth the effort, since the receiver was so dated.

The protocols used by other weather sensor companies (like Acurite) are much more forgiving, so I still use one of those as a display.

If you have a functioning V2.1 weather sensor, use a good oscilloscope or logic analyzer to measure the timing parameters as accurately as possible, and make sure that all bits in the message you construct on the Arduino are correct.

Thanks for this info. Right now I don't have working v2.1 sensor, but am looking for one at a reasonable price. If I can find one, I'll look into this more, or just use it with the base station.

I was hoping someone else had done the hard work of figuring out how the make the emulated sensors work with the station.

I looked on eBay and the prices for those V2.1 sensors seem outrageous (much higher than the original purchase price). It might be cheaper and certainly easier just to buy the V3.0 base station, since you have the code to imitate the protocol.

I quite agree with @jremington . The V2.1 protocol requires microsecond-precision timing, exact Manchester encoding, correct preamble sequences, and precise checksum calculations. Even minor deviations that generic 433MHz receivers accept will cause your base station to ignore the signal entirely. Finding a replacement THGR122NX or compatible sensor would be the quickest path to getting your weather station working again.
However, if you insist on making the emulation work you may have to invest a significant amount of time with little or no guarantees. I can assist you with a systematic testing sketch to help identify your base station's exact timing requirements. This sends test packets that should register for instance as Channel 1, 20.5°C, 65% humidity. Note that even with correct timing, issues with checksum, channel encoding, or sensor ID format may prevent success. Without an oscilloscope for comparison, this becomes largely trial and error. Another option to consider( If you are on just trying out a DIY project is building an independent weather display system. Since your Arduino successfully transmits to generic 433MHz receivers, you can create a transmitter with DHT22 sensor and a separate receiver with LCD display. This provides immediate results and allows for additional features like data logging and WiFi connectivity.

Thanks for these suggestions--and offer to help. I agree it might just be easier to find a compatible replacement sensor or change stations. I don't have an oscilloscope, and am not sure how much effort I want to put into this. I may mess around with this more in the fall or winter when I have more time.

I am getting close to marking this one "solved" and will provide details when I do. Before then, can anyone explain what is going wrong with the attached sketch and how to fix it?

The sketch is supposed to extract the tenths value of a float (whether positive or negative) as a positive integer. It loops through floats beginning with 10.1 and increasing by 10.1 each loop. It works as expected up to 70.7, but then is 1 too low though 131.3 (e.g., 8 is extracted as the decimal value for 90.9)

The problem seems to be in the casting, But how to fix? Thanks in advance for any ideas.


```cpp
float t = 10.1;

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

void loop() {
  uint8_t t_sign = t < 0;
  uint8_t t_deci = (int)(t * (t_sign ? -10 : 10)) % 10;
  Serial.print("t: ");
  Serial.print(t);
  Serial.print("  t*10: ");
  Serial.print (t * 10);
  Serial.print("  (int)(t*10): ");
  Serial.print ((int) (t * 10));
  Serial.print("  ");
  Serial.print(t_deci);
  Serial.println(" = decimal value extracted");
  t = t + 10.1;
  if (t  > 201.0) t = 10.1;
  delay(3000);
}
```

Most values can't be represented exactly in a 4 byte float. 80.8 can't. It's actually 80.799995. Ish. 10 times that is 807.999938. Ish.

Prove it to yourself by printing your floating point values with 6 decimal places.

t: 10.100001  t*10: 101.000000  (int)(t*10): 101  1 = decimal value extracted
t: 20.200000  t*10: 202.000000  (int)(t*10): 202  2 = decimal value extracted
t: 30.300001  t*10: 303.000000  (int)(t*10): 303  3 = decimal value extracted
t: 40.400001  t*10: 404.000000  (int)(t*10): 404  4 = decimal value extracted
t: 50.500000  t*10: 505.000000  (int)(t*10): 505  5 = decimal value extracted
t: 60.599998  t*10: 606.000000  (int)(t*10): 606  6 = decimal value extracted
t: 70.699996  t*10: 707.000000  (int)(t*10): 707  7 = decimal value extracted
t: 80.799995  t*10: 807.999938  (int)(t*10): 807  7 = decimal value extracted
t: 90.899993  t*10: 908.999938  (int)(t*10): 908  8 = decimal value extracted
t: 100.999992  t*10: 1009.999938  (int)(t*10): 1009  9 = decimal value extracted
t: 111.099990  t*10: 1110.999877  (int)(t*10): 1110  0 = decimal value extracted
t: 121.199989  t*10: 1211.999877  (int)(t*10): 1211  1 = decimal value extracted
t: 131.299987  t*10: 1312.999877  (int)(t*10): 1312  2 = decimal value extracted
t: 141.399993  t*10: 1414.000000  (int)(t*10): 1414  4 = decimal value extracted
t: 151.500000  t*10: 1515.000000  (int)(t*10): 1515  5 = decimal value extracted
t: 161.600006  t*10: 1616.000000  (int)(t*10): 1616  6 = decimal value extracted
t: 171.700012  t*10: 1717.000122  (int)(t*10): 1717  7 = decimal value extracted
t: 181.800018  t*10: 1818.000244  (int)(t*10): 1818  8 = decimal value extracted
t: 191.900024  t*10: 1919.000244  (int)(t*10): 1919  9 = decimal value extracted

Adding a fudge factor (in your case, maybe 0.001) to a floating point result before truncating or casting to int can mask such problems. But it's just a mask.

You will need to define what you mean by the "tenths value". Is the "tenths value" of 3.199999 better represented by 1 or 2?

Arguing that "2" is the correct answer, this code demonstrates one method of extracting a rounded "tenths" digit from a positive float value:

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(1);
  Serial.println("Starting");
  for (int i=0; i<20; i++) {  //20 random test values
    float x = float(random(0,10000))/100.0; //a number between 0 and 100, with tenths
    int tenths = int (x*10  + 0.5)%10;
    Serial.print(x,2);
    Serial.print(", tenths = ");
    Serial.println(tenths);
  }
}

void loop() {
}

Great! Thanks to both of you. I should have thought about the way floats are represented,

I’ve just changed this line to do the rounding by adding 0.5 and it works fine:
uint8_t t_deci = (int)(t * (t_sign ? -10 : 10) + 0.5) % 10

That is all I need as I am just trying to get the nearest tenths value.

I am marking this one solved–not because I solved it but because after my initial post, I found someone already had. In this excellent blog: Simulating an Oregon Scientific v2.1 remote temperature sensor | Stephen's Site Stephen Humphries describes how he got an Arduino to send data to his Oregon v2.1 weather station. He used a logic analyzer to figure out the data coding and get the signal timing right.

Since I don’t have a logic analyzer (or oscilloscope) I took him up on the suggestion in his sketch that the signal timing of the board could be calibrated by trial and error. I wrote the sketch below, based on his, to do just that (and more). It cycles through a pulse length he calls SHORT and when the value that works is found, it displays it on the weather station. It does this in less than a minute, taking advantage of the fact that Oregon receivers can receive transmissions continuously while searching for sensors.

Since I found a new old-stock Oregon temperature and humidity sensor for a reasonable price, I was able to decode the temperature and humidity trend nibbles using the OregonReciverDUMP sketch referenced in my sketch.

I also found that the nominal 40 second transmission interval for my Oregon sensor isn't 40 seconds at all. It is 39, 41, and 43 seconds for channels 1, 2, and 3, respectively. These values are important for keeping the sensor and receiver synced.

The sketch below does a of couple things. In CALIBRATE mode it finds the value of SHORT that works for your board. When you use that value in DEMONSTRATE mode, it displays dummy temperature data from -99.9 to 199.9 C and humidity data from 0 to 99%. It also cycles through the trend flags for temperature and humidity, and the low battery warning.

I've used this sketch on 3 different boards, and with the unique calibration value for each board, the transmission to the Oregon weather station is reliable--both at room temperature and in my freezer (at -20 C).

Let us know how it works if you try it out with your own board and weather station.

/*
  OSTXCaliDemo.ino, a sketch to calibrate and demonstrate an Arduino-based temperature/humidity sensor that can communicate
  with Oregon Scientific weather stations using the v2.1 protocol.  The sketch emulates an Oregon Scientific THGR122NX (1D20)
  sensor and operates in two modes:

  1. Calibrate--(default setting) determines the proper signal timing for the board used to drive the 433.92 MHz transmitter
  and displays the results on the Oregon Scientific weather station as humidity (first 2 digits) and temp (last digit)

  2.  Demonstrate—demonstrates the transmission of simulated temperature and humidity data and trend flags to the weather
  station using the signal timing found in calibration mode

  Hardware needed: Arduino Uno or other board, a 433.92 MHz transmitter, and a weather station that uses the Oregon Scientific
  Version 2.1 protocol and displays temperature and humidity (Model BAR608HGA was used for development and testing.)  While
  not required, another board and a 433.92 MHz receiver running the OregonReceiverDUMP sketch found here:
  https://github.com/sfrwmaker/WirelessOregonV2 is very helpful to see the hex data being sent (turn timestamp on).

  Sketch was created by Chris Loreti in October 2025, based the work and sensor.ino sketch of Stephen Humphries. See:
  https://shumphries.ca/blog/2022/12/04/oregon-scientific

   LICENCE

   Copyright Ā© 2022 Stephen Humphries

   Permission is hereby granted, free of charge, to any person obtaining a copy
   of this software and associated documentation files (the "Software"), to deal
   in the Software without restriction, including without limitation the rights
   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   copies of the Software, and to permit persons to whom the Software is
   furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in all
   copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   SOFTWARE.
*/
//    1. Define the mode to run in, CALIBRATE or DEMONSTRATE, by commenting out the other #define below
//    Use CALIBRATE (default) to find the SHORT value that works for your board with your Oregon weather station
//    Use that SHORT value in the sketch in DEMONSTRATE mode to see dummy data and trends on your station
//    2. Be sure receiver channel is set to "1" (or adjust data[4] to match), temperature units are in C, and receiver is in
//    "search" mode (CALIBRATE works only during the 3 minutes the weather station is searching for sensors).
//    Removing and replacing the batteries is the best way to trigger search mode when changing channels.

#define CALIBRATE //will display the values of SHORT that will work for your board (10 * humidity + temp C) on the weather
//station; the serial monitor will also show the current value of SHORT being tested

//#define DEMONSTRATE //substitute the value (or midpoint value) of SHORT found above in the "int SHORT =" statement below
// Temperature values from -99.9 C to 199 C and humidity values from 0 to 99% will be displayed; trend values will be
// displayed based on the ones value of the humidity for the temp trend/battery nibble, and the humidity trend nibble

#define TX_PIN 8 // Output for the 433.92 Mhz modulator (connect to D8 on Uno to avoid SPI)
//#define TX_PIN 12 // Output for the 433.92 Mhz modulator (connect to D12 on Mega 2560 = D6 on ESP8266 NodeMCU 1.0)

int SHORT = 481; // Half of 1/1024 (which is about 488 us), REPLACE WITH THE MIDDLE VALUE YOU FOUND IN CALIBRATE MODE!
//SHORT values found for my boards: 481 for Uno, 482 for Mega2560, 487 for NodeMCU 1.0, // All worked at -20C (and at +/- 1) of values shown

#define SUM_MASK 0xfffe0 // Only some nibbles are included in the checksum and CRC calculations
#define CRC_MASK 0xff3e0
#define CRC_IV 0x42 // ĀÆ\_(惄)_/ĀÆ (see the blog post for details)
#define CRC_POLY 0x7 // CRC-8-CCITT

// Example transmission data
// - Bytes are transmitted in order, small nibble first
// - Nibbles are transmitted LSB-first
// - Nibble descriptions in this example are large nibble first, to align with the byte-wise representation
// - Sensor ID 1d20, Channel 1, Rolling ID bb, Battery low, Temperature 22.7°C, Humidity 30%
// uint8_t data[] = {
//   0xff, // Preamble (16 ones (transmitted as 32 bits, alternating 01))
//   0xff, // Preamble
//   0x1a, // Sensor ID (1d20) / Sync (0xa)
//   0x2d, // Sensor ID
//   0x10, // Channel (1=0x1, 2=0x2, 3=0x4) / Sensor ID
//   0xbb, // Rolling ID (randomly generated on startup in original sketch, changed to fixed value for this one)
//   0x7c, // Temperature, 10^-1 / Battery low (low battery warning is set by bit 4; see below for explanation of other bits)
//   0x22, // Temperature, 10^1 / Temperature, 10^0
//   0x00, // Humidity, 10^0 / Temperature sign (largest 2 bits, 0x0 for +ve, 0x8 for -ve) | Temperature 10^2 (smallest 2 bits)
//   0x83, // Unknown / Humidity, 10^1 / See below for explanation of "Unknown" (humidity trend nibble)
//   0x4a, // Checksum (simple sum)
//   0x55, // Postamble (CRC checksum)
// };
uint8_t data[] = { // Data frame, initialized with the parts that never change
  0xff,            // Preamble
  0xff,
  0x1a,            // Sync nibble and sensor ID
  0x2d,
  0x10,            // channel 10 = 1, 20 = 2, 40 = 3
  0xbb,            // keep rolling code fixed (randomization commented out below) unlike actual Oregon sensor
  0x00,            // T Trend and Battery Condition Flags:
  // 00 - 03 battery OK: 00 = T flat, 01 = T up, 02 = T down, 03 no trend arrows; 04 sets the low battery flag bit so
  // 04 - 07 = low battery: 04 = T flat, 05 = T up, 06 = T down, 07 = no trend arrows (THGR122NX sensor sets the 08 bit at
  // sensor startup for 1/2 hour. then reverts to 0; this has no effect on the battery and trend flags)
  0x00,
  0x00,
  0x80,           // H Trend Flags:0x80(=0x00) = H flat, 0x10(=0x90) = H up, 0x20(=0xa0)= H down, 0x30 = no trend arrow
  // (unclear what the 4 and 8 bits are for, but the actual Oregon sensor uses them; they do not affect the display)
  0x00,
  0x00,
};
unsigned long currentMillis = 0;
unsigned long LastTXMillis = 0;
unsigned long TXinterval = 38350; //TX interval for Ch 1 = 39 secs; reduce by 650 ms to account for millis pause while interrupts are off
float tRaw = -99.9;  //starting value for temperature C, in demo mode (or float temperature from a real temp sensor if used)
float hRaw = 0.0;    //starting value for humidity in demo mode (or float humidity from a real humidity sensor if used)
int  SHORTstart;  //initial value of SHORT for calibrating board;
int SHORTend;  //greatest value of SHORT for calibrating board;
int dataLen = 12;
void setup() {
  Serial.begin(115200);
  pinMode(TX_PIN, OUTPUT);
  /*  code below commented out because fixed rolling code selected in data frame (==>same rolling code each startup)
    randomSeed(analogRead(0));
    data[5] &= 0x00; data[5] |= (random(256) & 0xff); // Rolling ID
  */
  switch (data[4]) {
    case 0x20:
      TXinterval = 40350; //TX interval for Ch 2 = 41 secs; reduce by 650 ms to account for millis pause while interrupts are off
      break;
    case 0x40:
      TXinterval = 42350; //TX interval for Ch 3 = 43 secs; reduce by 650 ms to account for millis pause while interrupts are off
      break;
  }
#ifdef CALIBRATE
  TXinterval = 2000;  //increase SHORT value every 2 seconds and send to Oregon receiver. Receiver MUST be in SEARCH mode!
  SHORTstart = 475;  //reduce this value if calibration doesn't work
  SHORT = SHORTstart;
  SHORTend = 495;  //increase this value if calibration doesn't work
  tRaw = 0.0 + float(SHORTstart % 10);
  hRaw = SHORTstart / 10;
#endif
}
void loop() {
  currentMillis = millis();
  if (currentMillis - LastTXMillis >= TXinterval)  //test whether it's time to send data
  {
    LastTXMillis = currentMillis;
    float t = tRaw;
    uint8_t t_sign = t < 0;
    int t10RoundPos = (int)(t * (t_sign ? -10 : 10) + 0.5);
    uint8_t t_deci = t10RoundPos % 10;
    uint8_t t_ones = (t10RoundPos / 10) % 10;
    uint8_t t_tens = (t10RoundPos / 100) % 10;
    uint8_t t_huns = (t10RoundPos / 1000) % 10;
    /* original code below replaced with that above to round to nearest 10th and prevent wrong t_deci
      uint8_t t_ones = ((int)(t * (t_sign ? -10 : 10)) / 10) % 10;
      uint8_t t_tens = ((int)(t * (t_sign ? -10 : 10)) / 100) % 10;
      uint8_t t_huns = ((int)(t * (t_sign ? -10 : 10)) / 1000) % 10;
    */
    int hRound = (int) (hRaw + 0.5); //to round off to nearest whole number (weather station does not display H tenths)
    uint8_t h_ones = hRound % 10;
    uint8_t h_tens = (hRound / 10) % 10;
#ifdef DEMONSTRATE
    data[6] = h_ones;  //demo temp trend arrow and low battery flag by cycling nibble value  = ones digit of humidity
    data[9] = h_ones * 0x10;  //demo humidity trend arrow by cycling nibble value  = ones digit of humidity (*0x10 as it is the high nibble)
#endif
    data[6] &= 0x0f; data[6] |= ((t_deci << 4) & 0xf0); //taken from Attiny sketch as original causes problem with decimal display
    data[7] &= 0xf0; data[7] |= ((t_ones << 0) & 0x0f);
    data[7] &= 0x0f; data[7] |= ((t_tens << 4) & 0xf0);
    data[8] &= 0xfc; data[8] |= ((t_huns << 0) & 0x03);
    data[8] &= 0xf3; data[8] |= ((t_sign << 3) & 0x0c);
    data[8] &= 0x0f; data[8] |= ((h_ones << 4) & 0xf0);
    data[9] &= 0xf0; data[9] |= ((h_tens << 0) & 0x0f);
    data[10] &= 0x00; data[10] |= (checksumSimple(data, SUM_MASK) & 0xff);
    data[11] &= 0x00; data[11] |= (checksumCRC(data, CRC_MASK, CRC_IV) & 0xff);
    sendData(data, dataLen);
    for (uint8_t i = 2; i < 12; ++i) {  //print hex nibbles beginning with sensor ID to match OregonReceiverDump sketch
      Serial.print(data[i] >> 4, HEX);
      Serial.print(data[i] & 0x0F, HEX);
    }
    Serial.println();
#ifdef CALIBRATE
    Serial.print("SHORT = ");  //print the current value of SHORT being tested
    Serial.print(SHORT);
    Serial.print(" H = ");
    Serial.print(hRaw);
    Serial.print(" T = ");
    Serial.print(tRaw);
    Serial.print(" Ch:");
    Serial.println(data[4], HEX);
    Serial.println();
#endif
#ifdef DEMONSTRATE
    Serial.print("Ch:");  //print the raw values of temp and humidity, and the values transmitted
    Serial.print(data[4], HEX);
    Serial.print(" ID:");
    Serial.print(data [5], HEX);
    Serial.print(" Traw:");
    Serial.print(tRaw);
    Serial.print(" Tsent:");
    if (t_sign) Serial.print("-");
    if (t10RoundPos > 999) Serial.print(t_huns);
    if (t10RoundPos > 99) Serial.print(t_tens);
    Serial.print(t_ones);
    Serial.print("."); Serial.print(t_deci);
    Serial.print(" Hraw:");
    Serial.print(hRaw);
    Serial.print(" Hsent:");
    if (hRound > 9) Serial.print(h_tens);
    Serial.println(h_ones);
    Serial.println();
    hRaw = hRaw + 1.13;
    tRaw = tRaw + 3.33;
    if (hRaw > 99.0) hRaw = 0.0;
    if (tRaw > 199.9) tRaw = -99.9;
#endif
#ifdef CALIBRATE
    tRaw = tRaw + 1.0;
    SHORT = SHORT + 1;
    if (tRaw > 9.8)
    { tRaw = 0.0;
      hRaw = hRaw + 1.0;
    }
    if (SHORT > SHORTend)
    { SHORT = SHORTstart;
      hRaw = SHORTstart / 10;
      tRaw = 0.0 + float(SHORTstart % 10);
    }
#endif
  }
}
void sendData(uint8_t data[], int len) {
  for (int i = 0; i < 2; ++i) { // Send the message twice
    cli(); // Disable interrupts to avoid any timing funny business
    for (int j = 0; j < len * 8; ++j) { // Bits are transmitted LSB-first
      sendBit((data[j / 8] >> (j % 8)) & 0x1);
    }
    digitalWrite(TX_PIN, LOW); // Don't leave the transmitter on!
    sei(); // Re-enable interrupts
    delay(15); // Pause for a short time between transmissions (Protocol 2016 suggest 10.9 ms pause)  15 works well with OregonReceiver sketch
    //    delay(55); // Pause for a short time between transmissions (Does not work with OregonReceiver sketch)
  }
}
void sendBit(bool val) {
  if (val) {
    sendLowHigh();
    sendHighLow(); // Doing it this way keeps the timing consistent, though
  } else {         // any difference is probably negligible
    sendHighLow();
    sendLowHigh();
  }
}
void sendLowHigh() {
  digitalWrite(TX_PIN, LOW);
  delayMicroseconds(SHORT);
  digitalWrite(TX_PIN, HIGH);
  delayMicroseconds(SHORT);
}
void sendHighLow() {
  digitalWrite(TX_PIN, HIGH);
  delayMicroseconds(SHORT);
  digitalWrite(TX_PIN, LOW);
  delayMicroseconds(SHORT);
}
uint8_t checksumSimple(uint8_t data[], uint64_t mask) {
  uint16_t s = 0x0000;
  for (int i = 0; i < 64; ++i) {
    if (!((mask >> i) & 0x1)) continue; // Skip nibbles that aren't set in the mask
    s += (data[i / 2] >> ((i % 2) * 4)) & 0xf; // Sum data nibble by nibble
    s += (s >> 8) & 0x1; // Add any overflow back into the sum
    s &= 0xff;
  }
  return s;
}
uint8_t checksumCRC(uint8_t data[], uint64_t mask, uint8_t iv) {
  uint16_t s = iv;
  for (int i = 0; i < 64; ++i) {
    if (!((mask >> i) & 0x1)) continue; // Skip nibbles that aren't set in the mask
    uint8_t nibble = (data[i / 2] >> ((i % 2) * 4)) & 0xf;
    for (int j = 3; j >= 0; --j) {
      uint8_t bit = (nibble >> j) & 0x1;
      s <<= 1;
      s |= bit;
      if (s & 0x100) {
        s ^= CRC_POLY;
      }
    }
  }
  for (int i = 0; i < 8; ++i) {
    s <<= 1;
    if (s & 0x100) {
      s ^= CRC_POLY;
    }
  }
  return s;
}

Nice find on the timing determination and calibration.

I also strongly recommend the type of logic analyzer discussed by the blog author as "all you need" for Arduino. Very inexpensive and available from all the usual outlets. I use mine all the time.