Skip to content

Commit 3af8d68

Browse files
teancomclaude
andauthored
Add ADC resistor-ladder binary inputs (analog buttons) (#35)
* Add ADC resistor-ladder binary inputs (analog buttons) BinaryInputs input_type == 2: several buttons share one ADC pin, distinguished by voltage. Polled (no edge interrupt) and reported through the same MSD button byte as digital buttons, so hosts decode one uniform contract. Wire layout (num_buttons, id_base, N+1 LE uint16 descending thresholds in reserved[]) is documented in structs.h. device_control.cpp adds classify/register/poll with sample debounce, per-pin ADC attenuation, a static_assert guarding reserved[] capacity, and rejection of non-descending thresholds from any host. ESP32 only; NRF gets an empty poll stub. Verified on an ESP32-C3 (XTEINK X4): valid ladders register and report presses; a malformed (non-descending) config is skipped at init with a diagnostic while a valid ladder beside it still works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Reject out-of-range ADC-ladder id_base instead of masking it registerAdcLadder stored id_base raw and the poll path masked the reported id with & 0x07. A host sending id_base + count > 8 (3-bit id field) produced wrapped, colliding button ids that HA cannot disambiguate — silently, with no feedback. The firmware is the validation boundary for any host, not just py-opendisplay, so reject it. Also give the existing count and byte_index guards the same diagnostic the descending-threshold check already emits; previously they dropped a malformed config silently. Hardware-verified on XTEINK X4: a GPIO2 ladder with id_base=6, N=4 is now rejected ("exceeds 3-bit id space ... skipping") while a valid GPIO1 ladder registers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Move ADC ladder to input_type 3 (2 is reserved for switches) Per maintainer review on #35: BinaryInputs input_type == 2 is reserved for the host-side switch feature. Renumber the ADC resistor-ladder type to 3 and document 2 = switch in the struct contract. Wire layout and the reused reserved[] fields are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Log ADC ladder binary input summary * Fix ADC ladder diagnostics for canonical input fields * Use canonical button input type in button setup --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 67a83d3 commit 3af8d68

2 files changed

Lines changed: 147 additions & 1 deletion

File tree

‎src/config_parser.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,11 @@ void printConfigSummary(){
778778
writeSerial(" Invert: 0x" + String(globalConfig.binary_inputs[i].invert, HEX));
779779
writeSerial(" Pullups: 0x" + String(globalConfig.binary_inputs[i].pullups, HEX));
780780
writeSerial(" Pulldowns: 0x" + String(globalConfig.binary_inputs[i].pulldowns, HEX));
781+
if (globalConfig.binary_inputs[i].input_type == 3) {
782+
writeSerial(" ADC Ladder: count=" + String(globalConfig.binary_inputs[i].reserved[0]) +
783+
" idBase=" + String(globalConfig.binary_inputs[i].reserved[1]) +
784+
" byteIdx=" + String(globalConfig.binary_inputs[i].button_data_byte_index));
785+
}
781786
writeSerial("");
782787
}
783788
writeSerial("--- Touch Controllers (" + String(globalConfig.touch_controller_count) + ") ---");

‎src/device_control.cpp‎

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,137 @@ static void pollConfiguredPowerOffButtons() {
8181
}
8282
}
8383
}
84+
85+
// BinaryInputs.input_type wire values (see structs.h for the full contract).
86+
// 2 is reserved for switches (host-side feature); the ADC ladder uses 3.
87+
#define BINARY_INPUT_TYPE_ADC_LADDER 3
88+
89+
// --- ADC resistor-ladder buttons (e.g. XTEINK X4) -------------------------
90+
// Several buttons share one ADC pin via a resistor ladder, distinguished by
91+
// voltage. They have no edge interrupt, so they are polled. Reported through
92+
// the same MSD button byte as digital buttons for a uniform host contract.
93+
#define MAX_ADC_LADDERS 4
94+
#define MAX_LADDER_BUTTONS 4 // reserved[] holds at most N+1 = 5 LE uint16 thresholds
95+
#define ADC_LADDER_POLL_MS 5
96+
#define ADC_LADDER_DEBOUNCE 3 // consecutive equal samples required to accept a change
97+
98+
// Thresholds for N+1 buttons must fit in BinaryInputs.reserved[]; fail the build if not.
99+
static_assert(2 + 2 * (MAX_LADDER_BUTTONS + 1) <= sizeof(BinaryInputs::reserved),
100+
"ADC ladder thresholds would overflow BinaryInputs.reserved[]");
101+
102+
struct AdcLadder {
103+
uint8_t pin;
104+
uint8_t num_buttons;
105+
uint8_t id_base;
106+
uint8_t byte_index;
107+
uint16_t thresholds[MAX_LADDER_BUTTONS + 1]; // descending; [0] = idle ceiling
108+
int8_t current_button; // -1 = none pressed
109+
int8_t candidate_button; // debounce: last raw classification
110+
uint8_t candidate_count; // consecutive samples equal to candidate
111+
uint8_t press_count; // 0-15, increments per press (5 s reset window)
112+
uint8_t last_button_id; // id of most recent press (for clean release reporting)
113+
uint32_t last_press_time;
114+
};
115+
static AdcLadder adcLadders[MAX_ADC_LADDERS];
116+
static uint8_t adcLadderCount = 0;
117+
118+
// Returns button index 0..num_buttons-1, or -1 when nothing is pressed.
119+
static int classifyAdcLadder(int adc, const AdcLadder* l) {
120+
if (adc > (int)l->thresholds[0]) return -1; // above idle ceiling
121+
for (uint8_t i = 0; i < l->num_buttons; i++) {
122+
if (adc > (int)l->thresholds[i + 1]) return (int)i; // thr[i+1] < adc <= thr[i]
123+
}
124+
return (int)l->num_buttons - 1; // catch-all bottom bucket
125+
}
126+
127+
static void registerAdcLadder(const struct BinaryInputs* input) {
128+
if (adcLadderCount >= MAX_ADC_LADDERS) return;
129+
uint8_t n = input->reserved[0]; // num_buttons
130+
if (n == 0 || n > MAX_LADDER_BUTTONS) {
131+
writeSerial("ADC ladder: count " + String(n) + " out of range 1.." +
132+
String(MAX_LADDER_BUTTONS) + " on pin " + String(input->input_pin_1) + ", skipping", true);
133+
return;
134+
}
135+
if (input->button_data_byte_index > 10) { // index into the 11-byte MSD block
136+
writeSerial("ADC ladder: byte_index " + String(input->button_data_byte_index) +
137+
" out of range 0..10 on pin " + String(input->input_pin_1) + ", skipping", true);
138+
return;
139+
}
140+
if ((int)input->reserved[1] + n > 8) { // 3-bit id field: id_base..id_base+n-1 must be <= 7
141+
writeSerial("ADC ladder: id_base " + String(input->reserved[1]) + " + count " + String(n) +
142+
" exceeds 3-bit id space on pin " + String(input->input_pin_1) + ", skipping", true);
143+
return;
144+
}
145+
AdcLadder* l = &adcLadders[adcLadderCount];
146+
l->pin = input->input_pin_1; // ADC GPIO
147+
l->num_buttons = n;
148+
l->id_base = input->reserved[1];
149+
l->byte_index = input->button_data_byte_index;
150+
for (uint8_t k = 0; k <= n; k++) {
151+
l->thresholds[k] = (uint16_t)input->reserved[2 + 2 * k] |
152+
((uint16_t)input->reserved[3 + 2 * k] << 8);
153+
}
154+
for (uint8_t k = 0; k < n; k++) { // contract: thresholds strictly descending
155+
if (l->thresholds[k] <= l->thresholds[k + 1]) { // reject malformed config from any host
156+
writeSerial("ADC ladder: thresholds not strictly descending on pin " +
157+
String(input->input_pin_1) + ", skipping", true);
158+
return;
159+
}
160+
}
161+
l->current_button = -1;
162+
l->candidate_button = -1;
163+
l->candidate_count = 0;
164+
l->press_count = 0;
165+
l->last_button_id = (uint8_t)(l->id_base & 0x07);
166+
l->last_press_time = 0;
167+
pinMode(l->pin, INPUT);
168+
analogSetPinAttenuation(l->pin, ADC_11db);
169+
adcLadderCount++;
170+
writeSerial("ADC ladder: pin " + String(l->pin) + " n " + String(n) +
171+
" idBase " + String(l->id_base) + " byteIdx " + String(l->byte_index), true);
172+
}
173+
174+
static void pollAdcButtons() {
175+
if (adcLadderCount == 0) return;
176+
static uint32_t lastPoll = 0;
177+
uint32_t now = millis();
178+
if (now - lastPoll < ADC_LADDER_POLL_MS) return;
179+
lastPoll = now;
180+
for (uint8_t i = 0; i < adcLadderCount; i++) {
181+
AdcLadder* l = &adcLadders[i];
182+
int adc = analogRead(l->pin);
183+
int btn = classifyAdcLadder(adc, l);
184+
if (btn == l->candidate_button) {
185+
if (l->candidate_count < 255) l->candidate_count++;
186+
} else {
187+
l->candidate_button = (int8_t)btn;
188+
l->candidate_count = 1;
189+
}
190+
if (l->candidate_count < ADC_LADDER_DEBOUNCE) continue; // not yet stable
191+
if (btn == l->current_button) continue; // no change
192+
uint8_t state;
193+
if (btn >= 0) {
194+
if (l->last_press_time == 0 || now - l->last_press_time > 5000) l->press_count = 0;
195+
l->press_count = (uint8_t)((l->press_count + 1) & 0x0F);
196+
l->last_press_time = now;
197+
l->last_button_id = (uint8_t)((l->id_base + btn) & 0x07);
198+
state = 1;
199+
} else {
200+
state = 0; // released; last_button_id identifies which button
201+
}
202+
l->current_button = (int8_t)btn;
203+
uint8_t data = (uint8_t)((l->last_button_id & 0x07) |
204+
((l->press_count & 0x0F) << 3) |
205+
((state & 0x01) << 7));
206+
if (l->byte_index < 11) dynamicreturndata[l->byte_index] = data;
207+
updatemsdata();
208+
writeSerial("ADC btn pin " + String(l->pin) + " adc=" + String(adc) + " idx=" + String(btn) +
209+
" id=" + String(l->last_button_id) + " cnt=" + String(l->press_count) +
210+
" state=" + String(state), true);
211+
}
212+
}
213+
#else
214+
static void pollAdcButtons() {}
84215
#endif
85216

