Skip to content

Commit 69146e4

Browse files
committed
Refactor firmware
1 parent 9c10abf commit 69146e4

7 files changed

Lines changed: 344 additions & 333 deletions

File tree

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#include "irhandler.h"
2+
#include <IRremote.hpp>
3+
#include "webserial.h"
4+
5+
uint8_t RECV_PIN = IR_RECEIVE_PIN;
6+
uint32_t mode_change = MODE_CHANGE_CODE;
7+
uint8_t currentMode = 0;
8+
uint8_t numModes = 2;
9+
char modeNames[MODE_COUNT][20] = {
10+
"Layer 1", "Layer 2", "Layer 3", "Layer 4", "Layer 5"
11+
};
12+
bool HANDLE_REPEAT_CONFIG = HANDLE_REPEAT;
13+
uint8_t REPEAT_DELAY_REPORTS = REPEAT_INITIAL_DELAY_REPORTS;
14+
15+
static uint32_t lastCode = 0;
16+
static uint8_t repeatCount = 0;
17+
18+
IRSlot modeSlots[MODE_COUNT][MAX_MAPPINGS];
19+
ComboStep comboData[MODE_COUNT][MAX_MAPPINGS][MAX_COMBO_STEPS];
20+
char slotTextData[MODE_COUNT][MAX_MAPPINGS][MAX_TEXT_STEP_LEN];
21+
char comboTextData[MODE_COUNT][MAX_MAPPINGS][MAX_COMBO_STEPS][MAX_TEXT_STEP_LEN];
22+
23+
void clearModeSlots(uint8_t modeIndex) {
24+
if (modeIndex >= MODE_COUNT) return;
25+
for (uint8_t i = 0; i < MAX_MAPPINGS; i++) {
26+
modeSlots[modeIndex][i] = {0, 0, SLOT_NONE, 0};
27+
slotTextData[modeIndex][i][0] = '\0';
28+
for (uint8_t s = 0; s < MAX_COMBO_STEPS; s++) {
29+
comboData[modeIndex][i][s] = {0, 0, 0};
30+
comboTextData[modeIndex][i][s][0] = '\0';
31+
}
32+
}
33+
}
34+
35+
int findSlotIndex(uint8_t modeIndex, uint32_t code) {
36+
if (modeIndex >= MODE_COUNT) return -1;
37+
for (uint8_t i = 0; i < MAX_MAPPINGS; i++) {
38+
if (modeSlots[modeIndex][i].type != SLOT_NONE && modeSlots[modeIndex][i].irCode == code) {
39+
return i;
40+
}
41+
}
42+
return -1;
43+
}
44+
45+
bool parseKeyValue(JsonVariant keyVar, uint16_t* outKey) {
46+
if (keyVar.is<const char*>()) {
47+
const char* keyStr = keyVar.as<const char*>();
48+
size_t len = strlen(keyStr);
49+
if (len == 1) {
50+
*outKey = (uint8_t)keyStr[0];
51+
return true;
52+
}
53+
if (len > 2 && keyStr[0] == '0' && (keyStr[1] == 'x' || keyStr[1] == 'X')) {
54+
*outKey = (uint16_t)strtoul(keyStr, NULL, 16);
55+
return true;
56+
}
57+
*outKey = (uint16_t)atoi(keyStr);
58+
return true;
59+
}
60+
if (keyVar.is<int>()) {
61+
*outKey = (uint16_t)keyVar.as<int>();
62+
return true;
63+
}
64+
return false;
65+
}
66+
67+
uint8_t parseSlotType(JsonVariant typeVar) {
68+
if (typeVar.is<const char*>()) {
69+
const char* typeStr = typeVar.as<const char*>();
70+
if (strcmp(typeStr, "keyboard") == 0) return SLOT_KEYBOARD;
71+
if (strcmp(typeStr, "consumer") == 0) return SLOT_CONSUMER;
72+
if (strcmp(typeStr, "mode_switch") == 0) return SLOT_MODE_SWITCH;
73+
if (strcmp(typeStr, "combo") == 0) return SLOT_COMBO;
74+
if (strcmp(typeStr, "text") == 0) return SLOT_TEXT;
75+
}
76+
if (typeVar.is<uint8_t>()) {
77+
uint8_t typeVal = typeVar.as<uint8_t>();
78+
if (typeVal <= SLOT_MODE_SWITCH) return typeVal;
79+
}
80+
return SLOT_NONE;
81+
}
82+
83+
void applyIrSettings(JsonObject ir) {
84+
if (ir["modeChangeCode"].is<const char*>())
85+
mode_change = strtoul((const char*)ir["modeChangeCode"], NULL, 16);
86+
if (ir["receivePin"].is<uint8_t>())
87+
RECV_PIN = ir["receivePin"];
88+
if (ir["handleRepeat"].is<bool>())
89+
HANDLE_REPEAT_CONFIG = ir["handleRepeat"];
90+
if (ir["repeatInitialDelayReports"].is<uint8_t>())
91+
REPEAT_DELAY_REPORTS = ir["repeatInitialDelayReports"];
92+
if (ir["modeCount"].is<uint8_t>()) {
93+
uint8_t mc = ir["modeCount"].as<uint8_t>();
94+
if (mc >= 1 && mc <= MODE_COUNT) numModes = mc;
95+
}
96+
}
97+
98+
void applySlotsFromArray(uint8_t modeIndex, JsonArray slots) {
99+
if (modeIndex >= MODE_COUNT) return;
100+
clearModeSlots(modeIndex);
101+
102+
uint8_t slotIndex = 0;
103+
for (JsonObject slot : slots) {
104+
if (slotIndex >= MAX_MAPPINGS) break;
105+
if (!slot["irCode"].is<const char*>()) { slotIndex++; continue; }
106+
107+
const char* irCodeStr = slot["irCode"];
108+
uint32_t irCode = strtoul(irCodeStr, NULL, 16);
109+
uint8_t typeVal = parseSlotType(slot["type"]);
110+
if (typeVal == SLOT_NONE) { slotIndex++; continue; }
111+
112+
if (typeVal == SLOT_TEXT) {
113+
if (!slot["value"].is<const char*>()) { slotIndex++; continue; }
114+
strlcpy(slotTextData[modeIndex][slotIndex], slot["value"].as<const char*>(), MAX_TEXT_STEP_LEN);
115+
modeSlots[modeIndex][slotIndex] = {irCode, 0, SLOT_TEXT, 0};
116+
slotIndex++;
117+
continue;
118+
}
119+
120+
if (typeVal == SLOT_COMBO) {
121+
if (!slot["steps"].is<JsonArray>()) { slotIndex++; continue; }
122+
uint8_t stepCount = 0;
123+
for (JsonObject step : slot["steps"].as<JsonArray>()) {
124+
if (stepCount >= MAX_COMBO_STEPS) break;
125+
uint8_t stepType = parseSlotType(step["type"]);
126+
if (stepType == SLOT_TEXT) {
127+
if (!step["value"].is<const char*>()) continue;
128+
strlcpy(comboTextData[modeIndex][slotIndex][stepCount], step["value"].as<const char*>(), MAX_TEXT_STEP_LEN);
129+
comboData[modeIndex][slotIndex][stepCount] = {0, SLOT_TEXT, 0};
130+
stepCount++;
131+
continue;
132+
}
133+
if (stepType != SLOT_KEYBOARD && stepType != SLOT_CONSUMER) continue;
134+
uint16_t stepKey = 0;
135+
if (!parseKeyValue(step["key"], &stepKey)) continue;
136+
uint8_t stepMods = 0;
137+
if (step["mods"].is<const char*>())
138+
stepMods = (uint8_t)strtoul(step["mods"].as<const char*>(), NULL, 16);
139+
comboData[modeIndex][slotIndex][stepCount] = {stepKey, stepType, stepMods};
140+
stepCount++;
141+
}
142+
modeSlots[modeIndex][slotIndex] = {irCode, stepCount, SLOT_COMBO, 0};
143+
slotIndex++;
144+
continue;
145+
}
146+
147+
uint16_t keyVal = 0;
148+
if (typeVal != SLOT_MODE_SWITCH && !parseKeyValue(slot["key"], &keyVal)) {
149+
slotIndex++; continue;
150+
}
151+
uint8_t modsVal = 0;
152+
if (slot["mods"].is<const char*>())
153+
modsVal = (uint8_t)strtoul(slot["mods"].as<const char*>(), NULL, 16);
154+
155+
modeSlots[modeIndex][slotIndex] = {irCode, keyVal, typeVal, modsVal};
156+
slotIndex++;
157+
}
158+
}
159+
160+
void applyModeNamesFromArray(JsonArray modes) {
161+
uint8_t idx = 0;
162+
for (JsonObject mode : modes) {
163+
if (idx >= MODE_COUNT) break;
164+
if (mode["name"].is<const char*>()) {
165+
strlcpy(modeNames[idx], mode["name"].as<const char*>(), sizeof(modeNames[idx]));
166+
}
167+
idx++;
168+
}
169+
}
170+
171+
void irremoteSetup() {
172+
IrReceiver.begin(RECV_PIN, ENABLE_LED_FEEDBACK);
173+
}
174+
175+
void irremoteTick() {
176+
if (!IrReceiver.decode()) return;
177+
178+
uint32_t code = IrReceiver.decodedIRData.decodedRawData;
179+
Serial.print("IR code: 0x");
180+
Serial.println(code, HEX);
181+
182+
if (webSerialConsumeIrCode(code)) {
183+
IrReceiver.resume();
184+
return;
185+
}
186+
187+
if (code == 0x00 && HANDLE_REPEAT_CONFIG) {
188+
if (repeatCount < REPEAT_DELAY_REPORTS) {
189+
repeatCount++;
190+
IrReceiver.resume();
191+
return;
192+
}
193+
code = lastCode;
194+
} else {
195+
lastCode = code;
196+
repeatCount = 0;
197+
}
198+
199+
if (code == mode_change) {
200+
currentMode = (currentMode + 1) % numModes;
201+
updateLED();
202+
Serial.print("Mode switched: ");
203+
Serial.println(currentMode == 0 ? "Mode 1" : "Mode 2");
204+
} else {
205+
int slotIndex = findSlotIndex(currentMode, code);
206+
if (slotIndex >= 0) {
207+
IRSlot slot = modeSlots[currentMode][slotIndex];
208+
if (slot.type == SLOT_KEYBOARD) {
209+
sendKeyboardReport((uint8_t)slot.key, slot.mods);
210+
Serial.print("Sent keyboard key: 0x");
211+
Serial.print(slot.key, HEX);
212+
if (slot.mods) { Serial.print(" mods: 0x"); Serial.print(slot.mods, HEX); }
213+
Serial.println();
214+
} else if (slot.type == SLOT_CONSUMER) {
215+
sendConsumerKey(slot.key);
216+
Serial.print("Sent consumer key: 0x");
217+
Serial.println(slot.key, HEX);
218+
} else if (slot.type == SLOT_MODE_SWITCH) {
219+
currentMode = (currentMode + 1) % numModes;
220+
updateLED();
221+
Serial.print("Slot mode switch: ");
222+
Serial.println(currentMode);
223+
} else if (slot.type == SLOT_TEXT) {
224+
const char* text = slotTextData[currentMode][slotIndex];
225+
for (uint8_t c = 0; text[c] != '\0'; c++) {
226+
sendKeyboardKey((uint8_t)text[c]);
227+
delay(10);
228+
}
229+
Serial.print("Sent text: ");
230+
Serial.println(text);
231+
} else if (slot.type == SLOT_COMBO) {
232+
uint8_t stepCount = (uint8_t)slot.key;
233+
for (uint8_t s = 0; s < stepCount && s < MAX_COMBO_STEPS; s++) {
234+
ComboStep cs = comboData[currentMode][slotIndex][s];
235+
if (cs.type == SLOT_TEXT) {
236+
const char* text = comboTextData[currentMode][slotIndex][s];
237+
for (uint8_t c = 0; text[c] != '\0'; c++) {
238+
sendKeyboardKey((uint8_t)text[c]);
239+
delay(10);
240+
}
241+
} else if (cs.type == SLOT_KEYBOARD) {
242+
sendKeyboardReport((uint8_t)cs.key, cs.mods);
243+
} else if (cs.type == SLOT_CONSUMER) {
244+
sendConsumerKey(cs.key);
245+
}
246+
delay(20);
247+
}
248+
Serial.print("Sent combo (");
249+
Serial.print(stepCount);
250+
Serial.println(" steps)");
251+
}
252+
}
253+
}
254+
255+
IrReceiver.resume();
256+
}

