← Back Concept Advanced Connectivity
LoRa Range Bridge Kilometers of range for your sensor mesh — the spec-complete CameraPlus base station.
Board LILYGO LoRa32 v2.1 (ESP32 + SX1276)
Est. cost ~$42
Install time 90 min
Sensors LoRa 915 MHz radio, SSD1306 OLED status display
When WiFi is jammed, down, or simply out of reach, LoRa keeps sensor events flowing. This is the CameraPlus base station design: a LILYGO LoRa32 bridging 915 MHz LoRa packets to HTTP, with a 60-byte authenticated binary protocol (HMAC + replay protection), an OLED status display, an outbox poll loop, and a 12-hour self-heal reboot. The protocol, endpoints, and packet format are locked and documented; the firmware is a structure-complete skeleton awaiting Phase 1 hardware — follow along and build it with us.
Maintained and field-tested by the Latent team.
Start guided install What you need LILYGO LoRa32 v2.1 (915 MHz) ~$35 915 MHz antenna ~$5 USB cable + 5V adapter ~$5 Est. cost ~$42
Install steps 1 Read the protocol first
2 Check your regional frequency
3 Provision, do not hardcode
Device config Full config — copy it, download it, or follow the guided install.
Copy Download
# =============================================================================
# CameraPlus — LoRa base station
# Hardware: LILYGO LoRa32 915 MHz (ESP32 + SX1276)
#
# STRUCTURE-ONLY SKELETON. No functional firmware yet — only section
# headings + comments describing what each block will contain when
# the device arrives. To be completed in firmware Phase 1
# (see ../../ROADMAP.md).
# =============================================================================
# ── 1. Identity ──────────────────────────────────────────────────────────────
# esphome:
# name: cameraplus-lora-base
# friendly_name: CameraPlus LoRa Base Station
# platformio_options:
# board_build.flash_mode: dio
# on_boot:
# priority: 800
# then:
# - lambda: |-
# id(boot_reason_int) = (int) esp_reset_reason();
# id(sender_id) = SENDER_ID_BASE_STATION; // from compile-time define
# id(last_seq) = 0;
# ── 2. Chip + framework ──────────────────────────────────────────────────────
# esp32:
# board: esp32dev
# framework:
# type: esp-idf
# version: recommended
# sdkconfig_options:
# CONFIG_ESP_TASK_WDT_TIMEOUT_S: "15"
# ── 3. WiFi (rugged default; LoRa is the additive path) ──────────────────────
# wifi:
# ssid: !secret uplink_ssid
# password: !secret uplink_password
# power_save_mode: none
# reboot_timeout: 1800s
# ── 4. Logger ────────────────────────────────────────────────────────────────
# logger:
# level: INFO
# ── 5. HTTP — outbound to CRM ────────────────────────────────────────────────
# http_request:
# verify_ssl: false
# timeout: 5s
# ── 6. OLED display (SSD1306 over I2C) ──────────────────────────────────────
# i2c:
# sda: GPIO21
# scl: GPIO22
# display:
# - platform: ssd1306_i2c
# model: "SSD1306 128x64"
# update_interval: 1s
# lambda: |-
# // 4-line status display:
# // line 1: "LFnet base · v1"
# // line 2: wifi state + IP
# // line 3: last rx (sender_id, RSSI, time)
# // line 4: outbox queue length
# // when usb_relay_on is true, line 1 ends with " · USB"
# ── 7. LoRa SX1276 over SPI ─────────────────────────────────────────────────
# spi:
# clk_pin: GPIO5
# miso_pin: GPIO19
# mosi_pin: GPIO27
# # NOTE: LoRa support in ESPHome native is partial. Options:
# # a) Use a third-party `lora_sx127x` external_component
# # b) Talk SPI directly from a lambda — possible but verbose
# # c) Fall back to IDF + ESPHome custom component (most flexible)
# # Decision pending — write up the choice once we have the device in hand
# # and can test bandwidth/latency.
# ── 8. Globals — protocol state ──────────────────────────────────────────────
# globals:
# - { id: boot_reason_int, type: int, initial_value: '0' }
# - { id: sender_id, type: 'uint8_t', initial_value: '0xF0' } # base station sender_id
# - { id: last_seq, type: 'uint32_t', initial_value: '0' }
# - { id: usb_relay_on, type: 'bool', initial_value: 'false', restore_value: true }
# - { id: outbox_len, type: 'uint16_t', initial_value: '0' }
# - { id: last_rx_sender, type: 'uint8_t', initial_value: '0' }
# - { id: last_rx_at, type: 'uint32_t', initial_value: '0' }
# - { id: last_rx_rssi, type: int, initial_value: '0' }
# ── 9. Scripts ──────────────────────────────────────────────────────────────
# script:
# - id: post_lora_event
# parameters: { body: string }
# then:
# - http_request.post:
# url: "https://crm.darksoc.org/api/dcmonitor/lora_event"
# body: !lambda 'return body;'
# request_headers:
# Content-Type: application/octet-stream
# X-LoRa-Sender: !lambda 'return std::to_string(id(last_rx_sender));'
# X-LoRa-RSSI: !lambda 'return std::to_string(id(last_rx_rssi));'
#
# - id: poll_outbox
# then:
# - http_request.get:
# url: "https://crm.darksoc.org/api/dcmonitor/lora_outbox"
# capture_response: true
# on_response:
# - lambda: |-
# // body is binary bytes of next-to-send LoRa packet
# // (or zero-length if outbox is empty)
# // tx via SX1276 driver
# // POST ack back to CRM
# ── 10. Intervals ────────────────────────────────────────────────────────────
# interval:
# - interval: 100ms
# then:
# - lambda: |-
# // Poll SX1276 for received packet (DIO0 interrupt-driven in real impl)
# // On rx:
# // 1. Validate HMAC against PSK from NVS
# // 2. Check replay (seq monotonic per sender_id)
# // 3. Stamp last_rx_* globals (for OLED)
# // 4. id(post_lora_event).execute(binary_payload)
# // 5. If usb_relay_on, also ESP_LOGI("lfnet", "LFNET:<base64>")
#
# - interval: 2000ms
# then:
# - script.execute: poll_outbox
#
# - interval: 12h
# startup_delay: 12h
# then:
# - button.press: reboot_button # routine self-heal
# ── 11. Encoder / button — USB relay toggle ──────────────────────────────────
# binary_sensor:
# - platform: gpio
# pin: { number: GPIO0, inverted: true, mode: { input: true, pullup: true } }
# id: usb_btn
# on_click:
# min_length: 30ms
# max_length: 700ms
# then:
# - lambda: |-
# id(usb_relay_on) = !id(usb_relay_on);
# ── 12. Reboot button ────────────────────────────────────────────────────────
# button:
# - platform: restart
# id: reboot_button
# name: "Reboot"
# =============================================================================
# CONFIGURATION TO PROVISION AT FLASH TIME (via compile-time defines or NVS):
#
# SENDER_ID_BASE_STATION — this device's sender_id (0xF0–0xFE reserved for
# non-sensor infrastructure like base stations)
# LORA_PSK_HEX — 32-byte hex string, deployment-wide PSK
# LORA_FREQUENCY_HZ — e.g. 915000000 for 915 MHz
# LORA_SF — spreading factor 7..12
# LORA_BW — bandwidth 125 / 250 / 500 kHz
#
# These are NOT in this file. They are loaded from NVS provisioned at first
# flash via a separate setup tool, or compiled in via build flags.
# =============================================================================
Reviews (0) No reviews yet — be the first.
Sign in to use this feature — it takes 20 seconds and it’s free. Sign in