This commit is contained in:
Michael Gjelsø 2026-04-20 10:53:44 +02:00 committed by GitHub
commit c71331d885
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 2016 additions and 2 deletions

View file

@ -0,0 +1,37 @@
{
"build": {
"arduino": {
"ldscript": "esp32_out.ld",
"partitions": "default_8MB.csv"
},
"core": "esp32",
"extra_flags": "-DARDUINO_RADIOMASTER_BANDIT -DARDUINO_ARCH_ESP32",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "esp32"
},
"connectivity": [
"wifi",
"bluetooth",
"ethernet"
],
"debug": {
"openocd_board": "esp-wroom-32.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Radiomaster Bandit",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"require_upload_port": true,
"speed": 460800
},
"url": "https://www.espressif.com/en/products/socs/esp32",
"vendor": "Espressif"
}

View file

@ -19,11 +19,31 @@
#define LONG_PRESS_MILLIS 1200
#ifdef RADIOMASTER_900_BANDIT
// NeoPixel message notification settings
#define NEOPIXEL_MSG_UPDATE_MILLIS 30 // Update every 30ms for smooth breathing
#define NEOPIXEL_MIN_BRIGHTNESS 5 // Minimum brightness (dim)
#define NEOPIXEL_MAX_BRIGHTNESS 80 // Maximum brightness (bright blue)
#define NEOPIXEL_BRIGHTNESS_STEP 2 // Brightness change per update
// User-definable message notification color (RGB hex format)
// Examples: 0x0000FF (blue), 0x00FF00 (green), 0xFF0000 (red),
// 0xFF00FF (magenta), 0x00FFFF (cyan), 0xFFFF00 (yellow)
#ifndef NEW_MSG_LED
#define NEW_MSG_LED 0x0000FF // Default: Blue
#endif
// Extract RGB components from hex color
#define NEOPIXEL_MSG_RED ((NEW_MSG_LED >> 16) & 0xFF)
#define NEOPIXEL_MSG_GREEN ((NEW_MSG_LED >> 8) & 0xFF)
#define NEOPIXEL_MSG_BLUE (NEW_MSG_LED & 0xFF)
#endif
#ifndef UI_RECENT_LIST_SIZE
#define UI_RECENT_LIST_SIZE 4
#endif
#if UI_HAS_JOYSTICK
#if defined(UI_HAS_JOYSTICK) || defined(PIN_USER_JOYSTICK)
#define PRESS_LABEL "press Enter"
#else
#define PRESS_LABEL "long press"
@ -557,6 +577,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#if defined(PIN_USER_BTN_ANA)
analog_btn.begin();
#endif
#if defined(PIN_USER_JOYSTICK)
analog_joystick.begin();
#endif
_node_prefs = node_prefs;
@ -662,6 +685,58 @@ void UITask::userLedHandler() {
#endif
}
#ifdef RADIOMASTER_900_BANDIT
void UITask::neopixelMsgHandler() {
unsigned long cur_time = millis();
if (_msgcount > 0) {
// We have unread messages - do breathing effect
if (cur_time >= next_neopixel_change) {
// Update brightness
if (neopixel_brightness_increasing) {
neopixel_brightness += NEOPIXEL_BRIGHTNESS_STEP;
if (neopixel_brightness >= NEOPIXEL_MAX_BRIGHTNESS) {
neopixel_brightness = NEOPIXEL_MAX_BRIGHTNESS;
neopixel_brightness_increasing = false;
}
} else {
if (neopixel_brightness <= NEOPIXEL_BRIGHTNESS_STEP) {
neopixel_brightness = NEOPIXEL_MIN_BRIGHTNESS;
neopixel_brightness_increasing = true;
} else {
neopixel_brightness -= NEOPIXEL_BRIGHTNESS_STEP;
}
}
// Set NeoPixels 2-5 to user-defined color with current brightness
// Leave 0-1 for button backlights
// Scale each RGB component by the brightness level
uint8_t r = (NEOPIXEL_MSG_RED * neopixel_brightness) / NEOPIXEL_MAX_BRIGHTNESS;
uint8_t g = (NEOPIXEL_MSG_GREEN * neopixel_brightness) / NEOPIXEL_MAX_BRIGHTNESS;
uint8_t b = (NEOPIXEL_MSG_BLUE * neopixel_brightness) / NEOPIXEL_MAX_BRIGHTNESS;
for (int i = 2; i < NEOPIXEL_NUM; i++) {
pixels.setPixelColor(i, pixels.Color(r, g, b));
}
pixels.show();
next_neopixel_change = cur_time + NEOPIXEL_MSG_UPDATE_MILLIS;
}
} else {
// No messages - turn off message notification NeoPixels (2-5)
// Only turn them off if they were previously on
if (neopixel_brightness > 0) {
neopixel_brightness = 0;
neopixel_brightness_increasing = true;
for (int i = 2; i < NEOPIXEL_NUM; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
pixels.show();
}
}
}
#endif
void UITask::setCurrScreen(UIScreen* c) {
curr = c;
_next_refresh = 100;
@ -697,6 +772,8 @@ void UITask::shutdown(bool restart){
bool UITask::isButtonPressed() const {
#ifdef PIN_USER_BTN
return user_btn.isPressed();
#elif defined(PIN_USER_JOYSTICK)
return analog_joystick.isPressed();
#else
return false;
#endif
@ -754,6 +831,33 @@ void UITask::loop() {
_analogue_pin_read_millis = millis();
}
#endif
#if defined(PIN_USER_JOYSTICK)
if ((millis() - _analogue_pin_read_millis) > 10) {
uint8_t key = analog_joystick.check();
if (key != 0) {
// Map joystick directions to key codes and check display
switch (key) {
case KEY_UP:
c = checkDisplayOn(KEY_UP);
break;
case KEY_DOWN:
c = checkDisplayOn(KEY_DOWN);
break;
case KEY_LEFT:
c = checkDisplayOn(KEY_PREV);
break;
case KEY_RIGHT:
c = checkDisplayOn(KEY_NEXT);
break;
case KEY_SELECT:
// For center button, maybe long press support?
c = checkDisplayOn(KEY_ENTER);
break;
}
}
_analogue_pin_read_millis = millis();
}
#endif
#if defined(BACKLIGHT_BTN)
if (millis() > next_backlight_btn_check) {
bool touch_state = digitalRead(PIN_BUTTON2);
@ -774,6 +878,10 @@ void UITask::loop() {
userLedHandler();
#ifdef RADIOMASTER_900_BANDIT
neopixelMsgHandler();
#endif
#ifdef PIN_BUZZER
if (buzzer.isPlaying()) buzzer.loop();
#endif
@ -802,6 +910,11 @@ void UITask::loop() {
#if AUTO_OFF_MILLIS > 0
if (millis() > _auto_off) {
_display->turnOff();
#ifdef RADIOMASTER_900_BANDIT
pixels.setPixelColor(0, pixels.Color(0, 0, 0));
pixels.setPixelColor(1, pixels.Color(0, 0, 0));
pixels.show();
#endif
}
#endif
}
@ -841,6 +954,12 @@ char UITask::checkDisplayOn(char c) {
if (!_display->isOn()) {
_display->turnOn(); // turn display on and consume event
c = 0;
#ifdef RADIOMASTER_900_BANDIT
// Restore backlight for buttons here.
// pixels.setPixelColor(0, pixels.Color(255, 0, 0));
// pixels.setPixelColor(1, pixels.Color(0, 255, 0));
// pixels.show();
#endif
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 0; // trigger refresh

View file

@ -19,6 +19,12 @@
#include <helpers/ui/GenericVibration.h>
#endif
// Add NeoPixel support for Bandit Board
#ifdef RADIOMASTER_900_BANDIT
#include <Adafruit_NeoPixel.h>
extern Adafruit_NeoPixel pixels;
#endif
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
@ -44,7 +50,14 @@ class UITask : public AbstractUITask {
int last_led_increment = 0;
#endif
#ifdef PIN_USER_BTN_ANA
#ifdef RADIOMASTER_900_BANDIT
// NeoPixel message notification support
unsigned long next_neopixel_change = 0;
uint8_t neopixel_brightness = 0;
bool neopixel_brightness_increasing = true;
#endif
#if defined(PIN_USER_JOYSTICK) || defined(PIN_USER_BTN_ANA)
unsigned long _analogue_pin_read_millis = millis();
#endif
@ -54,6 +67,9 @@ class UITask : public AbstractUITask {
UIScreen* curr;
void userLedHandler();
#ifdef RADIOMASTER_900_BANDIT
void neopixelMsgHandler();
#endif
// Button action handlers
char checkDisplayOn(char c);

View file

@ -0,0 +1,106 @@
#include "AnalogJoystick.h"
AnalogJoystick::AnalogJoystick(int8_t pin, JoyADCMapping *mappings, uint8_t num_mappings,
uint8_t select_key_code, unsigned long long_press_ms, int tolerance,
unsigned long debounce_ms) {
_pin = pin;
_mappings = mappings;
_num_mappings = num_mappings;
_select_key = select_key_code;
_long_press_ms = long_press_ms;
_tolerance = tolerance;
_debounce_ms = debounce_ms;
prev = 0;
_last_change_time = 0;
_select_press_start = 0;
_long_press_triggered = false;
}
void AnalogJoystick::begin() {
if (_pin >= 0) {
pinMode(_pin, INPUT);
}
}
uint8_t AnalogJoystick::findClosestKey(int adc_value) const {
int closest_index = -1;
int min_diff = 32767;
for (uint8_t i = 0; i < _num_mappings; i++) {
int diff = abs(adc_value - _mappings[i].adc_value);
if (diff < min_diff) {
min_diff = diff;
closest_index = i;
}
}
if (closest_index >= 0 && min_diff < _tolerance) {
return _mappings[closest_index].key_code;
}
return 0;
}
uint8_t AnalogJoystick::check() {
if (_pin < 0) return 0;
int adc_value = analogRead(_pin);
uint8_t key = findClosestKey(adc_value);
// Handle SELECT button with long press support
if (key == _select_key) {
if (_select_press_start == 0) {
// SELECT just pressed - start tracking
_select_press_start = millis();
_long_press_triggered = false;
prev = key;
} else if (!_long_press_triggered && (millis() - _select_press_start) >= _long_press_ms) {
// Long press threshold reached
_long_press_triggered = true;
return 0xFF; // Special code for long press (only sent once)
}
// Still holding, waiting for either release or long press
return 0;
} else if (prev == _select_key && _select_press_start > 0) {
// SELECT was just released
bool was_long_press = _long_press_triggered;
_select_press_start = 0;
_long_press_triggered = false;
prev = key; // Update to new state (likely 0/idle)
if (!was_long_press) {
// Released before long press - this is a click
_last_change_time = millis();
return _select_key;
}
// Was long press, already handled, don't send click
return 0;
} else {
// Not SELECT button - handle other directions with debouncing
if (key != prev) {
unsigned long now = millis();
if ((now - _last_change_time) > _debounce_ms) {
prev = key;
_last_change_time = now;
return key;
}
}
}
return 0;
}
bool AnalogJoystick::isLongPress() {
return _long_press_triggered;
}
bool AnalogJoystick::isPressed() const {
if (_pin < 0) return false;
int adc_value = analogRead(_pin);
uint8_t key = findClosestKey(adc_value);
// Return true if any key is pressed (not idle/released)
return key != 0;
}

View file

@ -0,0 +1,37 @@
#pragma once
#include <Arduino.h>
class AnalogJoystick {
public:
struct JoyADCMapping {
int adc_value;
uint8_t key_code;
};
private:
int8_t _pin;
uint8_t prev;
int _tolerance;
unsigned long _debounce_ms;
unsigned long _last_change_time; // Long press tracking
uint8_t _select_key;
unsigned long _select_press_start;
bool _long_press_triggered;
unsigned long _long_press_ms;
JoyADCMapping *_mappings;
uint8_t _num_mappings;
uint8_t findClosestKey(int adc_value) const;
public:
AnalogJoystick(int8_t pin, JoyADCMapping *mappings, uint8_t num_mappings, uint8_t select_key_code,
unsigned long long_press_ms = 1000, int tolerance = 300, unsigned long debounce_ms = 50);
void begin();
uint8_t check();
bool isLongPress();
bool isPressed() const;
uint8_t getPin() const { return _pin; }
uint8_t getCurrentKey() const { return prev; }
};

View file

@ -0,0 +1,92 @@
#include "SH1115Display.h"
#include <Adafruit_GrayOLED.h>
//#include "Adafruit_SH110X.h"
#include <Adafruit_SH1115.h>
bool SH1115Display::i2c_probe(TwoWire &wire, uint8_t addr)
{
wire.beginTransmission(addr);
uint8_t error = wire.endTransmission();
return (error == 0);
}
bool SH1115Display::begin()
{
return display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS);
}
void SH1115Display::turnOn()
{
display.oled_command(SH110X_DISPLAYON);
_isOn = true;
}
void SH1115Display::turnOff()
{
display.oled_command(SH110X_DISPLAYOFF);
_isOn = false;
}
void SH1115Display::clear()
{
display.clearDisplay();
display.display();
}
void SH1115Display::startFrame(Color bkg)
{
display.clearDisplay(); // TODO: apply 'bkg'
_color = SH110X_WHITE;
display.setTextColor(_color);
display.setTextSize(1);
display.cp437(true); // Use full 256 char 'Code Page 437' font
}
void SH1115Display::setTextSize(int sz)
{
display.setTextSize(sz);
}
void SH1115Display::setColor(Color c)
{
_color = (c != 0) ? SH110X_WHITE : SH110X_BLACK;
display.setTextColor(_color);
}
void SH1115Display::setCursor(int x, int y)
{
display.setCursor(x, y);
}
void SH1115Display::print(const char *str)
{
display.print(str);
}
void SH1115Display::fillRect(int x, int y, int w, int h)
{
display.fillRect(x, y, w, h, _color);
}
void SH1115Display::drawRect(int x, int y, int w, int h)
{
display.drawRect(x, y, w, h, _color);
}
void SH1115Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h)
{
display.drawBitmap(x, y, bits, w, h, SH110X_WHITE);
}
uint16_t SH1115Display::getTextWidth(const char *str)
{
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void SH1115Display::endFrame()
{
display.display();
}

View file

@ -0,0 +1,44 @@
#pragma once
#include "DisplayDriver.h"
#include <Adafruit_GFX.h>
#include <Wire.h>
#define SH110X_NO_SPLASH
// #include <Adafruit_SH110X.h>
#include <Adafruit_SH1115.h>
#ifndef PIN_OLED_RESET
#define PIN_OLED_RESET -1
#endif
#ifndef DISPLAY_ADDRESS
#define DISPLAY_ADDRESS 0x3C
#endif
class SH1115Display : public DisplayDriver {
Adafruit_SH1115 display;
bool _isOn;
uint8_t _color;
bool i2c_probe(TwoWire &wire, uint8_t addr);
public:
SH1115Display() : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET) { _isOn = false; }
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char *str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
uint16_t getTextWidth(const char *str) override;
void endFrame() override;
};

View file

@ -0,0 +1,137 @@
#pragma once
#include <Adafruit_NeoPixel.h>
#include <Arduino.h>
#include <helpers/ESP32Board.h>
#include <helpers/ui/AnalogJoystick.h>
#define RADIOMASTER_900_BANDIT
#define DISPLAY_CLASS SH1115Display
extern Adafruit_NeoPixel pixels;
// User-definable TX LED color (RGB hex format)
// Examples: 0x00FF00 (green), 0xFF0000 (red), 0x0000FF (blue)
#ifndef TX_LED_COLOR
#define TX_LED_COLOR 0x009600 // Default: Green (0, 150, 0)
#endif
// Extract RGB components from hex color for TX LED
#define TX_LED_RED ((TX_LED_COLOR >> 16) & 0xFF)
#define TX_LED_GREEN ((TX_LED_COLOR >> 8) & 0xFF)
#define TX_LED_BLUE (TX_LED_COLOR & 0xFF)
/*
6 x Neopixels, GRB
GPIO 15
Backgroundlight button 1 at index 0
Backgroundlight button 2 at index 1
Button 1 at GPIO 34 - UNUSED
Button 2 at GPIO 35 - UNUSED
STK8XXX Accelerometer I2C address 0x18 and Interrupt at GPIO 37
*/
/*
Pin connections from ESP32-D0WDQ6 to SX1276.
*/
#define P_LORA_DIO_0 22
#define P_LORA_DIO_1 21
#define P_LORA_NSS 4
#define P_LORA_RESET 5
#define P_LORA_SCLK 18
#define P_LORA_MISO 19
#define P_LORA_MOSI 23
#define SX176X_TXEN 33
/*
I2C SDA and SCL.
*/
#define PIN_BOARD_SDA 14
#define PIN_BOARD_SCL 12
/*
This unit has a FAN built-in.
FAN is active at 250mW on it's ExpressLRS Firmware.
Always ON
*/
#define PA_FAN_EN 2 // FAN on GPIO 2
/*
This module has Skyworks SKY66122 controlled by dacWrite
power rangeing from 100mW to 1000mW.
Mapping of PA_LEVEL to Power output: GPIO26/dacWrite
168 -> 100mW -> 2.11v
148 -> 250mW -> 1.87v
128 -> 500mW -> 1.63v
90 -> 1000mW -> 1.16v
*/
#define DAC_PA_PIN 26 // GPIO 26 enables the PA
// Configuration - adjust these for your hardware
#define PA_CONSTANT_GAIN 18 // SKY66122 operates at constant 18dB gain
#define MIN_OUTPUT_DBM 20 // 100mW minimum
#define MAX_OUTPUT_DBM 30 // 1000mW maximum
// Calibration points from manufacturer
struct PowerCalibration {
uint8_t output_dbm;
int8_t sx1278_dbm;
uint8_t dac_value;
};
// Values are from Radiomaster.
const PowerCalibration calibration[] = {
{ 20, 2, 165 }, // 100mW
{ 24, 6, 155 }, // 250mW
{ 27, 9, 142 }, // 500mW
{ 30, 10, 110 } // 1000mW
};
const int NUM_CAL_POINTS = sizeof(calibration) / sizeof(calibration[0]);
class BanditBoard : public ESP32Board {
private:
public:
void begin() {
ESP32Board::begin();
pixels.begin();
pixels.clear();
// pixels.setPixelColor(0, pixels.Color(255, 0, 0));
// pixels.setPixelColor(1, pixels.Color(0, 255, 0));
pixels.show();
}
// Return fake battery status, battery/fixed power is not monitored.
uint16_t getBattMilliVolts() override { return 5000; }
const char *getManufacturerName() const override { return "RadioMaster Bandit"; }
// Add wake-enabled power off
void powerOff() override {
#if defined(PIN_USER_BTN)
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_USER_BTN, LOW); // Wake when button pressed (LOW)
#elif defined(PIN_USER_JOYSTICK)
// For analog joystick, you'd need to use the center button GPIO
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_USER_JOYSTICK, LOW);
#endif
// Enter deep sleep
esp_deep_sleep_start();
}
void onBeforeTransmit() override {
// Use user-defined TX LED color
for (byte i = 2; i < NEOPIXEL_NUM; i++) {
pixels.setPixelColor(i, pixels.Color(TX_LED_RED, TX_LED_GREEN, TX_LED_BLUE));
}
pixels.show();
}
void onAfterTransmit() override {
for (byte i = 2; i < NEOPIXEL_NUM; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
pixels.show();
}
};

View file

@ -0,0 +1,276 @@
# NeoPixel Color Examples & Recommendations
## 🎨 Complete Color Palette
### Primary Colors
```
Red: 0xFF0000 🔴 High urgency, attention-grabbing
Green: 0x00FF00 🟢 Success, go, nature
Blue: 0x0000FF 🔵 Calm, default, trust (DEFAULT)
```
### Secondary Colors
```
Yellow: 0xFFFF00 🟡 Warning, caution, bright
Cyan: 0x00FFFF ⚡ Cool, techy, calming
Magenta: 0xFF00FF 💜 Unique, stands out
```
### Extended Palette
```
Orange: 0xFF8000 🟠 Warm, friendly
Purple: 0x8000FF 🟣 Creative, gentle
Pink: 0xFF1493 💗 Soft, playful
Hot Pink: 0xFF69B4 🎀 Very noticeable
Lime: 0xBFFF00 🟢 Fresh, energetic
Teal: 0x008080 🌊 Sophisticated
Navy: 0x000080 🌑 Subtle, professional
Maroon: 0x800000 🍷 Rich, subdued
Olive: 0x808000 🫒 Natural, earthy
Aqua: 0x00FF88 💎 Tropical, bright
Violet: 0x8800FF 🔮 Mystical
Coral: 0xFF7F50 🐠 Warm, inviting
Turquoise: 0x40E0D0 🏖️ Refreshing
Gold: 0xFFD700 ⭐ Valuable, special
Silver: 0xC0C0C0 🤖 Modern, sleek
```
### White Variants
```
Pure White: 0xFFFFFF ⚪ Maximum brightness
Warm White: 0xFFCC88 🕯️ Cozy, comfortable
Cool White: 0xCCDDFF ❄️ Clinical, clear
Soft White: 0xFFEEDD 🌟 Gentle, easy on eyes
```
## 📋 Use Case Recommendations
### High Priority / Urgent Messages
**Best choices:**
- `0xFF0000` (Red) - Classic alert color
- `0xFF00FF` (Magenta) - Unusual, catches attention
- `0xFFFF00` (Yellow) - Warning/caution
- `0xFF1493` (Hot Pink) - Very noticeable
**Settings for urgent:**
```ini
build_flags =
-DNEW_MSG_LED=0xFF0000 ; Red
-DNEOPIXEL_MAX_BRIGHTNESS=120 ; Brighter
-DNEOPIXEL_MSG_UPDATE_MILLIS=20 ; Faster breathing
```
### Normal Priority Messages
**Best choices:**
- `0x0000FF` (Blue) - Default, professional
- `0x00FFFF` (Cyan) - Visible but calm
- `0x8000FF` (Purple) - Distinctive yet gentle
- `0x00FF00` (Green) - Positive feeling
**Settings for normal:**
```ini
build_flags =
-DNEW_MSG_LED=0x0000FF ; Blue
-DNEOPIXEL_MAX_BRIGHTNESS=80 ; Standard
-DNEOPIXEL_MSG_UPDATE_MILLIS=30 ; Normal speed
```
### Low Distraction / Ambient
**Best choices:**
- `0x8000FF` (Purple) - Subtle
- `0x008080` (Teal) - Sophisticated
- `0x40E0D0` (Turquoise) - Calming
- `0xFF7F50` (Coral) - Warm but not harsh
**Settings for ambient:**
```ini
build_flags =
-DNEW_MSG_LED=0x8000FF ; Purple
-DNEOPIXEL_MAX_BRIGHTNESS=40 ; Dim
-DNEOPIXEL_MSG_UPDATE_MILLIS=50 ; Slow breathing
```
### Battery Saving
**Best choices (single LED color):**
- `0xFF0000` (Red) - Only red LED
- `0x00FF00` (Green) - Only green LED
- `0x0000FF` (Blue) - Only blue LED
**Avoid for battery:**
- ❌ `0xFFFFFF` (White) - All 3 LEDs at full
- ❌ `0xFFFF00` (Yellow) - 2 LEDs at full
- ❌ `0x00FFFF` (Cyan) - 2 LEDs at full
- ❌ `0xFF00FF` (Magenta) - 2 LEDs at full
**Battery-conscious settings:**
```ini
build_flags =
-DNEW_MSG_LED=0x00FF00 ; Green (single LED)
-DNEOPIXEL_MAX_BRIGHTNESS=60 ; Moderate brightness
-DNEOPIXEL_MIN_BRIGHTNESS=3 ; Very dim minimum
```
## 🎭 Themed Configurations
### "Stealth Mode" - Minimal Visibility
```ini
build_flags =
-DNEW_MSG_LED=0x000080 ; Navy blue
-DNEOPIXEL_MAX_BRIGHTNESS=30
-DNEOPIXEL_MIN_BRIGHTNESS=2
-DNEOPIXEL_MSG_UPDATE_MILLIS=60
```
### "Party Mode" - Maximum Attention
```ini
build_flags =
-DNEW_MSG_LED=0xFF00FF ; Magenta
-DNEOPIXEL_MAX_BRIGHTNESS=150
-DNEOPIXEL_MIN_BRIGHTNESS=10
-DNEOPIXEL_MSG_UPDATE_MILLIS=15
```
### "Professional" - Subtle Business Setting
```ini
build_flags =
-DNEW_MSG_LED=0x0000FF ; Blue
-DNEOPIXEL_MAX_BRIGHTNESS=60
-DNEOPIXEL_MSG_UPDATE_MILLIS=40
```
### "Emergency Services" - High Visibility Red
```ini
build_flags =
-DNEW_MSG_LED=0xFF0000 ; Red
-DNEOPIXEL_MAX_BRIGHTNESS=120
-DNEOPIXEL_MSG_UPDATE_MILLIS=15
```
### "Nature Lover" - Earth Tones
```ini
build_flags =
-DNEW_MSG_LED=0x00FF00 ; Green
-DNEOPIXEL_MAX_BRIGHTNESS=70
-DNEOPIXEL_MSG_UPDATE_MILLIS=35
```
### "Gamer RGB" - Vibrant Cyan
```ini
build_flags =
-DNEW_MSG_LED=0x00FFFF ; Cyan
-DNEOPIXEL_MAX_BRIGHTNESS=100
-DNEOPIXEL_MSG_UPDATE_MILLIS=25
```
## 🔬 Color Mixing Guide
Want a custom color? Mix Red, Green, and Blue:
```
Formula: 0xRRGGBB
RR = Red intensity (00 to FF in hex, 0 to 255 decimal)
GG = Green intensity (00 to FF in hex, 0 to 255 decimal)
BB = Blue intensity (00 to FF in hex, 0 to 255 decimal)
```
### Quick Mix Examples:
**Orange** (Red + half Green):
```
0xFF8000 = 0xFF (255 red) + 0x80 (128 green) + 0x00 (0 blue)
```
**Purple** (Red + Blue):
```
0x8000FF = 0x80 (128 red) + 0x00 (0 green) + 0xFF (255 blue)
```
**Pink** (Red + some Green + Blue):
```
0xFF69B4 = 0xFF (255 red) + 0x69 (105 green) + 0xB4 (180 blue)
```
### Hex to Decimal Quick Reference:
```
00 = 0 40 = 64 80 = 128 C0 = 192
20 = 32 60 = 96 A0 = 160 E0 = 224
30 = 48 70 = 112 B0 = 176 FF = 255
```
## 💡 Pro Tips
### Visibility in Different Lighting
- **Daylight/Bright**: Use high brightness (100-150) and saturated colors
- **Indoor/Office**: Medium brightness (60-80) with any color
- **Dark/Night**: Low brightness (30-50) to avoid being blinding
- **Outdoors**: Blue/Cyan show well in daylight, red at night
### Color Psychology
- **Red**: Urgency, danger, stop, important
- **Green**: Go, success, safe, nature
- **Blue**: Trust, calm, professional, default
- **Yellow**: Warning, caution, attention
- **Purple**: Creative, unique, mystical
- **Cyan**: Tech, modern, cool
- **Orange**: Friendly, energetic, warm
- **White**: Clean, pure, maximum visibility
### Multi-User Scenarios
If multiple people use devices, assign colors:
- User 1: Blue `0x0000FF`
- User 2: Green `0x00FF00`
- User 3: Red `0xFF0000`
- User 4: Magenta `0xFF00FF`
- User 5: Cyan `0x00FFFF`
## 🧪 Testing Your Color
1. Choose your hex color
2. Add to platformio.ini: `-DNEW_MSG_LED=0xYOURCOLOR`
3. Build and flash
4. Send yourself a test message
5. Observe the breathing effect
6. Adjust brightness/speed if needed
## 📱 Web Color Picker
Use any online color picker (search "RGB color picker") to find your perfect color:
1. Pick your color visually
2. Copy the RGB hex value (usually shown as #RRGGBB)
3. Change # to 0x (e.g., #00FF00 becomes 0x00FF00)
4. Use in your build configuration
## 🎨 Seasonal Colors
### Spring
```
Pastel Pink: 0xFFB6C1
Mint Green: 0x98FF98
Lavender: 0xE6E6FA
```
### Summer
```
Tropical Blue: 0x00CED1
Bright Yellow: 0xFFFF00
Coral: 0xFF7F50
```
### Autumn
```
Burnt Orange: 0xFF8C00
Rust: 0xB7410E
Gold: 0xFFD700
```
### Winter
```
Ice Blue: 0xAFEEEE
Snow White: 0xFFFAFA
Silver: 0xC0C0C0
```
---
Remember: The color appears as a breathing/pulsing animation, so it will cycle between dim and bright, making it more dynamic than a static color!

View file

@ -0,0 +1,286 @@
# LED Color Configuration Guide
## Overview
Your RadioMaster Bandit now has **two user-configurable LED colors**:
1. **TX_LED_COLOR** - Color during radio transmission (flash)
2. **NEW_MSG_LED** - Color for new message notifications (breathing effect)
## Quick Setup
### platformio.ini Configuration
```ini
[env:bandit]
build_flags =
-DTX_LED_COLOR=0x00FF00 ; Green transmission flash (default)
-DNEW_MSG_LED=0x0000FF ; Blue message breathing (default)
```
### Example Configurations
**Default (Green TX, Blue Messages):**
```ini
build_flags =
-DTX_LED_COLOR=0x009600 ; Green (0, 150, 0) - slightly dimmed
-DNEW_MSG_LED=0x0000FF ; Blue
```
**All Red:**
```ini
build_flags =
-DTX_LED_COLOR=0xFF0000 ; Red transmission
-DNEW_MSG_LED=0xFF0000 ; Red messages
```
**High Contrast (Red TX, Blue Messages):**
```ini
build_flags =
-DTX_LED_COLOR=0xFF0000 ; Red transmission
-DNEW_MSG_LED=0x0000FF ; Blue messages
```
**Professional (Blue TX, Cyan Messages):**
```ini
build_flags =
-DTX_LED_COLOR=0x0000FF ; Blue transmission
-DNEW_MSG_LED=0x00FFFF ; Cyan messages
```
**Stealth (Dim Purple for both):**
```ini
build_flags =
-DTX_LED_COLOR=0x400040 ; Dim purple transmission
-DNEW_MSG_LED=0x8000FF ; Purple messages
```
**High Visibility (Yellow TX, Magenta Messages):**
```ini
build_flags =
-DTX_LED_COLOR=0xFFFF00 ; Yellow transmission
-DNEW_MSG_LED=0xFF00FF ; Magenta messages
```
## Color Behavior
### TX_LED_COLOR (Transmission Flash)
- **When**: During radio transmission
- **NeoPixels**: 2-5 (indices 2, 3, 4, 5)
- **Duration**: Brief flash while transmitting
- **Effect**: Solid color, no animation
- **Default**: Green (0x009600 = RGB 0, 150, 0)
### NEW_MSG_LED (Message Notification)
- **When**: Unread messages exist
- **NeoPixels**: 2-5 (indices 2, 3, 4, 5)
- **Duration**: Continuous until messages read
- **Effect**: Smooth breathing/pulsing
- **Default**: Blue (0x0000FF)
### NeoPixel Layout
```
Index 0: Button 1 backlight (not controlled by these settings)
Index 1: Button 2 backlight (not controlled by these settings)
Index 2: TX flash / Message notification
Index 3: TX flash / Message notification
Index 4: TX flash / Message notification
Index 5: TX flash / Message notification
```
## Color Format
Both use RGB hex format: `0xRRGGBB`
- RR = Red (00-FF hex / 0-255 decimal)
- GG = Green (00-FF hex / 0-255 decimal)
- BB = Blue (00-FF hex / 0-255 decimal)
### Common Colors
| Color | Hex | Decimal RGB |
|-------|-----|-------------|
| Red | `0xFF0000` | 255, 0, 0 |
| Green | `0x00FF00` | 0, 255, 0 |
| Blue | `0x0000FF` | 0, 0, 255 |
| Yellow | `0xFFFF00` | 255, 255, 0 |
| Cyan | `0x00FFFF` | 0, 255, 255 |
| Magenta | `0xFF00FF` | 255, 0, 255 |
| White | `0xFFFFFF` | 255, 255, 255 |
| Orange | `0xFF8000` | 255, 128, 0 |
| Purple | `0x8000FF` | 128, 0, 255 |
### Brightness Control
You can dim colors by reducing the values:
**Full Brightness Green:** `0x00FF00` (0, 255, 0)
**Medium Green:** `0x008000` (0, 128, 0)
**Dim Green:** `0x004000` (0, 64, 0)
**Very Dim Green:** `0x002000` (0, 32, 0)
**Current default TX color:** `0x009600` (0, 150, 0) - medium-bright green
## Use Case Examples
### Emergency Services / High Visibility
```ini
build_flags =
-DTX_LED_COLOR=0xFF0000 ; Red transmission (urgent)
-DNEW_MSG_LED=0xFF0000 ; Red messages (urgent)
```
### Professional / Office
```ini
build_flags =
-DTX_LED_COLOR=0x004080 ; Dim blue transmission
-DNEW_MSG_LED=0x0000FF ; Blue messages
```
### Battery Saver (Single color LEDs)
```ini
build_flags =
-DTX_LED_COLOR=0x00FF00 ; Green only (1 LED per pixel)
-DNEW_MSG_LED=0x00FF00 ; Green only (1 LED per pixel)
```
### Nighttime / Low Light
```ini
build_flags =
-DTX_LED_COLOR=0x400000 ; Very dim red
-DNEW_MSG_LED=0x400000 ; Very dim red
```
### Color-Coded Operations
```ini
build_flags =
-DTX_LED_COLOR=0xFFFF00 ; Yellow = transmitting
-DNEW_MSG_LED=0x00FF00 ; Green = new message received
```
### Rainbow Theme
```ini
build_flags =
-DTX_LED_COLOR=0xFF00FF ; Magenta TX
-DNEW_MSG_LED=0x00FFFF ; Cyan messages
```
## Recommended Color Combinations
### Same Color (Unified Look)
- Both Blue: `TX=0x0000FF, MSG=0x0000FF`
- Both Green: `TX=0x00FF00, MSG=0x00FF00`
- Both Red: `TX=0xFF0000, MSG=0xFF0000`
### Contrasting (Easy to Distinguish)
- Red TX, Blue MSG: `TX=0xFF0000, MSG=0x0000FF`
- Green TX, Magenta MSG: `TX=0x00FF00, MSG=0xFF00FF`
- Yellow TX, Cyan MSG: `TX=0xFFFF00, MSG=0x00FFFF`
### Subtle (Professional)
- Dim Blue TX, Blue MSG: `TX=0x000080, MSG=0x0000FF`
- Dim Green TX, Cyan MSG: `TX=0x004000, MSG=0x00FFFF`
- Dim Purple TX, Purple MSG: `TX=0x400040, MSG=0x8000FF`
## Power Consumption Notes
**Most Efficient (Single LED color):**
- Red: `0xFF0000`
- Green: `0x00FF00`
- Blue: `0x0000FF`
**Medium Efficiency (Two LED colors):**
- Yellow: `0xFFFF00` (Red + Green)
- Cyan: `0x00FFFF` (Green + Blue)
- Magenta: `0xFF00FF` (Red + Blue)
**Least Efficient (All LEDs):**
- White: `0xFFFFFF` (Red + Green + Blue)
Lower brightness values also save power:
- `0x00FF00` = 255 brightness
- `0x008000` = 128 brightness (half power)
- `0x004000` = 64 brightness (quarter power)
## Testing Your Configuration
1. Set your colors in `platformio.ini`
2. Build and flash: `pio run -t upload`
3. **Test TX LED**: Send a message - should see brief flash in TX_LED_COLOR
4. **Test MSG LED**: Receive a message - should see breathing effect in NEW_MSG_LED
5. Adjust brightness/colors as needed
## Troubleshooting
**Colors look wrong:**
- Verify hex format: `0xRRGGBB` with `0x` prefix
- Check build flags are being applied (view compiler output)
- NeoPixels are GRB format, but code handles conversion
**TX flash too bright:**
- Reduce TX_LED_COLOR values (e.g., `0x00FF00``0x008000`)
**Message breathing too dim:**
- Adjust `NEOPIXEL_MAX_BRIGHTNESS` in UITask.cpp (default: 80)
**Want different brightness for TX vs MSG:**
- TX: Adjust the color value directly (e.g., `0x009600` is dimmer than `0x00FF00`)
- MSG: Adjust `NEOPIXEL_MAX_BRIGHTNESS` in code
## Advanced: Hex to RGB Conversion
If you want a specific RGB value:
**Formula:** `0xRRGGBB`
**Example - RGB(255, 128, 64) = Orange:**
1. Red = 255 = 0xFF
2. Green = 128 = 0x80
3. Blue = 64 = 0x40
4. Result = `0xFF8040`
**Hex conversion chart:**
```
0 = 0x00 64 = 0x40 128 = 0x80 192 = 0xC0
32 = 0x20 96 = 0x60 160 = 0xA0 224 = 0xE0
48 = 0x30 112 = 0x70 176 = 0xB0 255 = 0xFF
```
## Files Modified
1. **BanditBoard.h** - Added TX_LED_COLOR support
2. **UITask.h** - NeoPixel message notification support
3. **UITask.cpp** - NEW_MSG_LED color system
---
## Quick Copy-Paste Examples
### Default Configuration
```ini
-DTX_LED_COLOR=0x009600 -DNEW_MSG_LED=0x0000FF
```
### All Green
```ini
-DTX_LED_COLOR=0x00FF00 -DNEW_MSG_LED=0x00FF00
```
### All Red
```ini
-DTX_LED_COLOR=0xFF0000 -DNEW_MSG_LED=0xFF0000
```
### Red TX, Blue Messages
```ini
-DTX_LED_COLOR=0xFF0000 -DNEW_MSG_LED=0x0000FF
```
### Yellow TX, Magenta Messages
```ini
-DTX_LED_COLOR=0xFFFF00 -DNEW_MSG_LED=0xFF00FF
```
### Dim Everything (Stealth)
```ini
-DTX_LED_COLOR=0x202020 -DNEW_MSG_LED=0x404040
```

View file

@ -0,0 +1,190 @@
# Quick Reference: NeoPixel LED Configuration
## 🎨 Two Configurable LED Colors
### TX_LED_COLOR - Radio Transmission Flash
Color shown on NeoPixels 2-5 during transmission
### NEW_MSG_LED - Message Notification Breathing
Color shown on NeoPixels 2-5 when you have unread messages
## ⚡ Quick Setup
### platformio.ini
```ini
build_flags =
-DTX_LED_COLOR=0x00FF00 ; Green transmission flash
-DNEW_MSG_LED=0x0000FF ; Blue message breathing
```
### In Code
```cpp
#define TX_LED_COLOR 0x00FF00 // Green transmission
#define NEW_MSG_LED 0x0000FF // Blue messages
```
## 🌈 Popular Colors
| Color | Hex | Use For |
|-------|-----|---------|
| Blue | `0x0000FF` | Professional, calm |
| Green | `0x00FF00` | Success, go, battery-friendly |
| Red | `0xFF0000` | Urgent, stop, attention |
| Yellow | `0xFFFF00` | Warning, caution |
| Cyan | `0x00FFFF` | Modern, cool |
| Magenta | `0xFF00FF` | Unique, stands out |
| Orange | `0xFF8000` | Friendly, warm |
| Purple | `0x8000FF` | Subtle, creative |
| White | `0xFFFFFF` | Maximum visibility |
## 📋 Recommended Combinations
### Defaults
```ini
-DTX_LED_COLOR=0x009600 -DNEW_MSG_LED=0x0000FF
```
Green TX flash (dimmed), Blue message breathing
### All Green (Battery Saver)
```ini
-DTX_LED_COLOR=0x00FF00 -DNEW_MSG_LED=0x00FF00
```
### All Red (High Urgency)
```ini
-DTX_LED_COLOR=0xFF0000 -DNEW_MSG_LED=0xFF0000
```
### High Contrast
```ini
-DTX_LED_COLOR=0xFF0000 -DNEW_MSG_LED=0x0000FF
```
Red TX, Blue messages - easy to distinguish
### Professional
```ini
-DTX_LED_COLOR=0x0000FF -DNEW_MSG_LED=0x00FFFF
```
Blue TX, Cyan messages - business-friendly
### Stealth Mode
```ini
-DTX_LED_COLOR=0x202020 -DNEW_MSG_LED=0x404040
```
Very dim for low visibility
## 🔧 Files Modified
1. **BanditBoard.h** - TX_LED_COLOR support
2. **UITask.h** - NEW_MSG_LED variables
3. **UITask.cpp** - NEW_MSG_LED color system
## 📊 LED Behavior
| Event | NeoPixels 0-1 | NeoPixels 2-5 |
|-------|---------------|---------------|
| Radio TX | Unchanged | TX_LED_COLOR (flash) |
| New messages | Unchanged | NEW_MSG_LED (breathing) |
| No messages | Unchanged | OFF |
## 🎛️ Advanced Tweaks
### Dim TX Flash
```cpp
#define TX_LED_COLOR 0x004000 // Dim green (was 0x00FF00)
```
### Brighter Messages
```cpp
#define NEOPIXEL_MAX_BRIGHTNESS 120 // In UITask.cpp (was 80)
```
### Faster Breathing
```cpp
#define NEOPIXEL_MSG_UPDATE_MILLIS 20 // In UITask.cpp (was 30)
```
### Smoother Breathing
```cpp
#define NEOPIXEL_BRIGHTNESS_STEP 1 // In UITask.cpp (was 2)
```
## 🔋 Battery Considerations
**Most Efficient (single LED):**
- `0xFF0000` (Red only)
- `0x00FF00` (Green only)
- `0x0000FF` (Blue only)
**Less Efficient (multiple LEDs):**
- `0xFFFF00` (Yellow = Red + Green)
- `0x00FFFF` (Cyan = Green + Blue)
- `0xFF00FF` (Magenta = Red + Blue)
- `0xFFFFFF` (White = all three)
**Brightness matters:**
- `0x00FF00` = full brightness
- `0x008000` = half brightness (50% power)
- `0x004000` = quarter brightness (25% power)
## 💡 Color Format
`0xRRGGBB` where:
- RR = Red (00-FF hex)
- GG = Green (00-FF hex)
- BB = Blue (00-FF hex)
Examples:
- Orange: `0xFF8000` = RGB(255, 128, 0)
- Pink: `0xFF69B4` = RGB(255, 105, 180)
- Teal: `0x008080` = RGB(0, 128, 128)
## 🧪 Testing
1. Set colors in platformio.ini
2. Build: `pio run`
3. Flash: `pio run -t upload`
4. Test TX: Send a message → see TX_LED_COLOR flash
5. Test MSG: Have unread messages → see NEW_MSG_LED breathing
6. Adjust as needed
## ⚠️ Troubleshooting
**Wrong color?**
→ Check hex format: `0xRRGGBB` with `0x` prefix
**TX too bright?**
→ Lower TX_LED_COLOR values: `0x00FF00``0x008000`
**Messages too dim?**
→ Increase NEOPIXEL_MAX_BRIGHTNESS in UITask.cpp
**Colors not applying?**
→ Check compiler output for build flags
## 📝 Quick Copy Examples
### Green TX, Blue MSG (default)
```
-DTX_LED_COLOR=0x009600 -DNEW_MSG_LED=0x0000FF
```
### Red TX, Red MSG
```
-DTX_LED_COLOR=0xFF0000 -DNEW_MSG_LED=0xFF0000
```
### Yellow TX, Magenta MSG
```
-DTX_LED_COLOR=0xFFFF00 -DNEW_MSG_LED=0xFF00FF
```
### Cyan TX, Purple MSG
```
-DTX_LED_COLOR=0x00FFFF -DNEW_MSG_LED=0x8000FF
```
### Dim White TX, Bright White MSG
```
-DTX_LED_COLOR=0x404040 -DNEW_MSG_LED=0xFFFFFF
```

View file

@ -0,0 +1,96 @@
[radiomaster_900_bandit]
extends = esp32_base
board = radiomaster_bandit
build_flags =
${esp32_base.build_flags}
-I variants/radiomaster_900_bandit
-D RADIOMASTER_900_BANDIT
-D SX127X_CURRENT_LIMIT=120
-D RADIO_CLASS=CustomSX1276
-D WRAPPER_CLASS=CustomSX1276Wrapper
-D DISPLAY_CLASS=SH1115Display ; I've added the SH1115 driver to Adafruit_SH110x that is avaible on ny github @ https://github.com/gjelsoe/Adafruit_SH110x
; The SH1106 will work, but there are some small pixels misplacement. Haven't testet if SH1107 will work better.
-D NEW_MSG_LED=0x0000FF ; Blue New message notification LED color, to diable enter 0x000000.
-D TX_LED_COLOR=0x009600 ; Green TX LED color, to diable enter 0x000000.
-D PIN_NEOPIXEL=15 ; Data GPIO of the NEOPIXELS.
-D NEOPIXEL_NUM=6 ; Number of NEOPIXELS.
-D SX176X_TXEN=33
-D MAX_LORA_TX_POWER=30
-D LORA_TX_POWER=20
-D PIN_BOARD_SDA=14
-D PIN_BOARD_SCL=12
-D PIN_USER_JOYSTICK=39
-D ENV_INCLUDE_GPS=0
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/radiomaster_900_bandit>
+<helpers/ui/AnalogJoystick.cpp>
+<helpers/ui/SH1115Display.cpp>
+<helpers/sensors>
lib_deps =
${esp32_base.lib_deps}
adafruit/Adafruit NeoPixel@^1.10.0
https://github.com/gjelsoe/Adafruit_SH110x
[env:radiomaster_900_bandit_companion_radio_ble]
extends = radiomaster_900_bandit
build_flags =
${radiomaster_900_bandit.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=160
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D RADIOLIB_DEBUG_BASIC=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${radiomaster_900_bandit.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${radiomaster_900_bandit.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:radiomaster_900_bandit_repeater]
extends = radiomaster_900_bandit
build_flags =
${radiomaster_900_bandit.build_flags}
-D ADVERT_NAME='"Radiomaster Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D PERSISTANT_GPS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${radiomaster_900_bandit.build_src_filter}
+<../examples/simple_repeater>
lib_deps =
${radiomaster_900_bandit.lib_deps}
${esp32_ota.lib_deps}
[env:radiomaster_900_bandit_room_server]
extends = radiomaster_900_bandit
build_flags =
${radiomaster_900_bandit.build_flags}
-D ADVERT_NAME='"Radiomaster Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${radiomaster_900_bandit.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${radiomaster_900_bandit.lib_deps}
${esp32_ota.lib_deps}

View file

@ -0,0 +1,163 @@
#include "target.h"
#include <Arduino.h>
#include <helpers/ui/UIScreen.h>
#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel pixels(NEOPIXEL_NUM, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
BanditBoard board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#ifndef PIN_USER_BTN
#define PIN_USER_BTN (-1)
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
// MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#if defined(PIN_USER_JOYSTICK)
static AnalogJoystick::JoyADCMapping joystick_mappings[] = {
{ 0, KEY_DOWN }, { 1290, KEY_SELECT }, { 1961, KEY_LEFT },
{ 2668, KEY_RIGHT }, { 3227, KEY_UP }, { 4095, 0 } // IDLE
};
AnalogJoystick analog_joystick(PIN_USER_JOYSTICK, joystick_mappings, 6, KEY_SELECT);
#endif
#endif
bool radio_init() {
#ifdef PA_FAN_EN
pinMode(PA_FAN_EN, OUTPUT);
digitalWrite(PA_FAN_EN, 1);
#endif
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
/**
* Linear interpolation helper for integers
*/
int16_t lerp_int(uint8_t x, uint8_t x0, uint8_t x1, int16_t y0, int16_t y1) {
if (x1 == x0) return y0;
return y0 + ((int16_t)(x - x0) * (y1 - y0)) / (x1 - x0);
}
/**
* Set PA output power with automatic SX1276 and DAC calculation
*
* @param target_output_dbm Desired PA output power in dBm (20-30 dBm for 100-1000mW)
* @param clamp_to_range If true, clamp out-of-range values to min/max instead of failing
* @return true if successful, false if out of range and clamp_to_range is false
*/
bool setPAOutputPower(uint8_t target_output_dbm, bool clamp_to_range = true) {
// Validate and clamp range
if (target_output_dbm < MIN_OUTPUT_DBM) {
if (clamp_to_range) {
MESH_DEBUG_PRINT("Warning: Target %u dBm too low, clamping to min %u dBm\n", target_output_dbm,
MIN_OUTPUT_DBM);
target_output_dbm = MIN_OUTPUT_DBM;
} else {
MESH_DEBUG_PRINT("Error: Target %u dBm below minimum %u dBm\n", target_output_dbm, MIN_OUTPUT_DBM);
return false;
}
}
if (target_output_dbm > MAX_OUTPUT_DBM) {
if (clamp_to_range) {
MESH_DEBUG_PRINT("Warning: Target %u dBm too high, clamping to max %u dBm\n", target_output_dbm,
MAX_OUTPUT_DBM);
target_output_dbm = MAX_OUTPUT_DBM;
} else {
MESH_DEBUG_PRINT("Error: Target %u dBm above maximum %u dBm\n", target_output_dbm, MAX_OUTPUT_DBM);
return false;
}
}
// Find the calibration points to interpolate between
int lower_idx = 0;
int upper_idx = NUM_CAL_POINTS - 1;
for (int i = 0; i < NUM_CAL_POINTS - 1; i++) {
if (target_output_dbm >= calibration[i].output_dbm &&
target_output_dbm <= calibration[i + 1].output_dbm) {
lower_idx = i;
upper_idx = i + 1;
break;
}
}
// Handle exact matches
if (target_output_dbm == calibration[lower_idx].output_dbm) {
int8_t sx1278_power = calibration[lower_idx].sx1278_dbm;
uint8_t dac_value = calibration[lower_idx].dac_value;
radio.setOutputPower(sx1278_power, true); // RFO output
dacWrite(DAC_PA_PIN, dac_value);
MESH_DEBUG_PRINT("Set power: %u dBm (SX1278: %d dBm, DAC: %u)\n", target_output_dbm, sx1278_power,
dac_value);
return true;
}
// Linear interpolation between calibration points
uint8_t lower_out = calibration[lower_idx].output_dbm;
uint8_t upper_out = calibration[upper_idx].output_dbm;
// Interpolate SX1278 power (maintaining 18dB gain relationship)
int16_t sx1278_power = lerp_int(target_output_dbm, lower_out, upper_out, calibration[lower_idx].sx1278_dbm,
calibration[upper_idx].sx1278_dbm);
// Interpolate DAC value
int16_t dac_value = lerp_int(target_output_dbm, lower_out, upper_out, calibration[lower_idx].dac_value,
calibration[upper_idx].dac_value);
// Set the calculated values
radio.setOutputPower((int8_t)sx1278_power, true); // RFO output
dacWrite(DAC_PA_PIN, (uint8_t)dac_value);
MESH_DEBUG_PRINT("Set power: %u dBm (SX1278: %d dBm, DAC: %u)\n", target_output_dbm, sx1278_power,
dac_value);
return true;
}
void radio_set_tx_power(uint8_t dbm) {
setPAOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

View file

@ -0,0 +1,33 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
// #include <RadioLib.h>
#include <BanditBoard.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/radiolib/CustomSX1276Wrapper.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/AnalogJoystick.h>
#include <helpers/ui/SH1115Display.h>
#endif
extern BanditBoard board;
extern WRAPPER_CLASS radio_driver;
extern SensorManager sensors;
extern AutoDiscoverRTCClock rtc_clock;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
#if defined(PIN_USER_JOYSTICK)
extern AnalogJoystick analog_joystick;
#endif
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View file

@ -0,0 +1,86 @@
#pragma once
#include <Arduino.h>
#include <helpers/ESP32Board.h>
#include <helpers/ui/AnalogJoystick.h>
#define RADIOMASTER_900_BANDIT_NANO
/*
Pin connections from ESP32-D0WDQ6 to SX1276.
*/
#define P_LORA_DIO_0 22
#define P_LORA_DIO_1 21
#define P_LORA_NSS 4
#define P_LORA_RESET 5
#define P_LORA_SCLK 18
#define P_LORA_MISO 19
#define P_LORA_MOSI 23
#define SX176X_TXEN 33
/*
I2C SDA and SCL.
*/
#define PIN_BOARD_SDA 14
#define PIN_BOARD_SCL 12
/*
This unit has a FAN built-in.
FAN is active at 250mW on it's ExpressLRS Firmware.
Always ON
*/
#define PA_FAN_EN 2 // FAN on GPIO 2
/*
This module has Skyworks SKY66122 controlled by dacWrite
power rangeing from 100mW to 1000mW.
Mapping of PA_LEVEL to Power output: GPIO26/dacWrite
168 -> 100mW -> 2.11v
148 -> 250mW -> 1.87v
128 -> 500mW -> 1.63v
90 -> 1000mW -> 1.16v
*/
#define DAC_PA_PIN 26 // GPIO 26 controls the PA gain.
// Configuration - adjust these for your hardware
#define PA_CONSTANT_GAIN 18 // SKY66122 operates at constant 18dB gain
#define MIN_OUTPUT_DBM 20 // 100mW minimum
#define MAX_OUTPUT_DBM 30 // 1000mW maximum
// Calibration points from manufacturer
struct PowerCalibration {
uint8_t output_dbm;
int8_t sx1278_dbm;
uint8_t dac_value;
};
// Values are from Radiomaster.
const PowerCalibration calibration[] = {
{ 20, 2, 168 }, // 100mW
{ 24, 6, 148 }, // 250mW
{ 27, 9, 128 }, // 500mW
{ 30, 12, 90 } // 1000mW
};
const int NUM_CAL_POINTS = sizeof(calibration) / sizeof(calibration[0]);
class BanditNanoBoard : public ESP32Board {
private:
public:
// Add wake-enabled power off
void powerOff() override {
#if defined(PIN_USER_BTN)
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_USER_BTN, LOW); // Wake when button pressed (LOW)
#elif defined(PIN_USER_JOYSTICK)
// For analog joystick, you'd need to use the center button GPIO
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_USER_JOYSTICK, LOW);
#endif
// Enter deep sleep
esp_deep_sleep_start();
}
// Return fake battery status, battery/fixed power is not monitored.
uint16_t getBattMilliVolts() override { return 5000; }
const char *getManufacturerName() const override { return "RadioMaster Bandit Nano"; }
};

View file

@ -0,0 +1,101 @@
[radiomaster_900_bandit_nano]
extends = esp32_base
board = radiomaster_bandit
build_flags =
${esp32_base.build_flags}
-I variants/radiomaster_900_bandit_nano
-D RADIOMASTER_900_BANDIT_NANO
-D SX127X_CURRENT_LIMIT=120
-D RADIO_CLASS=CustomSX1276
-D WRAPPER_CLASS=CustomSX1276Wrapper
-D DISPLAY_CLASS=SSD1306Display
; -D P_LORA_DIO_1=21
; -D P_LORA_DIO_0=22
; -D P_LORA_NSS=4
; -D P_LORA_RESET=5
; -D P_LORA_SCLK=18
; -D P_LORA_MISO=19
; -D P_LORA_MOSI=23
-D SX176X_TXEN=33
-D MAX_LORA_TX_POWER=30
-D LORA_TX_POWER=20
-D P_LORA_TX_LED=15
-D PIN_BOARD_SDA=14
-D PIN_BOARD_SCL=12
-D PIN_USER_JOYSTICK=39
; -D PA_FAN_EN=2
; -D PIN_PA_EN=26
-D DISPLAY_ROTATION=2 ; Display is flipped
-D ENV_INCLUDE_GPS=0
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/radiomaster_900_bandit_nano>
+<helpers/ui/AnalogJoystick.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/sensors>
lib_deps =
${esp32_base.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
[env:radiomaster_900_bandit_nano_companion_radio_ble]
extends = radiomaster_900_bandit_nano
build_flags =
${radiomaster_900_bandit_nano.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=160
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D RADIOLIB_DEBUG_BASIC=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${radiomaster_900_bandit_nano.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${radiomaster_900_bandit_nano.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:radiomaster_900_bandit_nano_repeater]
extends = radiomaster_900_bandit_nano
build_flags =
${radiomaster_900_bandit_nano.build_flags}
-D ADVERT_NAME='"Radiomaster Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D PERSISTANT_GPS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${radiomaster_900_bandit_nano.build_src_filter}
+<../examples/simple_repeater>
lib_deps =
${radiomaster_900_bandit_nano.lib_deps}
${esp32_ota.lib_deps}
[env:radiomaster_900_bandit_nano_room_server]
extends = radiomaster_900_bandit_nano
build_flags =
${radiomaster_900_bandit_nano.build_flags}
-D ADVERT_NAME='"Radiomaster Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${radiomaster_900_bandit_nano.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${radiomaster_900_bandit_nano.lib_deps}
${esp32_ota.lib_deps}

View file

@ -0,0 +1,160 @@
#include "target.h"
#include <Arduino.h>
#include <helpers/ui/UIScreen.h>
BanditNanoBoard board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#ifndef PIN_USER_BTN
#define PIN_USER_BTN (-1)
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
// MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#if defined(PIN_USER_JOYSTICK)
static AnalogJoystick::JoyADCMapping joystick_mappings[] = {
{ 0, KEY_DOWN }, { 1290, KEY_SELECT }, { 1961, KEY_LEFT },
{ 2668, KEY_RIGHT }, { 3227, KEY_UP }, { 4095, 0 } // IDLE
};
AnalogJoystick analog_joystick(PIN_USER_JOYSTICK, joystick_mappings, 6, KEY_SELECT);
#endif
#endif
bool radio_init() {
#ifdef PA_FAN_EN
pinMode(PA_FAN_EN, OUTPUT);
digitalWrite(PA_FAN_EN, 1);
#endif
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
/**
* Linear interpolation helper for integers
*/
int16_t lerp_int(uint8_t x, uint8_t x0, uint8_t x1, int16_t y0, int16_t y1) {
if (x1 == x0) return y0;
return y0 + ((int16_t)(x - x0) * (y1 - y0)) / (x1 - x0);
}
/**
* Set PA output power with automatic SX1276 and DAC calculation
*
* @param target_output_dbm Desired PA output power in dBm (20-30 dBm for 100-1000mW)
* @param clamp_to_range If true, clamp out-of-range values to min/max instead of failing
* @return true if successful, false if out of range and clamp_to_range is false
*/
bool setPAOutputPower(uint8_t target_output_dbm, bool clamp_to_range = true) {
// Validate and clamp range
if (target_output_dbm < MIN_OUTPUT_DBM) {
if (clamp_to_range) {
MESH_DEBUG_PRINT("Warning: Target %u dBm too low, clamping to min %u dBm\n", target_output_dbm,
MIN_OUTPUT_DBM);
target_output_dbm = MIN_OUTPUT_DBM;
} else {
MESH_DEBUG_PRINT("Error: Target %u dBm below minimum %u dBm\n", target_output_dbm, MIN_OUTPUT_DBM);
return false;
}
}
if (target_output_dbm > MAX_OUTPUT_DBM) {
if (clamp_to_range) {
MESH_DEBUG_PRINT("Warning: Target %u dBm too high, clamping to max %u dBm\n", target_output_dbm,
MAX_OUTPUT_DBM);
target_output_dbm = MAX_OUTPUT_DBM;
} else {
MESH_DEBUG_PRINT("Error: Target %u dBm above maximum %u dBm\n", target_output_dbm, MAX_OUTPUT_DBM);
return false;
}
}
// Find the calibration points to interpolate between
int lower_idx = 0;
int upper_idx = NUM_CAL_POINTS - 1;
for (int i = 0; i < NUM_CAL_POINTS - 1; i++) {
if (target_output_dbm >= calibration[i].output_dbm &&
target_output_dbm <= calibration[i + 1].output_dbm) {
lower_idx = i;
upper_idx = i + 1;
break;
}
}
// Handle exact matches
if (target_output_dbm == calibration[lower_idx].output_dbm) {
int8_t sx1278_power = calibration[lower_idx].sx1278_dbm;
uint8_t dac_value = calibration[lower_idx].dac_value;
radio.setOutputPower(sx1278_power, true); // RFO output
dacWrite(DAC_PA_PIN, dac_value);
MESH_DEBUG_PRINT("Set power: %u dBm (SX1278: %d dBm, DAC: %u)\n", target_output_dbm, sx1278_power,
dac_value);
return true;
}
// Linear interpolation between calibration points
uint8_t lower_out = calibration[lower_idx].output_dbm;
uint8_t upper_out = calibration[upper_idx].output_dbm;
// Interpolate SX1278 power (maintaining 18dB gain relationship)
int16_t sx1278_power = lerp_int(target_output_dbm, lower_out, upper_out, calibration[lower_idx].sx1278_dbm,
calibration[upper_idx].sx1278_dbm);
// Interpolate DAC value
int16_t dac_value = lerp_int(target_output_dbm, lower_out, upper_out, calibration[lower_idx].dac_value,
calibration[upper_idx].dac_value);
// Set the calculated values
radio.setOutputPower((int8_t)sx1278_power, true); // RFO output
dacWrite(DAC_PA_PIN, (uint8_t)dac_value);
MESH_DEBUG_PRINT("Set power: %u dBm (SX1278: %d dBm, DAC: %u)\n", target_output_dbm, sx1278_power,
dac_value);
return true;
}
void radio_set_tx_power(uint8_t dbm) {
setPAOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

View file

@ -0,0 +1,35 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
// #include <RadioLib.h>
#include <BanditNanoBoard.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/radiolib/CustomSX1276Wrapper.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/AnalogJoystick.h>
// #include <helpers/ui/MomentaryButton.h>
#include <helpers/ui/SSD1306Display.h>
#endif
extern BanditNanoBoard board;
extern WRAPPER_CLASS radio_driver;
extern SensorManager sensors;
extern AutoDiscoverRTCClock rtc_clock;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
// extern MomentaryButton user_btn;
#if defined(PIN_USER_JOYSTICK)
extern AnalogJoystick analog_joystick;
#endif
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();