Skip to content

Commit d764ff3

Browse files
fhirschmannclaude
andcommitted
feat(ir): add configurable long-press actions for IR remote buttons
Each learned IR button can now carry a second command that fires when the button is held, so a short tap and a long hold run different actions (like the physical buttons already do). IR sends no "released" event, so release is inferred from the repeat-frame stream: a held button keeps re-sending frames, and once they stop arriving (IR_HOLD_RELEASE_MS gap) the button counts as released. A button with a long action fires the long action once the hold passes a configurable threshold and the short action on release; buttons without a long action keep their previous instant behavior (volume still ramps while held). - IrMapping gains a longCmd byte; the NVS blob moves to key "irMap2" with a one-time migration from the legacy 3-byte "irMap" layout so existing remotes survive the update (a separate key avoids the 3-vs-4 byte stride ambiguity). - Threshold persisted in NVS ("irLongMs", default 600 ms, clamped 200-5000), configurable via Settings -> IR. Exposed as ir.longPressMs and per-entry ir.map[].longCmd in settingsToJSON / JSONToSettings, so it round-trips through GET/POST /settings and the full backup/restore. - Web UI: second "long press" command column (with a "none" option) plus a ms threshold input; de/en/fr strings; demo mock + README updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9c4efb6 commit d764ff3

10 files changed

