Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 145 additions & 1 deletion src/device_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,140 @@ struct ButtonState {
#define MAX_BUTTONS 32
extern ButtonState buttonStates[MAX_BUTTONS];

// BinaryInputs.input_type wire values (see structs.h for the full contract).
// 2 is reserved for switches (host-side feature); the ADC ladder uses 3.
#define BINARY_INPUT_TYPE_DIGITAL 1
#define BINARY_INPUT_TYPE_ADC_LADDER 3

// --- ADC resistor-ladder buttons (e.g. XTEINK X4) -------------------------
// Several buttons share one ADC pin via a resistor ladder, distinguished by
// voltage. They have no edge interrupt, so they are polled. Reported through
// the same MSD button byte as digital buttons for a uniform host contract.
#ifdef TARGET_ESP32
#define MAX_ADC_LADDERS 4
#define MAX_LADDER_BUTTONS 5 // reserved[] holds at most N+1 = 6 LE uint16 thresholds
#define ADC_LADDER_POLL_MS 5
#define ADC_LADDER_DEBOUNCE 3 // consecutive equal samples required to accept a change

// Thresholds for N+1 buttons must fit in BinaryInputs.reserved[]; fail the build if not.
static_assert(2 + 2 * (MAX_LADDER_BUTTONS + 1) <= sizeof(BinaryInputs::reserved),
"ADC ladder thresholds would overflow BinaryInputs.reserved[]");

struct AdcLadder {
uint8_t pin;
uint8_t num_buttons;
uint8_t id_base;
uint8_t byte_index;
uint16_t thresholds[MAX_LADDER_BUTTONS + 1]; // descending; [0] = idle ceiling
int8_t current_button; // -1 = none pressed
int8_t candidate_button; // debounce: last raw classification
uint8_t candidate_count; // consecutive samples equal to candidate
uint8_t press_count; // 0-15, increments per press (5 s reset window)
uint8_t last_button_id; // id of most recent press (for clean release reporting)
uint32_t last_press_time;
};
static AdcLadder adcLadders[MAX_ADC_LADDERS];
static uint8_t adcLadderCount = 0;

// Returns button index 0..num_buttons-1, or -1 when nothing is pressed.
static int classifyAdcLadder(int adc, const AdcLadder* l) {
if (adc > (int)l->thresholds[0]) return -1; // above idle ceiling
for (uint8_t i = 0; i < l->num_buttons; i++) {
if (adc > (int)l->thresholds[i + 1]) return (int)i; // thr[i+1] < adc <= thr[i]
}
return (int)l->num_buttons - 1; // catch-all bottom bucket
}

static void registerAdcLadder(const struct BinaryInputs* input) {
if (adcLadderCount >= MAX_ADC_LADDERS) return;
uint8_t n = input->reserved[0]; // num_buttons
if (n == 0 || n > MAX_LADDER_BUTTONS) {
writeSerial("ADC ladder: count " + String(n) + " out of range 1.." +
String(MAX_LADDER_BUTTONS) + " on pin " + String(input->reserved_pin_1) + ", skipping", true);
return;
}
if (input->button_data_byte_index > 10) { // index into the 11-byte MSD block
writeSerial("ADC ladder: byte_index " + String(input->button_data_byte_index) +
" out of range 0..10 on pin " + String(input->reserved_pin_1) + ", skipping", true);
return;
}
if ((int)input->reserved[1] + n > 8) { // 3-bit id field: id_base..id_base+n-1 must be <= 7
writeSerial("ADC ladder: id_base " + String(input->reserved[1]) + " + count " + String(n) +
" exceeds 3-bit id space on pin " + String(input->reserved_pin_1) + ", skipping", true);
return;
}
AdcLadder* l = &adcLadders[adcLadderCount];
l->pin = input->reserved_pin_1; // ADC GPIO
l->num_buttons = n;
l->id_base = input->reserved[1];
l->byte_index = input->button_data_byte_index;
for (uint8_t k = 0; k <= n; k++) {
l->thresholds[k] = (uint16_t)input->reserved[2 + 2 * k] |
((uint16_t)input->reserved[3 + 2 * k] << 8);
}
for (uint8_t k = 0; k < n; k++) { // contract: thresholds strictly descending
if (l->thresholds[k] <= l->thresholds[k + 1]) { // reject malformed config from any host
writeSerial("ADC ladder: thresholds not strictly descending on pin " +
String(input->reserved_pin_1) + ", skipping", true);
return;
}
}
l->current_button = -1;
l->candidate_button = -1;
l->candidate_count = 0;
l->press_count = 0;
l->last_button_id = (uint8_t)(l->id_base & 0x07);
l->last_press_time = 0;
pinMode(l->pin, INPUT);
analogSetPinAttenuation(l->pin, ADC_11db);
adcLadderCount++;
writeSerial("ADC ladder: pin " + String(l->pin) + " n " + String(n) +
" idBase " + String(l->id_base) + " byteIdx " + String(l->byte_index), true);
}

static void pollAdcButtons() {
if (adcLadderCount == 0) return;
static uint32_t lastPoll = 0;
uint32_t now = millis();
if (now - lastPoll < ADC_LADDER_POLL_MS) return;
lastPoll = now;
for (uint8_t i = 0; i < adcLadderCount; i++) {
AdcLadder* l = &adcLadders[i];
int adc = analogRead(l->pin);
int btn = classifyAdcLadder(adc, l);
if (btn == l->candidate_button) {
if (l->candidate_count < 255) l->candidate_count++;
} else {
l->candidate_button = (int8_t)btn;
l->candidate_count = 1;
}
if (l->candidate_count < ADC_LADDER_DEBOUNCE) continue; // not yet stable
if (btn == l->current_button) continue; // no change
uint8_t state;
if (btn >= 0) {
if (l->last_press_time == 0 || now - l->last_press_time > 5000) l->press_count = 0;
l->press_count = (uint8_t)((l->press_count + 1) & 0x0F);
l->last_press_time = now;
l->last_button_id = (uint8_t)((l->id_base + btn) & 0x07);
state = 1;
} else {
state = 0; // released; last_button_id identifies which button
}
l->current_button = (int8_t)btn;
uint8_t data = (uint8_t)((l->last_button_id & 0x07) |
((l->press_count & 0x0F) << 3) |
((state & 0x01) << 7));
if (l->byte_index < 11) dynamicreturndata[l->byte_index] = data;
updatemsdata();
writeSerial("ADC btn pin " + String(l->pin) + " adc=" + String(adc) + " idx=" + String(btn) +
" id=" + String(l->last_button_id) + " cnt=" + String(l->press_count) +
" state=" + String(state), true);
}
}
#else
static void pollAdcButtons() {}
#endif

void connect_callback(uint16_t conn_handle) {
(void)conn_handle;
writeSerial("=== BLE CLIENT CONNECTED ===", true);
Expand Down Expand Up @@ -373,6 +507,7 @@ void handleLedStop(uint8_t* data, uint16_t len) {

void processButtonEvents() {
powerButtonPoll();
pollAdcButtons();
if (buttonEventPending) {
buttonEventPending = false;
uint32_t currentTime = millis();
Expand Down Expand Up @@ -531,10 +666,19 @@ void initButtons() {
buttonStates[i].pin = 0xFF;
buttonStates[i].instance_index = 0xFF;
}
#ifdef TARGET_ESP32
adcLadderCount = 0;
#endif
if (globalConfig.binary_input_count == 0) return;
for (uint8_t instanceIdx = 0; instanceIdx < globalConfig.binary_input_count; instanceIdx++) {
struct BinaryInputs* input = &globalConfig.binary_inputs[instanceIdx];
if (input->input_type != 1) continue;
#ifdef TARGET_ESP32
if (input->input_type == BINARY_INPUT_TYPE_ADC_LADDER) {
registerAdcLadder(input);
continue;
}
#endif
if (input->input_type != BINARY_INPUT_TYPE_DIGITAL) continue;
if (input->button_data_byte_index > 10) continue;
uint8_t* instancePins[8] = {
&input->reserved_pin_1,&input->reserved_pin_2,&input->reserved_pin_3,&input->reserved_pin_4,
Expand Down
19 changes: 18 additions & 1 deletion src/structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,26 @@ struct DataBus {
} __attribute__((packed));

// 0x25: binary_inputs (repeatable, max 4 instances)
//
// input_type == 1 (digital): each set reserved_pin_N is one button on its own
// GPIO, read with digitalRead + edge interrupt. invert/pullups/pulldowns apply
// per pin. All pins in the instance report into button_data_byte_index.
//
// input_type == 2 (switch): reserved for the host-side switch feature.
//
// input_type == 3 (ADC resistor ladder, e.g. XTEINK X4): several buttons share
// one ADC pin, distinguished by voltage; polled (no interrupt). Layout:
// reserved_pin_1 = ADC GPIO pin
// reserved[0] = num_buttons N (1..5)
// reserved[1] = button id base (button i reports id base+i, &7)
// reserved[2 + 2k .. +1] = threshold[k], LE uint16, k = 0..N (N+1 values)
// Thresholds descend; button i is pressed when thr[i+1] < adc <= thr[i], and
// nothing is pressed when adc > thr[0] (idle ceiling). thr[N] is the bottom
// floor (use 0). Reported into button_data_byte_index using the same byte
// format as digital buttons.
struct BinaryInputs {
uint8_t instance_number; // Unique index for multiple input blocks (0-based)
uint8_t input_type; // Input type enum
uint8_t input_type; // 1 = digital buttons, 2 = switch, 3 = ADC resistor ladder
uint8_t display_as; // How input should be represented in systems (enum)
uint8_t reserved_pin_1; // Reserved / spare pin 1
uint8_t reserved_pin_2; // Reserved / spare pin 2
Expand Down