firmware/lib/irremote/irhandler.h

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#pragma once
2+
#include <stdint.h>
3+
#include <ArduinoJson.h>
4+
5+
// IR Settings (defaults — overridden by JSON if present)
6+
#define MODE_CHANGE_CODE 0xC40387EE
7+
#define IR_RECEIVE_PIN 28
8+
#define HANDLE_REPEAT true
9+
#define REPEAT_INITIAL_DELAY_REPORTS 5
10+
11+
// Slot / mode sizing
12+
#define MAX_MAPPINGS 20
13+
#define MODE_COUNT 5
14+
#define MAX_COMBO_STEPS 8
15+
#define MAX_TEXT_STEP_LEN 48
16+
17+
enum IRSlotType : uint8_t {
18+
SLOT_NONE = 0,
19+
SLOT_KEYBOARD = 1,
20+
SLOT_CONSUMER = 2,
21+
SLOT_MODE_SWITCH = 3,
22+
SLOT_COMBO = 4,
23+
SLOT_TEXT = 5
24+
};
25+
26+
struct ComboStep {
27+
uint16_t key;
28+
uint8_t type;
29+
uint8_t mods;
30+
};
31+
32+
struct IRSlot {
33+
uint32_t irCode;
34+
uint16_t key; // For SLOT_COMBO: step count. Otherwise: HID usage ID.
35+
uint8_t type;
36+
uint8_t mods;
37+
};
38+
39+
// IR state
40+
extern uint8_t RECV_PIN;
41+
extern uint32_t mode_change;
42+
extern uint8_t currentMode;
43+
extern uint8_t numModes;
44+
extern char modeNames[MODE_COUNT][20];
45+
extern bool HANDLE_REPEAT_CONFIG;
46+
extern uint8_t REPEAT_DELAY_REPORTS;
47+
48+
// Slot data arrays
49+
extern IRSlot modeSlots[MODE_COUNT][MAX_MAPPINGS];
50+
extern ComboStep comboData[MODE_COUNT][MAX_MAPPINGS][MAX_COMBO_STEPS];
51+
extern char slotTextData[MODE_COUNT][MAX_MAPPINGS][MAX_TEXT_STEP_LEN];
52+
extern char comboTextData[MODE_COUNT][MAX_MAPPINGS][MAX_COMBO_STEPS][MAX_TEXT_STEP_LEN];
53+
54+
// Callbacks into main.cpp — defined there, called by irremoteTick
55+
void updateLED();
56+
void sendKeyboardReport(uint8_t keycode, uint8_t modifier = 0);
57+
void sendKeyboardKey(uint8_t ascii);
58+
void sendConsumerKey(uint16_t key);
59+
60+
// IR module API
61+
void clearModeSlots(uint8_t modeIndex);
62+
int findSlotIndex(uint8_t modeIndex, uint32_t code);
63+
bool parseKeyValue(JsonVariant keyVar, uint16_t* outKey);
64+
uint8_t parseSlotType(JsonVariant typeVar);
65+
void applyIrSettings(JsonObject ir);
66+
void applySlotsFromArray(uint8_t modeIndex, JsonArray slots);
67+
void applyModeNamesFromArray(JsonArray modes);
68+
void irremoteSetup();
69+
void irremoteTick();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#include <Arduino.h>
2+
#include <ArduinoJson.h>
13
#include "settings.h"
24
#include "webserial.h"
35

firmware/platformio.ini

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ upload_protocol = picotool
1919
; LittleFS filesystem configuration
2020
board_build.filesystem_size = 1m
2121

22+
lib_ldf_mode = deep+
23+
2224
lib_deps=
2325
arduino-irremote/IRremote
2426
bblanchon/ArduinoJson
@@ -27,4 +29,5 @@ lib_deps=
2729
build_flags =
2830
-D USE_TINYUSB
2931
-D CFG_TUD_HID=1
30-
-D CFG_TUD_CDC=1
32+
-D CFG_TUD_CDC=1
33+
-I ${PROJECT_SRC_DIR}

0 commit comments

Comments
 (0)