Add Radiomaster Bandit/Bandit Nano support

Added support for 5-Way analog joystick.
Added Custom Sh1115 OLED driver.
Added NeoPixels support for Radiomaster Bandit.

Power output 20-30 dbm (100mW-1000mW).

Changed so Analog joystick can be used in UI.
Changed so NeoPixels is used for new Message. (Color can be defined).

Radiomaster Bandit Micro uses the same code as Nano.
This commit is contained in:
gjelsoe 2026-02-11 19:45:59 +01:00
parent e33d93dc7f
commit 6c0da535b8
17 changed files with 1951 additions and 2 deletions

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"
@ -549,6 +569,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;
@ -666,6 +689,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 <= 6; 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 <= 6; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
pixels.show();
}
}
}
#endif
void UITask::setCurrScreen(UIScreen* c) {
curr = c;
_next_refresh = 100;
@ -701,6 +776,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
@ -758,6 +835,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);
@ -778,6 +882,10 @@ void UITask::loop() {
userLedHandler();
#ifdef RADIOMASTER_900_BANDIT
neopixelMsgHandler();
#endif
#ifdef PIN_BUZZER
if (buzzer.isPlaying()) buzzer.loop();
#endif
@ -806,6 +914,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
}
@ -845,6 +958,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,15 @@ class UITask : public AbstractUITask {
int last_led_increment = 0;
#endif
#ifdef PIN_USER_BTN_ANA
#ifdef RADIOMASTER_900_BANDIT
// NeoPixel message notification support
int neopixel_state = 0;
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 +68,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,127 @@
#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
};
inline Adafruit_NeoPixel pixels(NEOPIXEL_NUM, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
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"; }
void onBeforeTransmit() override {
// Use user-defined TX LED color
for (byte i = 2; i <= 6; 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 <= 6; 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,94 @@
[radiomaster_900_bandit]
extends = esp32_base
board = esp32dev
#upload_flags = --before default_reset
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>
board_upload.flash_size = 8MB
board_build.partitions = default_8MB.csv
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
board_build.upload.maximum_ram_size=2000000
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,160 @@
#include "target.h"
#include <Arduino.h>
#include <helpers/ui/UIScreen.h>
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,74 @@
#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:
// 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,99 @@
[radiomaster_900_bandit_nano]
extends = esp32_base
board = esp32dev
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>
board_upload.flash_size = 8MB
board_build.partitions = default_8MB.csv
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
;board_build.upload.maximum_ram_size=2000000
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();