init
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
|
||||||
|
src/secrets.h
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Fingerbot Node
|
||||||
|
|
||||||
|
A small PlatformIO/ESP32 firmware project for a BLE-enabled Fingerbot node.
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
The node accepts plain-text commands on the configured BLE characteristic.
|
||||||
|
|
||||||
|
- `identify`
|
||||||
|
- Blinks the LED for ~5 seconds to help identify the device.
|
||||||
|
- `push <durations...>`
|
||||||
|
- Executes a press/release pattern using space-separated durations in milliseconds.
|
||||||
|
- Durations must be between `10` and `18000` ms.
|
||||||
|
- Examples:
|
||||||
|
- Hold for 700ms: `push 700`
|
||||||
|
- Hold, release, hold: `push 700 200 700`
|
||||||
|
|
||||||
|
## Secrets setup
|
||||||
|
|
||||||
|
Sensitive values are stored in `src/secrets.h`. Use `src/secrets.example.h` as a template and enter your values.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[env:fb-node]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-c3-devkitm-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
build_flags =
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
lib_deps =
|
||||||
|
h2zero/NimBLE-Arduino @ ^1.4.1
|
||||||
|
bblanchon/ArduinoJson @ ^7.0.0
|
||||||
|
madhephaestus/ESP32Servo @ ^1.1.0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "utils/ble.h"
|
||||||
|
#include "secrets.h"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const NodeConfig kNodeConfig = {
|
||||||
|
.pinLed = 8,
|
||||||
|
.pinServo = 4,
|
||||||
|
.minPushDurationMs = 10,
|
||||||
|
.maxPushDurationMs = 18000,
|
||||||
|
.servoRestPos = 150,
|
||||||
|
.servoPressPos = 180,
|
||||||
|
.servoClickMs = 700,
|
||||||
|
.servoLongClickMs = 5000,
|
||||||
|
.servoDoubleGapMs = 200,
|
||||||
|
.deviceName = Secrets::kBleDeviceName,
|
||||||
|
.serviceUuid = Secrets::kBleServiceUuid,
|
||||||
|
.characteristicUuid = Secrets::kBleCharacteristicUuid};
|
||||||
|
|
||||||
|
const uint32_t kBaudRate = 115200;
|
||||||
|
|
||||||
|
TargetNode gNode(kNodeConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(kBaudRate);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
gNode.setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
gNode.loop(millis());
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#ifndef SECRETS_H
|
||||||
|
#define SECRETS_H
|
||||||
|
|
||||||
|
namespace Secrets
|
||||||
|
{
|
||||||
|
inline constexpr const char *kBleDeviceName = "YOUR_DEVICE_NAME";
|
||||||
|
inline constexpr const char *kBleServiceUuid = "YOUR_SERVICE_UUID";
|
||||||
|
inline constexpr const char *kBleCharacteristicUuid = "YOUR_CHARACTERISTIC_UUID";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
#ifndef FB_NODE_BLE_H
|
||||||
|
#define FB_NODE_BLE_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
#include <ESP32Servo.h>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct NodeConfig
|
||||||
|
{
|
||||||
|
uint8_t pinLed;
|
||||||
|
uint8_t pinServo;
|
||||||
|
uint32_t minPushDurationMs;
|
||||||
|
uint32_t maxPushDurationMs;
|
||||||
|
uint16_t servoRestPos;
|
||||||
|
uint16_t servoPressPos;
|
||||||
|
uint32_t servoClickMs;
|
||||||
|
uint32_t servoLongClickMs;
|
||||||
|
uint32_t servoDoubleGapMs;
|
||||||
|
const char *deviceName;
|
||||||
|
const char *serviceUuid;
|
||||||
|
const char *characteristicUuid;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TargetNode
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit TargetNode(const NodeConfig &config)
|
||||||
|
: config_(config), deviceConnected_(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
pinMode(config_.pinLed, OUTPUT);
|
||||||
|
digitalWrite(config_.pinLed, HIGH);
|
||||||
|
|
||||||
|
servo_.setPeriodHertz(50);
|
||||||
|
servo_.attach(config_.pinServo);
|
||||||
|
servo_.write(config_.servoRestPos);
|
||||||
|
|
||||||
|
NimBLEDevice::init(config_.deviceName);
|
||||||
|
Serial.printf("MAC_ADDRESS=%s\n", NimBLEDevice::getAddress().toString().c_str());
|
||||||
|
|
||||||
|
NimBLEServer *server = NimBLEDevice::createServer();
|
||||||
|
server->setCallbacks(new ServerCallbacks(*this));
|
||||||
|
|
||||||
|
NimBLEService *service = server->createService(config_.serviceUuid);
|
||||||
|
|
||||||
|
NimBLECharacteristic *characteristic = service->createCharacteristic(
|
||||||
|
config_.characteristicUuid,
|
||||||
|
NIMBLE_PROPERTY::WRITE |
|
||||||
|
NIMBLE_PROPERTY::WRITE_NR);
|
||||||
|
|
||||||
|
characteristic->setCallbacks(new TargetCallbacks(*this));
|
||||||
|
service->start();
|
||||||
|
|
||||||
|
NimBLEAdvertising *advertising = NimBLEDevice::getAdvertising();
|
||||||
|
advertising->addServiceUUID(config_.serviceUuid);
|
||||||
|
advertising->start();
|
||||||
|
|
||||||
|
Serial.println("[INF] Advertising started. Waiting for bridge connections...");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop(uint32_t now)
|
||||||
|
{
|
||||||
|
(void)now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string trim(const std::string &value)
|
||||||
|
{
|
||||||
|
const size_t first = value.find_first_not_of(" \t\r\n");
|
||||||
|
if (first == std::string::npos)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t last = value.find_last_not_of(" \t\r\n");
|
||||||
|
return value.substr(first, (last - first) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isNumberToken(const std::string &token)
|
||||||
|
{
|
||||||
|
if (token.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (char ch : token)
|
||||||
|
{
|
||||||
|
if (!isdigit(static_cast<unsigned char>(ch)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPressedState(bool pressed)
|
||||||
|
{
|
||||||
|
digitalWrite(config_.pinLed, pressed ? LOW : HIGH);
|
||||||
|
servo_.write(pressed ? config_.servoPressPos : config_.servoRestPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runPattern(const std::vector<uint32_t> &durations)
|
||||||
|
{
|
||||||
|
if (durations.empty())
|
||||||
|
{
|
||||||
|
Serial.println("[BLE] ERROR: Pattern contains no durations");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pressed = true;
|
||||||
|
for (uint32_t durationMs : durations)
|
||||||
|
{
|
||||||
|
if (durationMs < config_.minPushDurationMs || durationMs > config_.maxPushDurationMs)
|
||||||
|
{
|
||||||
|
Serial.printf("[BLE] ERROR: Pattern duration %lu ms out of range [%lu-%lu]\n", durationMs, config_.minPushDurationMs, config_.maxPushDurationMs);
|
||||||
|
setPressedState(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPressedState(pressed);
|
||||||
|
delay(durationMs);
|
||||||
|
pressed = !pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPressedState(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runIdentifyCommand()
|
||||||
|
{
|
||||||
|
constexpr uint32_t kIdentifyTotalMs = 5000;
|
||||||
|
constexpr uint32_t kBlinkStepMs = 100;
|
||||||
|
|
||||||
|
Serial.println("[BLE] Identify started");
|
||||||
|
servo_.write(config_.servoRestPos);
|
||||||
|
|
||||||
|
const uint32_t toggleCount = kIdentifyTotalMs / kBlinkStepMs;
|
||||||
|
bool ledOn = true;
|
||||||
|
for (uint32_t i = 0; i < toggleCount; i++)
|
||||||
|
{
|
||||||
|
digitalWrite(config_.pinLed, ledOn ? LOW : HIGH);
|
||||||
|
delay(kBlinkStepMs);
|
||||||
|
ledOn = !ledOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
digitalWrite(config_.pinLed, HIGH);
|
||||||
|
Serial.println("[BLE] Identify complete");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runTextCommand(const std::string &rawCommand)
|
||||||
|
{
|
||||||
|
std::string command = trim(rawCommand);
|
||||||
|
if (command.empty())
|
||||||
|
{
|
||||||
|
Serial.println("[BLE] ERROR: Empty command");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract command name (first token) and payload (rest)
|
||||||
|
size_t firstSpace = command.find(' ');
|
||||||
|
std::string commandName = firstSpace == std::string::npos ? command : command.substr(0, firstSpace);
|
||||||
|
std::string payload = firstSpace == std::string::npos ? "" : trim(command.substr(firstSpace + 1));
|
||||||
|
|
||||||
|
if (strcasecmp(commandName.c_str(), "identify") == 0)
|
||||||
|
{
|
||||||
|
return runIdentifyCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcasecmp(commandName.c_str(), "push") != 0)
|
||||||
|
{
|
||||||
|
Serial.printf("[BLE] ERROR: Unknown command '%s'\n", commandName.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.empty())
|
||||||
|
{
|
||||||
|
Serial.println("[BLE] ERROR: Empty push pattern");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pattern format (space-separated durations)
|
||||||
|
std::vector<uint32_t> durations;
|
||||||
|
size_t start = 0;
|
||||||
|
while (start < payload.length())
|
||||||
|
{
|
||||||
|
while (start < payload.length() && payload[start] == ' ')
|
||||||
|
{
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start >= payload.length())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t end = start;
|
||||||
|
while (end < payload.length() && payload[end] != ' ')
|
||||||
|
{
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string token = payload.substr(start, end - start);
|
||||||
|
if (!isNumberToken(token))
|
||||||
|
{
|
||||||
|
Serial.printf("[BLE] ERROR: Invalid pattern token '%s'\n", token.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
durations.push_back(static_cast<uint32_t>(strtoul(token.c_str(), nullptr, 10)));
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (durations.empty())
|
||||||
|
{
|
||||||
|
Serial.println("[BLE] ERROR: Empty pattern");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[BLE] Executing pattern with %d steps\n", static_cast<int>(durations.size()));
|
||||||
|
return runPattern(durations);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerCallbacks : public NimBLEServerCallbacks
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ServerCallbacks(TargetNode &node) : node_(node) {}
|
||||||
|
|
||||||
|
void onConnect(NimBLEServer *server) override
|
||||||
|
{
|
||||||
|
(void)server;
|
||||||
|
node_.deviceConnected_ = true;
|
||||||
|
Serial.println("[BLE] Bridge connected!");
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDisconnect(NimBLEServer *server) override
|
||||||
|
{
|
||||||
|
(void)server;
|
||||||
|
node_.deviceConnected_ = false;
|
||||||
|
Serial.println("[BLE] Bridge disconnected!");
|
||||||
|
NimBLEDevice::startAdvertising();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
TargetNode &node_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TargetCallbacks : public NimBLECharacteristicCallbacks
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit TargetCallbacks(TargetNode &node) : node_(node) {}
|
||||||
|
|
||||||
|
void onWrite(NimBLECharacteristic *characteristic) override
|
||||||
|
{
|
||||||
|
std::string rxValue = characteristic->getValue();
|
||||||
|
node_.runTextCommand(rxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
TargetNode &node_;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeConfig config_;
|
||||||
|
bool deviceConnected_;
|
||||||
|
Servo servo_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // FB_NODE_BLE_H
|
||||||
Reference in New Issue
Block a user