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;
}