AwesomeStudioPedal
A programmable, multi-profile foot controller for DAWs, score readers, and studio automation
Loading...
Searching...
No Matches
config_loader.cpp
Go to the documentation of this file.
1#include "config_loader.h"
2#include "button_constants.h"
3#include "config.h"
4#include "delayed_action.h"
5#include "file_system.h"
6#include "i_logger.h"
7#include "key_lookup.h"
8#include "non_send_action.h"
9#include "send_action.h"
10#include "serial_action.h"
11#include <ArduinoJson.h>
12#include <cstring>
13#include <memory>
14
15#ifndef HOST_TEST_BUILD
16#include <Arduino.h>
17#else
18#include "../test/fakes/arduino_shim.h"
19#endif
20
21using namespace ArduinoJson;
22
23// Forward declarations for platform-specific factories
25ILogger* createLogger(); // NOLINT(readability-redundant-declaration)
26
33const char* ConfigLoader::DEFAULT_CONFIG =
34 "{\n"
35 " \"profiles\": [\n"
36 " {\n"
37 " \"name\": \"Navigation\",\n"
38 " \"description\": \"Profile for controlling media playback\",\n"
39 " \"buttons\": {\n"
40 " \"A\": {\"type\": \"SendStringAction\", \"name\": \"Space\", \"value\": \" \"},\n"
41 " \"B\": {\"type\": \"SendMediaKeyAction\", \"name\": \"Stop\", \"value\": "
42 "\"MEDIA_STOP\"},\n"
43 " \"C\": {\"type\": \"SendCharAction\", \"name\": \"Rewind\", \"value\": "
44 "\"LEFT_ARROW\"},\n"
45 " \"D\": {\"type\": \"SendCharAction\", \"name\": \"Forward\", \"value\": "
46 "\"RIGHT_ARROW\"}\n"
47 " }\n"
48 " }\n"
49 " ]\n"
50 "}\n";
51
52// ---- Constructors ----
53
61
70ConfigLoader::ConfigLoader(IFileSystem* fs, ILogger* logger) : fileSystem_(fs), logger_(logger) {}
71
72// ---- Public API ----
73
85 IBleKeyboard* keyboard,
86 const std::string& configPath)
87{
88 std::string content;
89 if (! fileSystem_->readFile(configPath.c_str(), content))
90 {
91 logger_->log("Failed to read config file");
92 return false;
93 }
94 return loadFromString(profileManager, keyboard, content);
95}
96
109 IBleKeyboard* keyboard,
110 const std::string& jsonConfig)
111{
112 DynamicJsonDocument doc(8192);
113 DeserializationError error = deserializeJson(doc, jsonConfig);
114
115 if (error)
116 {
117 logger_->log("JSON parsing failed:", error.c_str());
118 return false;
119 }
120
121 for (uint8_t i = 0; i < hardwareConfig.numProfiles; i++)
122 {
123 profileManager.addProfile(i, nullptr);
124 }
125
126 JsonArray profiles = doc["profiles"];
127 for (uint8_t i = 0; i < profiles.size() && i < hardwareConfig.numProfiles; i++)
128 {
129 JsonObject profileJson = profiles[i];
130 const char* profileName = profileJson["name"] | "";
131 const char* profileDescription = profileJson["description"] | "";
132
133 auto newProfile = std::unique_ptr<Profile>(new Profile(profileName));
134 newProfile->setDescription(profileDescription);
135 populateProfileFromJson(*newProfile, profileJson["buttons"], keyboard);
136 profileManager.addProfile(i, std::move(newProfile));
137 }
138
140 logLoadedConfig(profileManager);
141 return true;
142}
143
144// ---- Helper methods ----
145
152uint8_t ConfigLoader::getButtonIndex(const char* buttonName)
153{
154 if (buttonName[0] >= 'A' && buttonName[0] <= ('A' + Btn::MAX - 1) && buttonName[1] == '\0')
155 {
156 return static_cast<uint8_t>(buttonName[0] - 'A');
157 }
158 return 255;
159}
160
171void ConfigLoader::populateProfileFromJson(Profile& profile,
172 JsonObject buttons,
173 IBleKeyboard* keyboard)
174{
175 char buttonName[2];
176 for (uint8_t b = 0; b < hardwareConfig.numButtons; b++)
177 {
178 Btn::name(b, buttonName);
179 if (! buttons.containsKey(buttonName))
180 {
181 continue;
182 }
183
184 JsonObject actionJson = buttons[buttonName];
185
186 std::unique_ptr<Action> action = createActionFromJson(actionJson, keyboard);
187 if (action)
188 {
189 const char* actionName = actionJson["name"] | "";
190 if (actionName[0] != '\0')
191 {
192 action->setName(actionName);
193 }
194 profile.addAction(b, std::move(action));
195 }
196 }
197}
198
202std::unique_ptr<Action> ConfigLoader::createSendCharActionFromJson(const JsonObject& actionJson,
203 IBleKeyboard* keyboard)
204{
205 const char* value = actionJson["value"] | "";
206 uint8_t code = lookupKey(value);
207 if (code != 0)
208 {
209 return std::unique_ptr<Action>(new SendCharAction(keyboard, static_cast<char>(code)));
210 }
211 // Single printable ASCII character (e.g. "[", "]", "c", " ")
212 if (value[0] != '\0' && value[1] == '\0')
213 {
214 return std::unique_ptr<Action>(new SendCharAction(keyboard, value[0]));
215 }
216 logger_->log("SendChar: unknown key value: ", value);
217 return nullptr;
218}
219
231std::unique_ptr<Action> ConfigLoader::createActionFromJson(const JsonObject& actionJson,
232 IBleKeyboard* keyboard)
233{
234 const char* typeName = actionJson["type"] | "";
235 Action::Type type = lookupActionType(typeName);
236
237 switch (type)
238 {
240 {
241 const char* value = actionJson["value"] | "";
242 return std::unique_ptr<Action>(new SendStringAction(keyboard, value));
243 }
245 return createSendCharActionFromJson(actionJson, keyboard);
247 {
248 uint8_t code = lookupKey(actionJson["value"] | "");
249 if (code != 0)
250 {
251 return std::unique_ptr<Action>(new SendKeyAction(keyboard, code));
252 }
253 break;
254 }
256 {
257 const uint8_t* report = lookupMediaKey(actionJson["value"] | "");
258 if (report)
259 {
260 return std::unique_ptr<Action>(new SendMediaKeyAction(keyboard, report));
261 }
262 break;
263 }
265 {
266 const char* value = actionJson["value"] | "";
267 return std::unique_ptr<Action>(new SerialOutputAction(value));
268 }
270 {
271 uint32_t delayMs = actionJson["delayMs"] | 0U;
272 JsonObject nestedJson = actionJson["action"];
273 std::unique_ptr<Action> inner = createActionFromJson(nestedJson, keyboard);
274 if (inner)
275 {
276 return std::unique_ptr<Action>(new DelayedAction(std::move(inner), delayMs));
277 }
278 break;
279 }
280 default:
281 break;
282 }
283
284 return nullptr;
285}
static uint8_t getButtonIndex(const char *buttonName)
Converts button name to button index.
ConfigLoader()
Default constructor: uses production singletons (firmware path)
bool loadFromFile(ProfileManager &profileManager, IBleKeyboard *keyboard, const std::string &configPath)
Loads configuration from a file.
bool loadFromString(ProfileManager &profileManager, IBleKeyboard *keyboard, const std::string &jsonConfig)
Loads configuration from a JSON string.
Wraps an action and defers its execution by a configurable delay.
Interface for Bluetooth LE keyboard functionality.
Abstract interface for file system operations.
Definition file_system.h:12
virtual bool readFile(const char *path, std::string &content)=0
Read entire file content.
Interface for logging functionality.
Definition i_logger.h:11
virtual void log(const char *message)=0
Logs a single message.
Manages up to MAX_PROFILES profiles with LED feedback.
void resetToFirstProfile()
Reset to the first populated slot (or slot 0 if all empty)
void addProfile(uint8_t profileIndex, std::unique_ptr< Profile > profile)
Represents a single button configuration profile.
Definition profile.h:15
void addAction(uint8_t button, std::unique_ptr< Action > action)
Adds an action to a specific button in this profile.
Definition profile.cpp:15
Sends a single character via BLE keyboard.
Definition send_action.h:60
Sends a USB HID key code via BLE keyboard.
Sends a media key report via BLE keyboard.
Sends a text string via BLE keyboard.
Action that outputs text to serial console.
const HardwareConfig hardwareConfig
Global hardware configuration instance.
IFileSystem * createFileSystem()
ILogger * createLogger()
Creates and returns a SerialLogger instance.
const uint8_t * lookupMediaKey(const char *name)
Looks up a media key report by name.
uint8_t lookupKey(const char *name)
Looks up a key code by name.
Action::Type lookupActionType(const char *name)
Looks up an action type by name.
ProfileManager * profileManager
Definition main.cpp:72
void name(uint8_t index, char *buf)
Write the letter name for a button index into buf.
uint8_t numButtons
Action buttons wired (1..26, A–Z)
Definition config.h:21
uint8_t numProfiles
Number of active profiles (1..MAX_PROFILES)
Definition config.h:19