86217
void connect_callback(uint16_t conn_handle) {
@@ -453,6 +584,7 @@ void processButtonEvents() {
453584
#ifdef TARGET_ESP32
454585
pollConfiguredPowerOffButtons();
455586
#endif
587+
pollAdcButtons();
456588
if (buttonEventPending) {
457589
noInterrupts();
458590
buttonEventPending = false;
@@ -593,10 +725,19 @@ void initButtons() {
593725
buttonStates[i].power_off = false;
594726
buttonStates[i].power_off_hold_ms = 0;
595727
}
728+
#ifdef TARGET_ESP32
729+
adcLadderCount = 0;
730+
#endif
596731
if (globalConfig.binary_input_count == 0) return;
597732
for (uint8_t instanceIdx = 0; instanceIdx < globalConfig.binary_input_count; instanceIdx++) {
598733
struct BinaryInputs* input = &globalConfig.binary_inputs[instanceIdx];
599-
if (input->input_type != 1) continue;
734+
#ifdef TARGET_ESP32
735+
if (input->input_type == BINARY_INPUT_TYPE_ADC_LADDER) {
736+
registerAdcLadder(input);
737+
continue;
738+
}
739+
#endif
740+
if (input->input_type != OD_INPUT_TYPE_BUTTON) continue;
600741
if (input->button_data_byte_index > 10) continue;
601742
uint16_t instanceHoldMs = (input->power_off_hold_sec == 0) ? 3000u : (uint16_t)input->power_off_hold_sec * 1000u;
602743
uint8_t* instancePins[8] = {

0 commit comments

Comments
 (0)