Lines changed: 189 additions & 33 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
268268
| Don't resume a barely-started audiobook: if the card is pulled within the first few seconds (new **"Don't resume if pulled within (sec.)"** general setting, NVS key `minResumeSec`, default 20 s, 0 = off), the resume point is reset to the very start instead of saving the couple of seconds played — so a briefly-sampled card starts cleanly from the beginning next time rather than a few seconds in. Gated to the very first track of the position-saving audiobook modes, so genuine mid-book progress is never discarded | [`7cacec7`](../../commit/7cacec7) |
269269
| **Smart forward/backward seek with press-coalescing** (`CMD_SMART_FORWARDS` / `CMD_SMART_BACKWARDS`, action codes 190/191; also track-control codes 10/11 for MQTT): on an audiobook that is a single long file these commands seek *inside* the file; on a playlist of several files they fall back to next/previous track. Rapid presses are bundled into one jump so the decoder re-syncs once instead of once per press (the old per-press seek felt sluggish on FLAC and dropped presses during the multi-second resync). A forward jump past the end of a single file **clamps to a few seconds before the end and keeps playing** instead of ending the book (`setAudioPlayTime` instead of `setTimeOffset`'s `stopSong`). On a single-file audiobook this works even in the first second after a tap (before the duration is known), and the target is clamped to the `setAudioPlayTime` 16-bit range so files longer than ~18 h can't wrap. The step is configurable in the web UI (General → options → **"Smart seek step (sec.)"**, NVS key `seekStep`, default 300 s = 5 min). Bind them to physical buttons in the **Buttons** settings tab. The plain `CMD_SEEK_FORWARDS`/`BACKWARDS` 30 s jump (`jumpOffset`) and the upstream next/previous-track behavior are both unchanged | [`ba3628e`](../../commit/ba3628e) |
270270
| **Infrared remote control** enabled on the complete board: an IR receiver (e.g. VS1838B) on `IRLED_PIN` (GPIO 5) drives play/pause, next/previous, first/last track, volume, mute, shutdown, Bluetooth and FTP via the `RC_*` command codes in `settings-complete.h`. The mapping defaults to empty so an un-learned remote does nothing until its codes are entered | [`9dbe588`](../../commit/9dbe588) |
271+
| **IR long-press actions**: every learned IR button can carry a **second command that fires when the button is held** — short tap and long hold trigger different actions, just like the physical buttons. IR sends no "released" event, so the firmware infers release from the repeat-frame stream: the long action fires once the hold passes a **configurable threshold** (Settings → IR → **"Long-press threshold"**, default 600 ms, NVS key `irLongMs`), the short action fires on release. Buttons left without a long action keep firing instantly (volume still ramps while held). Set per button in the IR settings tab and carried through backup/restore | |
271272

272273
### Virtual RFID cards
273274

html/locales/de.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
"title": "Infrarot-Fernbedienung",
5959
"intro": "Lerne die Tasten deiner Fernbedienung an und ordne jeder ein Kommando zu. Klicke auf „Anlernen“ und drücke dann die Taste auf der Fernbedienung – der empfangene Code wird automatisch eingetragen. Nicht vergessen zu speichern.",
6060
"command": "Kommando",
61+
"longCommand": "Lang drücken",
62+
"longPressLabel": "Schwelle für langes Drücken",
63+
"longPressHint": "Wie lange eine Taste gehalten werden muss, um die Lang-Aktion auszulösen (200–5000 ms). Tasten ohne Lang-Aktion reagieren weiterhin sofort.",
6164
"code": "IR-Code",
6265
"learn": "Anlernen",
6366
"learnPrompt": "Drücke jetzt eine Taste auf der Fernbedienung…",
@@ -68,6 +71,7 @@
6871
"timeout": "Kein IR-Signal empfangen – Anlernen abgebrochen.",
6972
"duplicate": "Derselbe Code ist mehrfach zugeordnet; der erste Treffer gewinnt.",
7073
"cmd": {
74+
"none": "— (keine)",
7175
"playpause": "Wiedergabe / Pause",
7276
"next": "Nächster Titel",
7377
"prev": "Vorheriger Titel",

html/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
"title": "Infrared remote control",
5959
"intro": "Teach the buttons of your remote and assign each one a command. Click “Learn”, then press the button on the remote — the received code is filled in automatically. Don't forget to save.",
6060
"command": "Command",
61+
"longCommand": "Long press",
62+
"longPressLabel": "Long-press threshold",
63+
"longPressHint": "How long a button must be held to trigger the long-press action (200–5000 ms). Buttons without a long action still react instantly.",
6164
"code": "IR code",
6265
"learn": "Learn",
6366
"learnPrompt": "Press a button on the remote now…",
@@ -68,6 +71,7 @@
6871
"timeout": "No IR signal received — learning cancelled.",
6972
"duplicate": "The same code is mapped more than once; the first match wins.",
7073
"cmd": {
74+
"none": "— (none)",
7175
"playpause": "Play / Pause",
7276
"next": "Next track",
7377
"prev": "Previous track",

html/locales/fr.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
"title": "Télécommande infrarouge",
5959
"intro": "Apprenez les touches de votre télécommande et associez à chacune une commande. Cliquez sur « Apprendre », puis appuyez sur la touche de la télécommande — le code reçu est rempli automatiquement. N'oubliez pas d'enregistrer.",
6060
"command": "Commande",
61+
"longCommand": "Appui long",
62+
"longPressLabel": "Seuil d’appui long",
63+
"longPressHint": "Durée pendant laquelle une touche doit être maintenue pour déclencher l’action d’appui long (200–5000 ms). Les touches sans action longue réagissent toujours immédiatement.",
6164
"code": "Code IR",
6265
"learn": "Apprendre",
6366
"learnPrompt": "Appuyez maintenant sur une touche de la télécommande…",
@@ -68,6 +71,7 @@
6871
"timeout": "Aucun signal IR reçu — apprentissage annulé.",
6972
"duplicate": "Le même code est associé plusieurs fois ; la première correspondance l'emporte.",
7073
"cmd": {
74+
"none": "— (aucune)",
7175
"playpause": "Lecture / Pause",
7276
"next": "Piste suivante",
7377
"prev": "Piste précédente",

html/management.html

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3063,11 +3063,20 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
30633063
<fieldset id="irConfig">
30643064
<legend class="w-auto" data-i18n="ir.title"></legend>
30653065
<p class="small text-secondary" data-i18n="ir.intro"></p>
3066+
<div class="mb-3" style="max-width: 24rem;">
3067+
<label class="form-label" for="irLongPressMs" data-i18n="ir.longPressLabel"></label>
3068+
<div class="input-group">
3069+
<input type="number" class="form-control" id="irLongPressMs" min="200" max="5000" step="50" value="600">
3070+
<span class="input-group-text">ms</span>
3071+
</div>
3072+
<div class="form-text" data-i18n="ir.longPressHint"></div>
3073+
</div>
30663074
<div class="table-responsive">
30673075
<table class="table table-sm align-middle mb-2" id="irMappingTable">
30683076
<thead>
30693077
<tr>
30703078
<th data-i18n="ir.command"></th>
3079+
<th data-i18n="ir.longCommand"></th>
30713080
<th data-i18n="ir.code"></th>
30723081
<th class="text-end"></th>
30733082
</tr>
@@ -5683,6 +5692,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
56835692
let irSettings = settings.ir;
56845693
if (irSettings) {
56855694
document.getElementById('settings-ir-subtab').setAttribute('data-visible', true);
5695+
if (irSettings.longPressMs) { document.getElementById('irLongPressMs').value = irSettings.longPressMs; }
56865696
buildIrTable(irSettings.map || []);
56875697
}
56885698
// playlist
@@ -7915,37 +7925,50 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
79157925
}
79167926

79177927
// Build the command <select> for a row, marking `cmd` as selected.
7918-
function irBuildCommandSelect(cmd) {
7928+
function irBuildCommandSelect(cmd, includeNone) {
79197929
var sel = document.createElement('select');
7920-
sel.className = 'form-select form-select-sm ir-cmd';
7930+
sel.className = 'form-select form-select-sm ' + (includeNone ? 'ir-longcmd' : 'ir-cmd');
7931+
var num = (cmd === null || cmd === undefined) ? (includeNone ? 0 : null) : Number(cmd);
79217932
var known = false;
7933+
if (includeNone) {
7934+
var none = document.createElement('option');
7935+
none.value = 0;
7936+
none.setAttribute('data-i18n', 'ir.cmd.none');
7937+
none.textContent = i18next.t('ir.cmd.none');
7938+
if (num === 0) { none.selected = true; known = true; }
7939+
sel.appendChild(none);
7940+
}
79227941
IR_COMMANDS.forEach(function (c) {
79237942
var opt = document.createElement('option');
79247943
opt.value = c.cmd;
79257944
opt.setAttribute('data-i18n', c.key);
79267945
opt.textContent = i18next.t(c.key);
7927-
if (Number(cmd) === c.cmd) { opt.selected = true; known = true; }
7946+
if (num === c.cmd) { opt.selected = true; known = true; }
79287947
sel.appendChild(opt);
79297948
});
79307949
// Preserve an unknown/legacy command value so saving doesn't silently drop it.
7931-
if (!known && cmd !== null && cmd !== undefined) {
7950+
if (!known && num !== null && num !== 0) {
79327951
var keep = document.createElement('option');
7933-
keep.value = cmd;
7934-
keep.textContent = 'CMD ' + cmd;
7952+
keep.value = num;
7953+
keep.textContent = 'CMD ' + num;
79357954
keep.selected = true;
79367955
sel.insertBefore(keep, sel.firstChild);
79377956
}
79387957
return sel;
79397958
}
79407959

79417960
// Create one mapping row (<tr>) for the given code (number|null) and command.
7942-
function irMakeRow(code, cmd) {
7961+
function irMakeRow(code, cmd, longCmd) {
79437962
var tr = document.createElement('tr');
79447963

79457964
var tdCmd = document.createElement('td');
7946-
tdCmd.appendChild(irBuildCommandSelect(cmd));
7965+
tdCmd.appendChild(irBuildCommandSelect(cmd, false));
79477966
tr.appendChild(tdCmd);
79487967

7968+
var tdLong = document.createElement('td');
7969+
tdLong.appendChild(irBuildCommandSelect(longCmd, true));
7970+
tr.appendChild(tdLong);
7971+
79497972
var tdCode = document.createElement('td');
79507973
var codeSpan = document.createElement('span');
79517974
codeSpan.className = 'ir-code font-monospace';
@@ -7995,7 +8018,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
79958018
if (!body) { return; }
79968019
body.innerHTML = '';
79978020
(map || []).forEach(function (m) {
7998-
body.appendChild(irMakeRow(m.code, m.cmd));
8021+
body.appendChild(irMakeRow(m.code, m.cmd, m.longCmd));
79998022
});
80008023
irUpdateEmptyHint();
80018024
}
@@ -8004,7 +8027,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
80048027
function addIrRow() {
80058028
var body = document.getElementById('irMappingBody');
80068029
if (!body) { return; }
8007-
var row = irMakeRow(null, IR_COMMANDS[0].cmd);
8030+
var row = irMakeRow(null, IR_COMMANDS[0].cmd, 0);
80088031
body.appendChild(row);
80098032
irUpdateEmptyHint();
80108033
var codeSpan = row.querySelector('.ir-code');
@@ -8049,10 +8072,11 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
80498072
var code = Number(codeStr);
80508073
if (seen[code]) { dup = true; }
80518074
seen[code] = true;
8052-
map.push({ code: code, cmd: Number(tr.querySelector('.ir-cmd').value) });
8075+
map.push({ code: code, cmd: Number(tr.querySelector('.ir-cmd').value), longCmd: Number(tr.querySelector('.ir-longcmd').value) });
80538076
});
80548077
if (dup) { toaster.warning(i18next.t('ir.duplicate')); }
8055-
await postSettings({ ir: { map: map } });
8078+
var longMs = Number(document.getElementById('irLongPressMs').value) || 600;
8079+
await postSettings({ ir: { map: map, longPressMs: longMs } });
80568080
}
80578081

80588082
var rfidScanCallback = null;

src/IrReceiver.cpp

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
// Debounce timestamp for non-volume actions
1919
std::atomic<uint32_t> IrReceiver_LastRcCmdTimestamp = 0u;
2020

21-
// Runtime mapping table (IR command -> CMD_* action). Seeded from the compiled-in RC_* defaults,
22-
// persisted in NVS (key "irMap") and editable via the web UI.
21+
// Runtime mapping table (IR command -> short/long CMD_* actions). Persisted in NVS (key "irMap2",
22+
// migrated from the legacy "irMap" layout) and editable via the web UI.
2323
static IrMapping sMap[IR_MAX_MAPPINGS];
2424
static uint8_t sMapCount = 0;
2525

@@ -28,9 +28,28 @@ static std::atomic<bool> sLearnMode = false;
2828
static uint32_t sLearnDeadline = 0;
2929
static constexpr uint32_t IR_LEARN_TIMEOUT = 30000; // ms; matches the web-UI learn modal timeout
3030

31-
static const char *irMapKey = "irMap";
31+
// Long-press handling. A held IR button keeps re-sending (repeat) frames; once they stop arriving
32+
// the button is considered released. Buttons that carry a long action fire the short action on
33+
// release and the long action once the hold passes the (configurable) threshold.
34+
static constexpr uint16_t IR_LONG_PRESS_DEFAULT = 600; // ms a button must be held for the long action
35+
static constexpr uint16_t IR_LONG_PRESS_MIN = 200;
36+
static constexpr uint16_t IR_LONG_PRESS_MAX = 5000;
37+
static constexpr uint32_t IR_HOLD_RELEASE_MS = 250; // no frame for this long -> button released
38+
static uint16_t sLongPressMs = IR_LONG_PRESS_DEFAULT;
39+
40+
// State of the button currently being held (only tracked for mappings that have a long action).
41+
static bool sHoldActive = false;
42+
static uint8_t sHoldIndex = 0; // index into sMap of the held mapping
43+
static uint32_t sHoldStartMs = 0; // when the genuine first frame arrived
44+
static uint32_t sHoldLastFrameMs = 0; // last time a frame for this button was seen
45+
static bool sHoldLongFired = false; // long action already triggered during this hold
46+
47+
static const char *irMapKey = "irMap2"; // current {code,cmd,longCmd} layout (4 bytes/entry)
48+
static const char *irLegacyMapKey = "irMap"; // pre-long-press {code,cmd} layout (3 bytes/entry), migration only
49+
static const char *irLongMsKey = "irLongMs";
3250

3351
static void loadMappings() {
52+
sHoldActive = false; // a reload (e.g. after a web save) invalidates any in-flight hold
3453
size_t blobLen = gPrefsSettings.getBytesLength(irMapKey);
3554
if ((blobLen >= sizeof(IrMapping)) && ((blobLen % sizeof(IrMapping)) == 0)) {
3655
uint8_t count = blobLen / sizeof(IrMapping);
@@ -39,13 +58,41 @@ static void loadMappings() {
3958
}
4059
gPrefsSettings.getBytes(irMapKey, sMap, (size_t) count * sizeof(IrMapping));
4160
sMapCount = count;
61+
} else if (size_t legacyLen = gPrefsSettings.getBytesLength(irLegacyMapKey); (legacyLen >= 3) && ((legacyLen % 3) == 0)) {
62+
// Migrate the legacy 3-byte {code,cmd} layout (pre long-press) so existing remotes survive
63+
// the firmware update. Each entry gains longCmd = 0 (no long action). The migrated table is
64+
// rewritten under the new key and the old blob is dropped so this runs exactly once.
65+
uint8_t count = legacyLen / 3;
66+
if (count > IR_MAX_MAPPINGS) {
67+
count = IR_MAX_MAPPINGS;
68+
}
69+
uint8_t legacy[IR_MAX_MAPPINGS * 3];
70+
gPrefsSettings.getBytes(irLegacyMapKey, legacy, (size_t) count * 3);
71+
for (uint8_t i = 0; i < count; i++) {
72+
sMap[i].code = (uint16_t) legacy[i * 3] | ((uint16_t) legacy[i * 3 + 1] << 8);
73+
sMap[i].cmd = legacy[i * 3 + 2];
74+
sMap[i].longCmd = 0;
75+
}
76+
sMapCount = count;
77+
gPrefsSettings.putBytes(irMapKey, sMap, (size_t) count * sizeof(IrMapping));
78+
gPrefsSettings.remove(irLegacyMapKey);
4279
} else {
4380
// No (valid) mapping stored yet -> start EMPTY: by default the remote does nothing at all.
4481
// The user assigns every button explicitly in the web UI. This deliberately drops the old
4582
// compiled-in RC_* layout so the upstream sample-remote codes can't accidentally match a
4683
// real remote and trigger unwanted actions (e.g. shutdown / Bluetooth toggle).
4784
sMapCount = 0;
4885
}
86+
87+
// Long-press threshold (configurable via the web UI), clamped to a sane range.
88+
uint32_t ms = gPrefsSettings.getUInt(irLongMsKey, IR_LONG_PRESS_DEFAULT);
89+
if (ms < IR_LONG_PRESS_MIN) {
90+
ms = IR_LONG_PRESS_MIN;
91+
}
92+
if (ms > IR_LONG_PRESS_MAX) {
93+
ms = IR_LONG_PRESS_MAX;
94+
}
95+
sLongPressMs = (uint16_t) ms;
4996
}
5097
#endif
5198

@@ -87,13 +134,31 @@ uint8_t IrReceiver_GetMappings(IrMapping *out, uint8_t maxCount) {
87134
#endif
88135
}
89136

137+
uint16_t IrReceiver_GetLongPressMs() {
138+
#ifdef IR_CONTROL_ENABLE
139+
return sLongPressMs;
140+
#else
141+
return 0;
142+
#endif
143+
}
144+
90145
void IrReceiver_Cyclic() {
91146
#ifdef IR_CONTROL_ENABLE
92147
// Auto-expire learn mode so a forgotten/aborted learn session can't suppress the remote forever.
93148
if (sLearnMode && ((int32_t) (millis() - sLearnDeadline) >= 0)) {
94149
sLearnMode = false;
95150
}
96151

152+
// IR has no "button released" event - a held button simply stops sending frames. Once the frames
153+
// for a long-press candidate stop arriving, treat it as released: fire the short action unless the
154+
// long action already fired while it was held.
155+
if (sHoldActive && ((millis() - sHoldLastFrameMs) > IR_HOLD_RELEASE_MS)) {
156+
if (!sHoldLongFired) {
157+
Cmd_Action(sMap[sHoldIndex].cmd);
158+
}
159+
sHoldActive = false;
160+
}
161+
97162
if (IrReceiver.decode()) {
98163
// Print a short summary of received data (serial-console learning still works as a fallback)
99164
IrReceiver.printIRResultShort(&Serial);
@@ -113,24 +178,63 @@ void IrReceiver_Cyclic() {
113178
return;
114179
}
115180

181+
// Look up the received command in the runtime mapping table.
182+
int16_t mi = -1;
183+
for (uint8_t i = 0; i < sMapCount; i++) {
184+
if (sMap[i].code == command) {
185+
mi = i;
186+
break;
187+
}
188+
}
189+
190+
// A button with a long action uses hold detection: nothing fires on the first frame, the long
191+
// action fires once the hold passes the threshold, the short action fires on release (above).
192+
if ((mi >= 0) && (sMap[mi].longCmd != 0)) {
193+
if (sHoldActive && (sMap[sHoldIndex].code == command)) {
194+
// Still holding the same button.
195+
sHoldLastFrameMs = millis();
196+
if (!sHoldLongFired && ((millis() - sHoldStartMs) >= sLongPressMs)) {
197+
Cmd_Action(sMap[mi].longCmd);
198+
sHoldLongFired = true;
199+
}
200+
} else {
201+
// A new button started: finalize the previous candidate's short action first.
202+
if (sHoldActive && !sHoldLongFired) {
203+
Cmd_Action(sMap[sHoldIndex].cmd);
204+
}
205+
sHoldActive = true;
206+
sHoldIndex = (uint8_t) mi;
207+
sHoldStartMs = millis();
208+
sHoldLastFrameMs = millis();
209+
sHoldLongFired = false;
210+
}
211+
return;
212+
}
213+
214+
// Plain button (no long action) or unknown code: a different mapped button means any pending
215+
// long-press candidate was released, so flush its short action now.
216+
if (sHoldActive && (mi >= 0)) {
217+
if (!sHoldLongFired) {
218+
Cmd_Action(sMap[sHoldIndex].cmd);
219+
}
220+
sHoldActive = false;
221+
}
222+
116223
bool rcActionOk = false;
117224
if (millis() - IrReceiver_LastRcCmdTimestamp >= IR_DEBOUNCE) {
118225
rcActionOk = true; // not used for volume up/down
119226
IrReceiver_LastRcCmdTimestamp = millis();
120227
}
121228

122-
// Look up the received command in the runtime mapping table and dispatch the mapped action.
123-
for (uint8_t i = 0; i < sMapCount; i++) {
124-
if (sMap[i].code == command) {
125-
const uint8_t cmd = sMap[i].cmd;
126-
// Volume up/down fire on every frame (incl. repeats) so holding the button ramps.
127-
if ((cmd == CMD_VOLUMEUP) || (cmd == CMD_VOLUMEDOWN)) {
128-
Cmd_Action(cmd);
129-
} else if (rcActionOk) {
130-
Cmd_Action(cmd);
131-
}
132-
return;
229+
if (mi >= 0) {
230+
const uint8_t cmd = sMap[mi].cmd;
231+
// Volume up/down fire on every frame (incl. repeats) so holding the button ramps.
232+
if ((cmd == CMD_VOLUMEUP) || (cmd == CMD_VOLUMEDOWN)) {
233+
Cmd_Action(cmd);
234+
} else if (rcActionOk) {
235+
Cmd_Action(cmd);
133236
}
237+
return;
134238
}
135239
if (rcActionOk) {
136240
Log_Println("RC: unknown", LOGLEVEL_NOTICE);

src/IrReceiver.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
// Maximum number of IR-code -> command mappings kept in RAM / NVS.
66
#define IR_MAX_MAPPINGS 32
77

8-
// One learned remote button: the raw IR command and the CMD_* action it triggers (see values.h).
8+
// One learned remote button: the raw IR command and the CMD_* actions it triggers (see values.h).
99
struct __attribute__((packed)) IrMapping {
1010
uint16_t code; // decodedIRData.command of the remote button
11-
uint8_t cmd; // CMD_* action to run when this code is received
11+
uint8_t cmd; // CMD_* action on a short press (tap)
12+
uint8_t longCmd; // CMD_* action on a long press (hold); 0 (CMD_NOTHING) = no long-press action
1213
};
1314

1415
void IrReceiver_Init();
@@ -18,3 +19,4 @@ void IrReceiver_Cyclic();
1819
void IrReceiver_ReloadMappings(); // re-read the mapping table from NVS (after a save via the web UI)
1920
void IrReceiver_SetLearnMode(bool enable); // arm/disarm learn mode (auto-expires after a timeout)
2021
uint8_t IrReceiver_GetMappings(IrMapping *out, uint8_t maxCount); // copy current table out; returns count
22+
uint16_t IrReceiver_GetLongPressMs(); // configured long-press threshold in milliseconds

0 commit comments

Comments
 (0)