Skip to content

Instantly share code, notes, and snippets.

@bjoern-r
Last active April 20, 2026 07:52
Show Gist options
  • Select an option

  • Save bjoern-r/67927da9bc9ecdec18ed03d24ec1566b to your computer and use it in GitHub Desktop.

Select an option

Save bjoern-r/67927da9bc9ecdec18ed03d24ec1566b to your computer and use it in GitHub Desktop.
esphome esp32-c3 BLE proxy combined with rtl433 decoder
esphome:
name: esp32c3-test
friendly_name: esp32c3-test for rtl433
esp32:
board: nologo_esp32c3_super_mini
framework:
type: esp-idf
# Enable logging
logger:
# level: VERBOSE
level: DEBUG
# Enable Home Assistant API
api:
encryption:
key: "XXX"
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
on_client_disconnected:
- esp32_ble_tracker.stop_scan:
ota:
platform: esphome
password: !secret esphome_ota_pw
time:
# see esphome docs for more places to get time from...
- platform: homeassistant
id: timesrc
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Esp32C3-Test Fallback Hotspot"
password: !secret wifi_fallback_password
web_server:
port: 80
captive_portal:
external_components:
- source: github://juanboro/esphome
components: [ udp_broadcast ]
packages:
# this generally should be all what you need to add this rtl_433 component to esphome:
rtl_433: github://juanboro/esphome-rtl_433-decoder/rtl_433.yaml
#you need to setup ESPHome and your radio board settings - customize as needed
#esp32_board: !include esp32devboard_and_cc1101_example.yaml
esp32c3-cc1101: !include cc1101/esp32c3-cc1101.yaml
output:
- platform: gpio
pin: 8
id: blue_led
inverted: True
light:
- platform: binary
name: "Blue LED"
output: blue_led
udp_broadcast:
port: 5009
id: udpbc
remote_receiver:
- id: !extend rf_receiver
#on_raw:
# then:
# - lambda: |-
# id(my_rtl433_id).recv_raw(x);
# for debugging
#
#dump: raw
on_raw:
then:
- lambda: |-
int rawcount=x.size();
if (rawcount<10) return; // ignore noise
char buffer[512];
size_t bwritten=0;
bwritten+=snprintf(buffer,sizeof(buffer) - bwritten,";pulse data\n;version 1\n;timescale 1us\n;centerfreq %d Hz\n;rssi %0.1f dB\n",(uint32_t) (id(mycc1101)._freq*1e6),id(mycc1101).last_rx_rssi);
bwritten+=snprintf(buffer+bwritten,sizeof(buffer) - bwritten,";received ");
bwritten+=id(timesrc).now().strftime(buffer+bwritten,sizeof(buffer) - bwritten, "%Y-%m-%d %H:%M:%S");
bwritten+=snprintf(buffer+bwritten,sizeof(buffer) - bwritten,"\n");
int j=0;
while (j<rawcount) {
if (x[j]>0) {
bwritten+=snprintf(buffer+bwritten,sizeof(buffer) - bwritten,"%d %d\n",x[j],(j<rawcount-1) && (x[j+1]<0) ? -x[j+1] : 10000);
++j;
}
++j;
if (bwritten>490) {
id(udpbc).send_data(buffer,bwritten);
bwritten=0;
}
}
bwritten+=snprintf(buffer+bwritten,sizeof(buffer) - bwritten,";end\n");
id(udpbc).send_data(buffer,bwritten);
# ESP-32 Remote Handling Notes
# Note: These notes are specific the platform: esp-idf --- similar but different settings exist for the arudiono platform as well.
# You REALLY probably want to use the esp-idf platform (unless you can't)
# https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/peripherals/rmt.html#rmt-resource-allocation
# https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf#rmt
# https://esphome.io/components/remote_receiver#esp32-idf-configuration-variables
# the original esp-32 has 2k of symbol memory (512 symbols)
# rmt_symbols sets mem_block_symbols in esp channel config - the esphome default will be 192 symbols/channel
# In this example - we max things out at 448 symbols for the receive (the transmit can recycle symbol memory - so a smaller symbol size of the default 64 is ok)
# note that if additional remotes are added (up to 8) - then the memory will need apportioned as necessary. Even 448 symbols (<1000 pulses) isn't real great
# for some of what we'd like to receive on the cc1101.
#rmt_symbols: 448
# generally - receive_symbols only makes sense to be bigger than rmt_symbols on systems w/ dma support (not the original esp32)
# but should always be set to at least rmt_symbols...
# IF you have a newer/better ESP-32 device - you can make receive_symbols as large as the buffer you need, and rmt_symbols can be smaller.
#receive_symbols: 448
###############################################
## rtl_433 decoder example
###############################################
# How to use:
# 1. Configure the rtl_433 component to receive ESPhome remote data (receiver_id)
# 2. Use the on_json_message trigger from the rtl_433 component to receive the json messages and process
# this example processes pulses from the remote receiver component and then sends decoded
# data via MQTT similar to how rtl_433 would. It also generates home-assistant event bus messages
# via the API (if HA is connected and listening)
#rtl_433:
# id: my_rtl433_id
#receiver_id: rf_receiver
# on_json_message:
# then:
# - logger.log: "on_json_message triggered"
# - lambda: |-
# if (x["model"]) {
# ESP_LOGI("on_json_message model: %s", x["model"].as<const char*>());
# }
# #, "Event %s triggered.", json::build_json([x](JsonObject root) { root.set(x); }));
# # MQTT publish similar to how rtl_433 would...
# # todo: add in the timestamp to the json message
# #- lambda: !lambda ESP_LOGI("on_json_message", x.as<const char*>());
# if (id(log_advertisments)) { ESP_LOGI("on_ble_advertise", "[%s] Packet %s", x.address_str().c_str(),
# ble_gateway::BLEGateway::scan_result_to_hci_packet_hex(x.get_scan_result()).c_str()); }
# - mqtt.publish_json:
# topic: !lambda |-
# std::string topic=str_sprintf("rtl_433/%s/events",App.get_name().c_str());
# if (x["model"]) {
# if (x["id"])
# topic+=str_sprintf("/%s/%d",x["model"].as<const char*>(),x["id"].as<int>());
# else
# topic+=str_sprintf("/%s",x["model"].as<const char*>());
# }
# return(topic);
# payload:
# root.set(x);
# # Also generate a home assistant event (you can then do all kinds of things with this data in home-assistant)
# # Note that HA will also receive all the event data above - so really only one of these is necessary...
# # HA event API is better, but MQTT is more universal. The choice is yours.
# - homeassistant.event:
# event: esphome.rtl_433
# data:
# hostname: !lambda |-
# return App.get_name();
# message: !lambda |-
# return json::build_json([x](JsonObject root) {
# root.set(x);
# });
# # You could also include other interesting information you may have from your RF receiver:
# # frequency: !lambda |-
# # return id(mycc1101)._freq;
# # rssi: !lambda |-
# # return id(mycc1101).last_rx_rssi;
esp32_ble_tracker:
scan_parameters:
# On dual-core devices the WiFi component runs on core 1, while this component runs on core 0.
# When using this component on single core chips such as the ESP32-C3 both WiFi and ble_tracker must run on the same core, and this has been known to cause issues when connecting to WiFi.
# A work-around for this is to enable the tracker only while the native API is connected.
active: false
continuous: false
#on_ble_advertise:
# - then:
# - lambda: |-
# ESP_LOGD("ble_adv", "New BLE device");
# ESP_LOGD("ble_adv", " address: %s", x.address_str().c_str());
# ESP_LOGD("ble_adv", " name: %s", x.get_name().c_str());
bluetooth_proxy:
active: true
button:
- platform: safe_mode
id: button_safe_mode
name: Safe Mode Boot
- platform: factory_reset
id: factory_reset_btn
name: Factory reset
number:
- platform: template
id: cc1101_freq_num
name: "CC1101 frequency"
lambda: return id(mycc1101)._freq;
set_action:
then:
- lambda: |-
id(mycc1101)._freq=x;
id(mycc1101).setup_direct_mode();
id(cc1101_freq_num).update();
min_value: 300.0
max_value: 928.0
step: 0.01
unit_of_measurement: MHz
- platform: template
id: cc1101_bw_num
name: "CC1101 filter bandwidth"
lambda: return id(mycc1101)._bandwidth;
set_action:
then:
- lambda: |-
id(mycc1101)._bandwidth=x;
id(mycc1101).setup_direct_mode();
id(cc1101_bw_num).update();
min_value: 58.0
max_value: 818.0
step: 10
unit_of_measurement: kHz
- platform: template
id: cc1101_br_num
name: "CC1101 bitrate"
lambda: return id(mycc1101)._bitrate;
set_action:
then:
- lambda: |-
id(mycc1101)._bitrate=x;
id(mycc1101).setup_direct_mode();
id(cc1101_br_num).update();
min_value: 0.025
max_value: 600
step: 0.025
unit_of_measurement: kbps
- platform: template
id: cc1101_cs_abs_thr_num
name: "CC1101 RSSI threshold to assert carrier sense, Thr = MAGN_TARGET + CARRIER_SENSE_ABS_TH [dB]"
lambda: return -((-(id(mycc1101)._REG_AGCCTRL1&3))&3);
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL1=((~((int)x))+1)&3;
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl1).update();
id(cc1101_cs_abs_thr_num).update();
min_value: -3
max_value: 0
step: 1
unit_of_measurement: dB
select:
- platform: template
id: cc1101_modulation
name: "CC1101 modulation"
options:
- "OOK (default)"
- "FSK"
lambda: |-
return id(cc1101_modulation).at(id(mycc1101)._modulation==esphome::radiolib_cc1101::OOK_MODULATION ? 0 : 1);
set_action:
then:
- lambda: |-
id(mycc1101)._modulation=id(cc1101_modulation).index_of(x).value()==0 ? esphome::radiolib_cc1101::OOK_MODULATION : esphome::radiolib_cc1101::FSK_MODULATION;
id(mycc1101).setup_direct_mode();
id(cc1101_modulation).update();
- platform: template
id: cc1101_dvga_gain_reduce
name: "Reduce maximum available DVGA gain by:"
options:
- "no reduction (default)"
- "disable top gain setting"
- "disable top two gain setting"
- "disable top three gain setting"
lambda: return id(cc1101_dvga_gain_reduce).at(0x3&(id(mycc1101)._REG_AGCCTRL2>>6)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL2=(id(mycc1101)._REG_AGCCTRL2&0xc7)|(id(cc1101_dvga_gain_reduce).index_of(x).value()<<6);
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl2).update();
id(cc1101_dvga_gain_reduce).update();
- platform: template
id: cc1101_lna_gain_reduce
name: "Reduce maximum LNA gain by:"
options:
- "0 dB (default)"
- "2.6 dB"
- "6.1 dB"
- "7.4 dB"
- "9.2 dB"
- "11.5 dB"
- "14.6 dB"
- "17.1 dB"
lambda: return id(cc1101_lna_gain_reduce).at(0x7&(id(mycc1101)._REG_AGCCTRL2>>3)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL2=(id(mycc1101)._REG_AGCCTRL2&0xc7)|(id(cc1101_lna_gain_reduce).index_of(x).value()<<3);
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl2).update();
id(cc1101_lna_gain_reduce).update();
- platform: template
id: cc1101_magn_target
name: "Average amplitude target for filter (MAGN_TARGET):"
options:
- "24 dB"
- "27 dB"
- "30 dB"
- "33 dB (default)"
- "36 dB"
- "38 dB"
- "40 dB"
- "42 dB"
lambda: return id(cc1101_magn_target).at(0x7&(id(mycc1101)._REG_AGCCTRL2)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL2=(id(mycc1101)._REG_AGCCTRL2&0xc7)|(id(cc1101_magn_target).index_of(x).value());
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl2).update();
id(cc1101_magn_target).update();
- platform: template
id: cc1101_lna_priority
name: "LNA priority setting:"
options:
- "LNA2 first"
- "LNA first (default)"
lambda: return id(cc1101_lna_priority).at(0x1&(id(mycc1101)._REG_AGCCTRL1>>6)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL1=(id(mycc1101)._REG_AGCCTRL1&0xbf)|(id(cc1101_lna_priority).index_of(x).value()<<6);
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl1).update();
id(cc1101_lna_priority).update();
- platform: template
id: cc1101_carrier_sense_rel_thr
name: "RSSI relative change to assert carrier sense:"
options:
- "disabled (default)"
- "6 dB"
- "10 dB"
- "14 dB"
lambda: return id(cc1101_carrier_sense_rel_thr).at(0x3&(id(mycc1101)._REG_AGCCTRL1>>4)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL2=(id(mycc1101)._REG_AGCCTRL1&0xcf)|(id(cc1101_carrier_sense_rel_thr).index_of(x).value()<<4);
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl1).update();
id(cc1101_carrier_sense_rel_thr).update();
# CARRIER_SENSE_ABS_THR is number template above
- platform: template
id: cc1101_hyst_level
name: "AGC hysteresis level:"
options:
- "none"
- "low"
- "medium (default)"
- "high"
lambda: return id(cc1101_hyst_level).at(0x3&(id(mycc1101)._REG_AGCCTRL0>>6)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL0=(id(mycc1101)._REG_AGCCTRL0&0x3f)|(id(cc1101_hyst_level).index_of(x).value()<<6);
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl0).update();
id(cc1101_hyst_level).update();
- platform: template
id: cc1101_wait_time
name: "AGC wait time:"
options:
- "8 samples"
- "16 samples (default)"
- "24 samples"
- "32 samples"
lambda: return id(cc1101_wait_time).at(0x3&(id(mycc1101)._REG_AGCCTRL0>>4)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL0=(id(mycc1101)._REG_AGCCTRL0&0xcf)|(id(cc1101_wait_time).index_of(x).value()<<4);
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl0).update();
id(cc1101_wait_time).update();
- platform: template
id: cc1101_agc_freeze
name: "Freeze AGC gain:"
options:
- "never (default)"
- "when sync word is found"
- "manually freeze analog control"
- "manually freeze analog and digital control"
lambda: return id(cc1101_agc_freeze).at(0x3&(id(mycc1101)._REG_AGCCTRL0>>2)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL0=(id(mycc1101)._REG_AGCCTRL0&0xf3)|(id(cc1101_agc_freeze).index_of(x).value()<<2);
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl0).update();
id(cc1101_agc_freeze).update();
- platform: template
id: cc1101_filter_length
name: "Averaging length for channel filter:"
options:
- "8 samples"
- "16 samples (default)"
- "32 samples"
- "64 samples"
lambda: return id(cc1101_filter_length).at(0x3&(id(mycc1101)._REG_AGCCTRL0)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL0=(id(mycc1101)._REG_AGCCTRL0&0xfc)|(id(cc1101_filter_length).index_of(x).value());
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl0).update();
id(cc1101_filter_length).update();
- platform: template
id: cc1101_ask_ook_boundary
name: "ASK,OOK decision boundary:"
options:
- "4 dB"
- "8 dB (default)"
- "12 dB"
- "16 dB"
lambda: return id(cc1101_ask_ook_boundary).at(0x3&(id(mycc1101)._REG_AGCCTRL0)).value();
set_action:
then:
- lambda: |-
id(mycc1101)._REG_AGCCTRL0=(id(mycc1101)._REG_AGCCTRL0&0xfc)|(id(cc1101_ask_ook_boundary).index_of(x).value());
id(mycc1101).setup_direct_mode();
id(cc1101_sensor_reg_agcctrl0).update();
id(cc1101_ask_ook_boundary).update();
sensor:
- platform: template
id: cc1101_sensor_rssi
name: "CC1101 RSSI"
lambda: return id(mycc1101).getRSSI();
update_interval: 10s
unit_of_measurement: dB
- platform: template
id: cc1101_sensor_rx_rssi
name: "CC1101 Last RX RSSI"
lambda: return id(mycc1101).last_rx_rssi;
update_interval: 10s
unit_of_measurement: dB
text_sensor:
- platform: template
id: cc1101_sensor_reg_agcctrl2
name: "CC1101 _REG_AGCCTRL2"
lambda: return str_sprintf("0x%x",id(mycc1101)._REG_AGCCTRL2);
- platform: template
id: cc1101_sensor_reg_agcctrl1
name: "CC1101 _REG_AGCCTRL1"
lambda: return str_sprintf("0x%x",id(mycc1101)._REG_AGCCTRL1);
- platform: template
id: cc1101_sensor_reg_agcctrl0
name: "CC1101 _REG_AGCCTRL0"
lambda: return str_sprintf("0x%x",id(mycc1101)._REG_AGCCTRL0);
substitutions:
cs: "7" # CC1101 pin 4
clk: "4" # CC1101 pin 5
mosi: "6" # CC1101 pin 6
miso: "5" # CC1101 pin 7
rx: "3" # CC1101 pin 3
tx: "3" # CC1101 pin 3
tx_rx_shared: "true"
external_components:
- source: github://juanboro/esphome-radiolib-cc1101@main
components: [ radiolib_cc1101 ]
packages:
cc1101_controls: !include cc1101-controls.yaml
spi:
# these are the default VSPI pins to use on ESP32
clk_pin: $clk # CC1101 pin 5
mosi_pin: $mosi # CC1101 pin 6
miso_pin: $miso # CC1101 pin 7
remote_receiver:
- id: rf_receiver
idle: 10ms
filter: 20us # Some RF sources have pretty high BW - adjust as needed
buffer_size: 2048 # This value is pretty large... default 10k is way too large
pin:
number: $rx
allow_other_uses: true
mode:
output: false
input: true
pullup: $tx_rx_shared
open_drain: false
id: cc1101_gd0_recv
remote_transmitter:
- id: rf_transmitter
pin:
number: $tx
allow_other_uses: ${tx_rx_shared}
mode:
output: true
input: true
pullup: true
open_drain: true
id: cc1101_gd0_xmit
on_transmit:
then:
- lambda: |-
id(mycc1101).xmit();
on_complete:
then:
- lambda: |-
id(mycc1101).recv();
carrier_duty_percent: 100%
radiolib_cc1101:
id: mycc1101
cs_pin: $cs # CC1101 pin 4
# optionally define rx_pin to allow monitoring rx rssi
rx_pin:
number: $rx # This is CC1101 GDO0 pin 3
allow_other_uses: true
# other optional settings.. (use control settings to verify they work and then set)
filter: 468khz # RX filter BW (58-812kHz)
bitrate: 10 # bitrate (0.025-600kbps)
reg_agcctrl0: 0xb2 # agcctrol0 register setting
reg_agcctrl1: 0x00 # agcctrol1 register setting
reg_agcctrl2: 0xc7 # agcctrol2 register setting
# it's a good idea to turn off the CC1101 receiver when doing OTA updates
ota:
platform: esphome
on_begin:
- lambda: id(mycc1101).standby();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment