Restructure ESP32 firmware into layered architecture with serial streaming

Architecture changes:
- config/: Centralized pin definitions and settings
- hal/: Hardware abstraction layer (servo PWM control)
- drivers/: Device drivers (hand, emg_sensor)
- core/: Business logic (gestures)
- app/: Application entry point

New features:
- Fake EMG data streaming over USB serial at ~100 Hz
- FEATURE_FAKE_EMG flag to switch between fake/real sensors
- Python RealSerialStream class for receiving serial data

Data format: timestamp_ms,ch0,ch1,ch2,ch3
This commit is contained in:
Surya Balaji
2026-01-19 00:02:02 -06:00
parent 8435f37f84
commit b48eebe5df
17 changed files with 1077 additions and 582 deletions

View File

@@ -1,6 +1,38 @@
# This file was automatically generated for projects # CMakeLists.txt for Bucky Arm EMG Robotic Hand
# without default 'CMakeLists.txt' file. #
# Project structure:
# src/
# ├── config/ Configuration headers
# ├── hal/ Hardware Abstraction Layer
# ├── drivers/ Device drivers
# ├── core/ Core business logic
# └── app/ Application entry point
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*) # Collect all source files from each layer
set(HAL_SOURCES
hal/servo_hal.c
)
idf_component_register(SRCS ${app_sources}) set(DRIVER_SOURCES
drivers/hand.c
drivers/emg_sensor.c
)
set(CORE_SOURCES
core/gestures.c
)
set(APP_SOURCES
app/main.c
)
# Register component with ESP-IDF
idf_component_register(
SRCS
${HAL_SOURCES}
${DRIVER_SOURCES}
${CORE_SOURCES}
${APP_SOURCES}
INCLUDE_DIRS
.
)

114
EMG_Arm/src/app/main.c Normal file
View File

@@ -0,0 +1,114 @@
/**
* @file main.c
* @brief Application entry point for the EMG-controlled robotic hand.
*
* This is the top-level application that initializes all subsystems
* and runs the main loop. Currently configured to stream EMG data
* over USB serial for Python to receive.
*
* @note This is Layer 4 (Application).
*/
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "esp_timer.h"
#include "config/config.h"
#include "drivers/hand.h"
#include "drivers/emg_sensor.h"
#include "core/gestures.h"
/*******************************************************************************
* Private Functions
******************************************************************************/
/**
* @brief Stream EMG data over USB serial.
*
* Outputs data in format: "timestamp_ms,ch0,ch1,ch2,ch3\n"
* This matches what Python's SimulatedEMGStream produces.
*/
static void stream_emg_data(void)
{
emg_sample_t sample;
printf("\n[EMG] Starting data stream at %d Hz...\n", EMG_SAMPLE_RATE_HZ);
printf("[EMG] Format: timestamp_ms,ch0,ch1,ch2,ch3\n\n");
/*
* FreeRTOS tick rate is typically 100Hz (10ms per tick).
* We must delay at least 1 tick to yield to the scheduler.
* This limits our max rate to ~100 Hz, which is fine for testing.
*/
const TickType_t delay_ticks = 1; /* Minimum 1 tick (~10ms) */
while (1) {
/* Read EMG (fake or real depending on FEATURE_FAKE_EMG) */
emg_sensor_read(&sample);
/* Output in CSV format matching Python expectation */
printf("%lu,%u,%u,%u,%u\n",
(unsigned long)sample.timestamp_ms,
sample.channels[0],
sample.channels[1],
sample.channels[2],
sample.channels[3]);
/* Yield to FreeRTOS scheduler - prevents watchdog timeout */
vTaskDelay(delay_ticks);
}
}
/**
* @brief Run demo mode - cycle through gestures.
*/
static void run_demo(void)
{
printf("\n[DEMO] Running gesture demo...\n");
while (1) {
gestures_demo_fist(1000);
}
}
/*******************************************************************************
* Application Entry Point
******************************************************************************/
void app_main(void)
{
printf("\n");
printf("========================================\n");
printf(" Bucky Arm - EMG Robotic Hand\n");
printf(" Firmware v1.0.0\n");
printf("========================================\n\n");
/* Initialize subsystems */
printf("[INIT] Initializing hand (servos)...\n");
hand_init();
printf("[INIT] Initializing EMG sensor...\n");
emg_sensor_init();
#if FEATURE_FAKE_EMG
printf("[INIT] Using FAKE EMG data (sensors not connected)\n");
#else
printf("[INIT] Using REAL EMG sensors\n");
#endif
printf("[INIT] Done!\n\n");
/*
* Choose what to run:
* - stream_emg_data(): Send EMG data to laptop (Phase 1)
* - run_demo(): Test servo movement
*
* For now, we stream EMG data.
* Comment out and use run_demo() to test servos.
*/
stream_emg_data();
/* Alternative: run servo demo */
// run_demo();
}

109
EMG_Arm/src/config/config.h Normal file
View File

@@ -0,0 +1,109 @@
/**
* @file config.h
* @brief Centralized configuration for the EMG-controlled robotic hand.
*
* All hardware pin definitions and system constants in one place.
* Modify this file to adapt to different hardware configurations.
*/
#ifndef CONFIG_H
#define CONFIG_H
#include "driver/gpio.h"
#include "driver/ledc.h"
/*******************************************************************************
* Feature Flags
*
* Compile-time switches to enable/disable features.
* Set to 1 to enable, 0 to disable.
******************************************************************************/
/**
* @brief Use fake EMG data (random values) instead of real ADC reads.
*
* Set to 1 while waiting for EMG sensors to arrive.
* Set to 0 when ready to use real sensors.
*/
#define FEATURE_FAKE_EMG 1
/*******************************************************************************
* GPIO Pin Definitions - Servos
******************************************************************************/
#define PIN_SERVO_THUMB GPIO_NUM_1
#define PIN_SERVO_INDEX GPIO_NUM_4
#define PIN_SERVO_MIDDLE GPIO_NUM_5
#define PIN_SERVO_RING GPIO_NUM_6
#define PIN_SERVO_PINKY GPIO_NUM_7
/*******************************************************************************
* Servo PWM Configuration
******************************************************************************/
#define SERVO_PWM_FREQ_HZ 50 /**< Standard servo frequency */
#define SERVO_PWM_RESOLUTION LEDC_TIMER_14_BIT /**< 14-bit = 16384 levels */
#define SERVO_PWM_TIMER LEDC_TIMER_0 /**< LEDC timer for servos */
#define SERVO_PWM_SPEED_MODE LEDC_LOW_SPEED_MODE /**< ESP32-S3 uses low-speed */
#define SERVO_DUTY_MIN 430 /**< Duty cycle for 0 degrees (extended) */
#define SERVO_DUTY_MAX 2048 /**< Duty cycle for 180 degrees (flexed) */
/*******************************************************************************
* LEDC Channel Assignments
******************************************************************************/
#define LEDC_CH_THUMB LEDC_CHANNEL_0
#define LEDC_CH_INDEX LEDC_CHANNEL_1
#define LEDC_CH_MIDDLE LEDC_CHANNEL_2
#define LEDC_CH_RING LEDC_CHANNEL_3
#define LEDC_CH_PINKY LEDC_CHANNEL_4
/*******************************************************************************
* EMG Configuration
******************************************************************************/
#define EMG_NUM_CHANNELS 4 /**< Number of EMG sensor channels */
#define EMG_SAMPLE_RATE_HZ 1000 /**< Samples per second per channel */
/*******************************************************************************
* Common Type Definitions
******************************************************************************/
/**
* @brief Finger identification.
*/
typedef enum {
FINGER_THUMB = 0,
FINGER_INDEX,
FINGER_MIDDLE,
FINGER_RING,
FINGER_PINKY,
FINGER_COUNT /**< Total number of fingers (5) */
} finger_t;
/**
* @brief Recognized gestures.
*/
typedef enum {
GESTURE_NONE = 0,
GESTURE_REST,
GESTURE_FIST,
GESTURE_OPEN,
GESTURE_HOOK_EM,
GESTURE_THUMBS_UP,
GESTURE_COUNT
} gesture_t;
/**
* @brief System operating modes.
*/
typedef enum {
MODE_IDLE = 0, /**< Waiting for commands */
MODE_DATA_STREAM, /**< Streaming EMG data to laptop */
MODE_COMMAND, /**< Executing gesture commands from laptop */
MODE_DEMO, /**< Running demo sequence */
MODE_COUNT
} system_mode_t;
#endif /* CONFIG_H */

122
EMG_Arm/src/core/gestures.c Normal file
View File

@@ -0,0 +1,122 @@
/**
* @file gestures.c
* @brief Named gesture implementation.
*
* Implements gesture functions using the hand driver.
*/
#include "gestures.h"
#include "drivers/hand.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
/*******************************************************************************
* Private Data
******************************************************************************/
/** @brief Gesture name lookup table. */
static const char* gesture_names[GESTURE_COUNT] = {
"NONE",
"REST",
"FIST",
"OPEN",
"HOOK_EM",
"THUMBS_UP"
};
/*******************************************************************************
* Public Functions
******************************************************************************/
void gestures_execute(gesture_t gesture)
{
switch (gesture) {
case GESTURE_REST:
gesture_rest();
break;
case GESTURE_FIST:
gesture_fist();
break;
case GESTURE_OPEN:
gesture_open();
break;
case GESTURE_HOOK_EM:
gesture_hook_em();
break;
case GESTURE_THUMBS_UP:
gesture_thumbs_up();
break;
default:
break;
}
}
const char* gestures_get_name(gesture_t gesture)
{
if (gesture >= GESTURE_COUNT) {
return "UNKNOWN";
}
return gesture_names[gesture];
}
/*******************************************************************************
* Individual Gesture Functions
******************************************************************************/
void gesture_open(void)
{
hand_unflex_all();
}
void gesture_fist(void)
{
hand_flex_all();
}
void gesture_hook_em(void)
{
/* Index and pinky extended, others flexed */
hand_flex_finger(FINGER_THUMB);
hand_unflex_finger(FINGER_INDEX);
hand_flex_finger(FINGER_MIDDLE);
hand_flex_finger(FINGER_RING);
hand_unflex_finger(FINGER_PINKY);
}
void gesture_thumbs_up(void)
{
/* Thumb extended, others flexed */
hand_unflex_finger(FINGER_THUMB);
hand_flex_finger(FINGER_INDEX);
hand_flex_finger(FINGER_MIDDLE);
hand_flex_finger(FINGER_RING);
hand_flex_finger(FINGER_PINKY);
}
void gesture_rest(void)
{
/* Rest is same as open - neutral position */
gesture_open();
}
/*******************************************************************************
* Demo Functions
******************************************************************************/
void gestures_demo_fingers(uint32_t delay_ms)
{
for (int finger = 0; finger < FINGER_COUNT; finger++) {
hand_flex_finger((finger_t)finger);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
hand_unflex_finger((finger_t)finger);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
}
void gestures_demo_fist(uint32_t delay_ms)
{
gesture_fist();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
gesture_open();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}

View File

@@ -0,0 +1,74 @@
/**
* @file gestures.h
* @brief Named gesture definitions and execution.
*
* This module provides named gestures (fist, open, hook_em, etc.)
* that map to specific finger configurations. These are the gestures
* that the ML model will predict.
*
* @note This is Layer 3 (Core). Uses drivers/hand internally.
*/
#ifndef GESTURES_H
#define GESTURES_H
#include <stdint.h>
#include "config/config.h"
/*******************************************************************************
* Public Functions
******************************************************************************/
/**
* @brief Execute a gesture by its enum value.
*
* @param gesture Which gesture to perform
*/
void gestures_execute(gesture_t gesture);
/**
* @brief Get the name of a gesture as a string.
*
* @param gesture Which gesture
* @return Pointer to gesture name string (e.g., "FIST", "OPEN")
*/
const char* gestures_get_name(gesture_t gesture);
/*******************************************************************************
* Individual Gesture Functions
******************************************************************************/
/** @brief Open hand - all fingers extended. */
void gesture_open(void);
/** @brief Fist - all fingers flexed. */
void gesture_fist(void);
/** @brief Hook 'em Horns - index and pinky extended, others flexed. */
void gesture_hook_em(void);
/** @brief Thumbs up - thumb extended, others flexed. */
void gesture_thumbs_up(void);
/** @brief Rest - same as open (neutral position). */
void gesture_rest(void);
/*******************************************************************************
* Demo Functions
******************************************************************************/
/**
* @brief Demo: cycle each finger individually.
*
* @param delay_ms Milliseconds between movements
*/
void gestures_demo_fingers(uint32_t delay_ms);
/**
* @brief Demo: open and close fist repeatedly.
*
* @param delay_ms Milliseconds between open/close
*/
void gestures_demo_fist(uint32_t delay_ms);
#endif /* GESTURES_H */

View File

@@ -0,0 +1,55 @@
/**
* @file emg_sensor.c
* @brief EMG sensor driver implementation.
*
* Provides EMG readings - fake data for now, real ADC when sensors arrive.
*/
#include "emg_sensor.h"
#include "esp_timer.h"
#include <stdlib.h>
/*******************************************************************************
* Public Functions
******************************************************************************/
void emg_sensor_init(void)
{
#if FEATURE_FAKE_EMG
/* Seed random number generator for fake data */
srand((unsigned int)esp_timer_get_time());
#else
/* TODO: Configure ADC channels when sensors arrive */
/* adc1_config_width(EMG_ADC_WIDTH); */
/* adc1_config_channel_atten(ADC_EMG_CH0, EMG_ADC_ATTEN); */
/* ... */
#endif
}
void emg_sensor_read(emg_sample_t *sample)
{
sample->timestamp_ms = emg_sensor_get_timestamp_ms();
#if FEATURE_FAKE_EMG
/*
* Generate fake EMG data:
* - Base value around 512 (middle of 10-bit range, matching Python sim)
* - Random noise of +/- 50
* - Mimics real EMG baseline noise
*/
for (int i = 0; i < EMG_NUM_CHANNELS; i++) {
int noise = (rand() % 101) - 50; /* -50 to +50 */
sample->channels[i] = (uint16_t)(512 + noise);
}
#else
/* TODO: Real ADC reads when sensors arrive */
/* sample->channels[0] = adc1_get_raw(ADC_EMG_CH0); */
/* sample->channels[1] = adc1_get_raw(ADC_EMG_CH1); */
/* ... */
#endif
}
uint32_t emg_sensor_get_timestamp_ms(void)
{
return (uint32_t)(esp_timer_get_time() / 1000);
}

View File

@@ -0,0 +1,56 @@
/**
* @file emg_sensor.h
* @brief EMG sensor driver for reading muscle signals.
*
* This module provides EMG data acquisition. Currently generates fake
* data for testing (FEATURE_FAKE_EMG=1). When sensors arrive, the
* implementation switches to real ADC reads without changing the interface.
*
* @note This is Layer 2 (Driver).
*/
#ifndef EMG_SENSOR_H
#define EMG_SENSOR_H
#include <stdint.h>
#include "config/config.h"
/*******************************************************************************
* Data Types
******************************************************************************/
/**
* @brief Single EMG reading from all channels.
*/
typedef struct {
uint32_t timestamp_ms; /**< Timestamp in milliseconds */
uint16_t channels[EMG_NUM_CHANNELS]; /**< ADC values for each channel */
} emg_sample_t;
/*******************************************************************************
* Public Functions
******************************************************************************/
/**
* @brief Initialize the EMG sensor system.
*
* If FEATURE_FAKE_EMG is enabled, just seeds the random generator.
* Otherwise, configures ADC channels for real sensor reading.
*/
void emg_sensor_init(void);
/**
* @brief Read current values from all EMG channels.
*
* @param sample Pointer to struct to fill with current readings
*/
void emg_sensor_read(emg_sample_t *sample);
/**
* @brief Get the current timestamp in milliseconds.
*
* @return Milliseconds since boot
*/
uint32_t emg_sensor_get_timestamp_ms(void);
#endif /* EMG_SENSOR_H */

View File

@@ -0,0 +1,48 @@
/**
* @file hand.c
* @brief Hand driver implementation.
*
* Provides finger-level control using the servo HAL.
*/
#include "hand.h"
#include "hal/servo_hal.h"
/*******************************************************************************
* Public Functions
******************************************************************************/
void hand_init(void)
{
servo_hal_init();
}
void hand_flex_finger(finger_t finger)
{
servo_hal_set_duty(finger, SERVO_DUTY_MAX);
}
void hand_unflex_finger(finger_t finger)
{
servo_hal_set_duty(finger, SERVO_DUTY_MIN);
}
void hand_set_finger_angle(finger_t finger, float degrees)
{
uint32_t duty = servo_hal_degrees_to_duty(degrees);
servo_hal_set_duty(finger, duty);
}
void hand_flex_all(void)
{
for (int i = 0; i < FINGER_COUNT; i++) {
hand_flex_finger((finger_t)i);
}
}
void hand_unflex_all(void)
{
for (int i = 0; i < FINGER_COUNT; i++) {
hand_unflex_finger((finger_t)i);
}
}

View File

@@ -0,0 +1,59 @@
/**
* @file hand.h
* @brief Hand driver for individual finger control.
*
* This module provides an intuitive interface for controlling
* individual fingers - flex, unflex, or set to a specific angle.
*
* @note This is Layer 2 (Driver). Uses hal/servo_hal internally.
*/
#ifndef HAND_H
#define HAND_H
#include "config/config.h"
/*******************************************************************************
* Public Functions
******************************************************************************/
/**
* @brief Initialize the hand (all finger servos).
*
* Sets up PWM for all servos. All fingers start extended (open).
*/
void hand_init(void);
/**
* @brief Flex a finger (close it).
*
* @param finger Which finger to flex
*/
void hand_flex_finger(finger_t finger);
/**
* @brief Unflex a finger (extend/open it).
*
* @param finger Which finger to unflex
*/
void hand_unflex_finger(finger_t finger);
/**
* @brief Set a finger to a specific angle.
*
* @param finger Which finger to move
* @param degrees Angle (0 = extended, 180 = fully flexed)
*/
void hand_set_finger_angle(finger_t finger, float degrees);
/**
* @brief Flex all fingers at once.
*/
void hand_flex_all(void);
/**
* @brief Unflex all fingers at once.
*/
void hand_unflex_all(void);
#endif /* HAND_H */

View File

@@ -1,140 +0,0 @@
/**
* @file gestures.c
* @brief Gesture control implementation for the EMG-controlled robotic hand.
*
* This module implements high-level gesture functions using the low-level
* servo control interface. Each gesture function translates intuitive
* commands into appropriate servo positions.
*/
#include "gestures.h"
#include "servo.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
/*******************************************************************************
* Individual Finger Control - Flex Functions
******************************************************************************/
void flex_thumb(void)
{
servo_set_finger(FINGER_THUMB, SERVO_POS_MAX);
}
void flex_index(void)
{
servo_set_finger(FINGER_INDEX, SERVO_POS_MAX);
}
void flex_middle(void)
{
servo_set_finger(FINGER_MIDDLE, SERVO_POS_MAX);
}
void flex_ring(void)
{
servo_set_finger(FINGER_RING, SERVO_POS_MAX);
}
void flex_pinky(void)
{
servo_set_finger(FINGER_PINKY, SERVO_POS_MAX);
}
/*******************************************************************************
* Individual Finger Control - Unflex Functions
******************************************************************************/
void unflex_thumb(void)
{
servo_set_finger(FINGER_THUMB, SERVO_POS_MIN);
}
void unflex_index(void)
{
servo_set_finger(FINGER_INDEX, SERVO_POS_MIN);
}
void unflex_middle(void)
{
servo_set_finger(FINGER_MIDDLE, SERVO_POS_MIN);
}
void unflex_ring(void)
{
servo_set_finger(FINGER_RING, SERVO_POS_MIN);
}
void unflex_pinky(void)
{
servo_set_finger(FINGER_PINKY, SERVO_POS_MIN);
}
/*******************************************************************************
* Composite Gestures
******************************************************************************/
void gesture_make_fist(void)
{
/* Flex all fingers simultaneously */
flex_thumb();
flex_index();
flex_middle();
flex_ring();
flex_pinky();
}
void gesture_open_hand(void)
{
/* Extend all fingers simultaneously */
unflex_thumb();
unflex_index();
unflex_middle();
unflex_ring();
unflex_pinky();
}
/*******************************************************************************
* Demo/Test Sequences
******************************************************************************/
void demo_individual_fingers(uint32_t delay_ms)
{
/* Thumb */
flex_thumb();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
unflex_thumb();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
/* Index */
flex_index();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
unflex_index();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
/* Middle */
flex_middle();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
unflex_middle();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
/* Ring */
flex_ring();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
unflex_ring();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
/* Pinky */
flex_pinky();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
unflex_pinky();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
void demo_close_open(uint32_t delay_ms)
{
gesture_make_fist();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
gesture_open_hand();
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}

View File

@@ -1,128 +0,0 @@
/**
* @file gestures.h
* @brief Gesture control interface for the EMG-controlled robotic hand.
*
* This module provides high-level gesture functions for controlling the
* robotic hand. It builds upon the low-level servo control module to
* implement intuitive finger movements and hand gestures.
*
* Gesture Categories:
* - Individual finger control (flex/unflex each finger)
* - Composite gestures (fist, open hand, etc.)
* - Demo/test sequences
*/
#ifndef GESTURES_H
#define GESTURES_H
#include <stdint.h>
/*******************************************************************************
* Individual Finger Control - Flex Functions
*
* Flex functions move a finger to the closed (180 degree) position.
******************************************************************************/
/**
* @brief Flex the thumb (move to closed position).
*/
void flex_thumb(void);
/**
* @brief Flex the index finger (move to closed position).
*/
void flex_index(void);
/**
* @brief Flex the middle finger (move to closed position).
*/
void flex_middle(void);
/**
* @brief Flex the ring finger (move to closed position).
*/
void flex_ring(void);
/**
* @brief Flex the pinky finger (move to closed position).
*/
void flex_pinky(void);
/*******************************************************************************
* Individual Finger Control - Unflex Functions
*
* Unflex functions move a finger to the extended (0 degree) position.
******************************************************************************/
/**
* @brief Unflex the thumb (move to extended position).
*/
void unflex_thumb(void);
/**
* @brief Unflex the index finger (move to extended position).
*/
void unflex_index(void);
/**
* @brief Unflex the middle finger (move to extended position).
*/
void unflex_middle(void);
/**
* @brief Unflex the ring finger (move to extended position).
*/
void unflex_ring(void);
/**
* @brief Unflex the pinky finger (move to extended position).
*/
void unflex_pinky(void);
/*******************************************************************************
* Composite Gestures
*
* These functions control multiple fingers simultaneously to form gestures.
******************************************************************************/
/**
* @brief Close all fingers to form a fist.
*
* All five fingers move to the flexed position simultaneously.
*/
void gesture_make_fist(void);
/**
* @brief Open the hand fully.
*
* All five fingers move to the extended position simultaneously.
*/
void gesture_open_hand(void);
/*******************************************************************************
* Demo/Test Sequences
*
* These functions provide demonstration sequences for testing servo operation.
******************************************************************************/
/**
* @brief Demo sequence: flex and unflex each finger individually.
*
* Cycles through each finger, flexing and unflexing with a delay
* between each movement. Useful for testing individual servo operation.
*
* @param delay_ms Delay in milliseconds between each movement.
*/
void demo_individual_fingers(uint32_t delay_ms);
/**
* @brief Demo sequence: repeatedly close and open the hand.
*
* Alternates between making a fist and opening the hand.
* Useful for testing simultaneous servo operation.
*
* @param delay_ms Delay in milliseconds between fist and open positions.
*/
void demo_close_open(uint32_t delay_ms);
#endif /* GESTURES_H */

View File

@@ -0,0 +1,87 @@
/**
* @file servo_hal.c
* @brief Hardware Abstraction Layer for servo PWM control.
*
* Implements low-level LEDC peripheral configuration and control.
*/
#include "servo_hal.h"
#include "driver/ledc.h"
#include "esp_err.h"
/*******************************************************************************
* Private Data
******************************************************************************/
/** @brief GPIO pin for each finger servo. */
static const int servo_pins[FINGER_COUNT] = {
PIN_SERVO_THUMB,
PIN_SERVO_INDEX,
PIN_SERVO_MIDDLE,
PIN_SERVO_RING,
PIN_SERVO_PINKY
};
/** @brief LEDC channel for each finger servo. */
static const ledc_channel_t servo_channels[FINGER_COUNT] = {
LEDC_CH_THUMB,
LEDC_CH_INDEX,
LEDC_CH_MIDDLE,
LEDC_CH_RING,
LEDC_CH_PINKY
};
/*******************************************************************************
* Public Functions
******************************************************************************/
void servo_hal_init(void)
{
/* Configure LEDC timer (shared by all servo channels) */
ledc_timer_config_t timer_config = {
.speed_mode = SERVO_PWM_SPEED_MODE,
.timer_num = SERVO_PWM_TIMER,
.duty_resolution = SERVO_PWM_RESOLUTION,
.freq_hz = SERVO_PWM_FREQ_HZ,
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&timer_config));
/* Configure each finger's LEDC channel */
for (int i = 0; i < FINGER_COUNT; i++) {
ledc_channel_config_t channel_config = {
.speed_mode = SERVO_PWM_SPEED_MODE,
.channel = servo_channels[i],
.timer_sel = SERVO_PWM_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = servo_pins[i],
.duty = SERVO_DUTY_MIN, /* Start extended (open) */
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&channel_config));
}
}
void servo_hal_set_duty(finger_t finger, uint32_t duty)
{
if (finger >= FINGER_COUNT) {
return;
}
ledc_set_duty(SERVO_PWM_SPEED_MODE, servo_channels[finger], duty);
ledc_update_duty(SERVO_PWM_SPEED_MODE, servo_channels[finger]);
}
uint32_t servo_hal_degrees_to_duty(float degrees)
{
/* Clamp to valid range */
if (degrees < 0.0f) {
degrees = 0.0f;
} else if (degrees > 180.0f) {
degrees = 180.0f;
}
/* Linear interpolation: duty = min + (degrees/180) * (max - min) */
float duty = SERVO_DUTY_MIN + (degrees / 180.0f) * (SERVO_DUTY_MAX - SERVO_DUTY_MIN);
return (uint32_t)duty;
}

View File

@@ -0,0 +1,47 @@
/**
* @file servo_hal.h
* @brief Hardware Abstraction Layer for servo PWM control.
*
* This module provides low-level access to the ESP32's LEDC peripheral
* for generating PWM signals to control servo motors.
*
* @note This is Layer 1 (HAL). Only drivers/ should use this directly.
*/
#ifndef SERVO_HAL_H
#define SERVO_HAL_H
#include <stdint.h>
#include "config/config.h"
/*******************************************************************************
* Public Functions
******************************************************************************/
/**
* @brief Initialize the LEDC peripheral for all servo channels.
*
* Configures the timer and all 5 channels for 50Hz PWM output.
* All servos start in the extended (open) position.
*
* @note Must be called once before any other servo_hal functions.
*/
void servo_hal_init(void);
/**
* @brief Set the duty cycle for a specific finger's servo.
*
* @param finger Which finger (use finger_t enum from config.h)
* @param duty Duty cycle value (SERVO_DUTY_MIN to SERVO_DUTY_MAX)
*/
void servo_hal_set_duty(finger_t finger, uint32_t duty);
/**
* @brief Convert degrees to duty cycle value.
*
* @param degrees Angle in degrees (0 to 180)
* @return Corresponding duty cycle value
*/
uint32_t servo_hal_degrees_to_duty(float degrees);
#endif /* SERVO_HAL_H */

View File

@@ -1,50 +0,0 @@
/**
* @file main.c
* @brief Main application entry point for the EMG-controlled robotic hand.
*
* This application controls a 5-finger robotic hand using servo motors.
* The servos are driven by PWM signals generated through the ESP32's LEDC
* peripheral. Future versions will integrate EMG signal processing to
* translate muscle activity into hand gestures.
*
* Hardware Platform: ESP32-S3-DevKitC-1
* Framework: ESP-IDF with FreeRTOS
*/
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "servo.h"
#include "gestures.h"
/*******************************************************************************
* Configuration
******************************************************************************/
/** @brief Delay between demo movements in milliseconds. */
#define DEMO_DELAY_MS 1000
/*******************************************************************************
* Application Entry Point
******************************************************************************/
/**
* @brief Main application entry point.
*
* Initializes the servo hardware and runs a demo sequence.
* The demo can be configured to test individual finger control
* or simultaneous hand gestures.
*/
void app_main(void)
{
/* Initialize servo motors */
servo_init();
/* Run demo sequence */
while (1) {
/* Option 1: Test individual finger control */
// demo_individual_fingers(DEMO_DELAY_MS);
/* Option 2: Test simultaneous hand gestures */
demo_close_open(DEMO_DELAY_MS);
}
}

View File

@@ -1,125 +0,0 @@
/**
* @file servo.c
* @brief Servo motor control implementation for the EMG-controlled robotic hand.
*
* This module implements low-level servo control using the ESP32's LEDC peripheral.
* The LEDC is configured to generate 50Hz PWM signals suitable for standard hobby servos.
*/
#include "servo.h"
#include "esp_err.h"
/*******************************************************************************
* Private Constants
******************************************************************************/
/** @brief PWM frequency for servo control (standard is 50Hz). */
#define SERVO_PWM_FREQ_HZ 50
/** @brief PWM resolution in bits (14-bit = 16384 levels). */
#define SERVO_PWM_RESOLUTION LEDC_TIMER_14_BIT
/** @brief LEDC speed mode (ESP32-S3 only supports low-speed mode). */
#define SERVO_SPEED_MODE LEDC_LOW_SPEED_MODE
/** @brief LEDC timer used for all servos. */
#define SERVO_TIMER LEDC_TIMER_0
/*******************************************************************************
* Private Data
******************************************************************************/
/**
* @brief Mapping of finger indices to GPIO pins.
*/
static const int servo_pins[FINGER_COUNT] = {
THUMB_SERVO_PIN,
INDEX_SERVO_PIN,
MIDDLE_SERVO_PIN,
RING_SERVO_PIN,
PINKY_SERVO_PIN
};
/**
* @brief Mapping of finger indices to LEDC channels.
*/
static const ledc_channel_t servo_channels[FINGER_COUNT] = {
THUMB_CHANNEL,
INDEX_CHANNEL,
MIDDLE_CHANNEL,
RING_CHANNEL,
PINKY_CHANNEL
};
/*******************************************************************************
* Public Function Implementations
******************************************************************************/
void servo_init(void)
{
/* Configure LEDC timer (shared by all servo channels) */
ledc_timer_config_t timer_config = {
.speed_mode = SERVO_SPEED_MODE,
.timer_num = SERVO_TIMER,
.duty_resolution = SERVO_PWM_RESOLUTION,
.freq_hz = SERVO_PWM_FREQ_HZ,
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&timer_config));
/* Configure LEDC channel for each finger servo */
for (int i = 0; i < FINGER_COUNT; i++) {
ledc_channel_config_t channel_config = {
.speed_mode = SERVO_SPEED_MODE,
.channel = servo_channels[i],
.timer_sel = SERVO_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = servo_pins[i],
.duty = SERVO_POS_MIN, /* Start with fingers extended */
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&channel_config));
}
}
void servo_set_position(ledc_channel_t channel, uint32_t duty)
{
ledc_set_duty(SERVO_SPEED_MODE, channel, duty);
ledc_update_duty(SERVO_SPEED_MODE, channel);
}
void servo_set_finger(finger_t finger, uint32_t duty)
{
if (finger >= FINGER_COUNT) {
return; /* Invalid finger index */
}
servo_set_position(servo_channels[finger], duty);
}
uint32_t servo_degrees_to_duty(float degrees)
{
/* Clamp input to valid range */
if (degrees < 0.0f) {
degrees = 0.0f;
} else if (degrees > 180.0f) {
degrees = 180.0f;
}
/*
* Linear interpolation formula:
* duty = min + (degrees / 180) * (max - min)
*
* Where:
* - min = SERVO_POS_MIN (duty at 0 degrees)
* - max = SERVO_POS_MAX (duty at 180 degrees)
*/
float duty = SERVO_POS_MIN + (degrees / 180.0f) * (SERVO_POS_MAX - SERVO_POS_MIN);
return (uint32_t)duty;
}
void servo_set_finger_degrees(finger_t finger, float degrees)
{
uint32_t duty = servo_degrees_to_duty(degrees);
servo_set_finger(finger, duty);
}

View File

@@ -1,135 +0,0 @@
/**
* @file servo.h
* @brief Servo motor control interface for the EMG-controlled robotic hand.
*
* This module provides low-level servo motor control using the ESP32's LEDC
* (LED Controller) peripheral for PWM generation. It handles initialization
* and position control for all five finger servos.
*
* Hardware Configuration:
* - Thumb: GPIO 1, LEDC Channel 0
* - Index: GPIO 4, LEDC Channel 1
* - Middle: GPIO 5, LEDC Channel 2
* - Ring: GPIO 6, LEDC Channel 3
* - Pinky: GPIO 7, LEDC Channel 4
*
* @note All servos share a single LEDC timer configured for 50Hz (standard servo frequency).
*/
#ifndef SERVO_H
#define SERVO_H
#include "driver/ledc.h"
#include "driver/gpio.h"
/*******************************************************************************
* GPIO Pin Definitions
******************************************************************************/
#define THUMB_SERVO_PIN GPIO_NUM_1
#define INDEX_SERVO_PIN GPIO_NUM_4
#define MIDDLE_SERVO_PIN GPIO_NUM_5
#define RING_SERVO_PIN GPIO_NUM_6
#define PINKY_SERVO_PIN GPIO_NUM_7
/*******************************************************************************
* LEDC Channel Definitions
******************************************************************************/
#define THUMB_CHANNEL LEDC_CHANNEL_0
#define INDEX_CHANNEL LEDC_CHANNEL_1
#define MIDDLE_CHANNEL LEDC_CHANNEL_2
#define RING_CHANNEL LEDC_CHANNEL_3
#define PINKY_CHANNEL LEDC_CHANNEL_4
/*******************************************************************************
* Servo Position Definitions
*
* These duty cycle values correspond to servo positions at 14-bit resolution.
* At 50Hz with 14-bit resolution (16384 counts per 20ms period):
* - 1ms pulse (~0 degrees) = ~819 counts
* - 2ms pulse (~180 degrees) = ~1638 counts
*
* Actual values may vary based on specific servo characteristics.
******************************************************************************/
#define SERVO_POS_MIN 430 /**< Duty cycle for 0 degrees (finger extended) */
#define SERVO_POS_MAX 2048 /**< Duty cycle for 180 degrees (finger flexed) */
/*******************************************************************************
* Finger Index Enumeration
******************************************************************************/
/**
* @brief Enumeration for finger identification.
*/
typedef enum {
FINGER_THUMB = 0,
FINGER_INDEX,
FINGER_MIDDLE,
FINGER_RING,
FINGER_PINKY,
FINGER_COUNT /**< Total number of fingers (5) */
} finger_t;
/*******************************************************************************
* Public Function Declarations
******************************************************************************/
/**
* @brief Initialize all servo motors.
*
* Configures the LEDC timer and channels for all five finger servos.
* All servos are initialized to the extended (open) position.
*
* @note Must be called before any other servo functions.
*/
void servo_init(void);
/**
* @brief Set a specific servo to a given duty cycle position.
*
* @param channel The LEDC channel corresponding to the servo.
* @param duty The duty cycle value (use SERVO_POS_MIN to SERVO_POS_MAX).
*/
void servo_set_position(ledc_channel_t channel, uint32_t duty);
/**
* @brief Set a finger servo to a specific position.
*
* @param finger The finger to control (use finger_t enumeration).
* @param duty The duty cycle value (use SERVO_POS_MIN to SERVO_POS_MAX).
*/
void servo_set_finger(finger_t finger, uint32_t duty);
/**
* @brief Convert an angle in degrees to the corresponding duty cycle value.
*
* Performs linear interpolation between SERVO_POS_MIN (0 degrees) and
* SERVO_POS_MAX (180 degrees). Input values are clamped to the valid range.
*
* @param degrees The desired angle in degrees (0 to 180).
* @return The corresponding duty cycle value for the LEDC peripheral.
*
* @example
* uint32_t duty = servo_degrees_to_duty(90); // Get duty for 90 degrees
* servo_set_finger(FINGER_INDEX, duty);
*/
uint32_t servo_degrees_to_duty(float degrees);
/**
* @brief Set a finger servo to a specific angle in degrees.
*
* Convenience function that combines degree-to-duty conversion with
* finger positioning. This is the recommended function for most use cases.
*
* @param finger The finger to control (use finger_t enumeration).
* @param degrees The desired angle in degrees (0 = extended, 180 = flexed).
*
* @example
* servo_set_finger_degrees(FINGER_THUMB, 90); // Move thumb to 90 degrees
* servo_set_finger_degrees(FINGER_INDEX, 45); // Move index to 45 degrees
*/
void servo_set_finger_degrees(finger_t finger, float degrees);
#endif /* SERVO_H */

270
serial_stream.py Normal file
View File

@@ -0,0 +1,270 @@
"""
@file serial_stream.py
@brief Real serial stream for reading EMG data from ESP32.
This module provides a serial communication interface for receiving
EMG data from the ESP32 microcontroller over USB. It implements the
same interface as SimulatedEMGStream, making it a drop-in replacement.
@section usage Usage Example
@code{.py}
from serial_stream import RealSerialStream
# Create stream (auto-detects port, or specify manually)
stream = RealSerialStream(port='COM3')
stream.start()
# Read data (same interface as SimulatedEMGStream)
while True:
line = stream.readline()
if line:
print(line) # "1234,512,489,501,523"
stream.stop()
@endcode
@section format Data Format
The ESP32 sends data as CSV lines:
"timestamp_ms,ch0,ch1,ch2,ch3\\n"
Example:
"12345,512,489,501,523\\n"
@author Bucky Arm Project
"""
import serial
import serial.tools.list_ports
from typing import Optional, List
class RealSerialStream:
"""
@brief Reads EMG data from ESP32 over USB serial.
This class provides the same interface as SimulatedEMGStream:
- start() : Open serial connection
- stop() : Close serial connection
- readline() : Read one line of data
This allows it to be used as a drop-in replacement for testing
with real hardware instead of simulated data.
@note Requires pyserial: pip install pyserial
"""
def __init__(self, port: str = None, baud_rate: int = 115200, timeout: float = 1.0):
"""
@brief Initialize the serial stream.
@param port Serial port name (e.g., 'COM3' on Windows, '/dev/ttyUSB0' on Linux).
If None, will attempt to auto-detect the ESP32.
@param baud_rate Communication speed in bits per second. Default 115200 matches ESP32.
@param timeout Read timeout in seconds. Returns None if no data within this time.
"""
self.port = port
self.baud_rate = baud_rate
self.timeout = timeout
self.serial: Optional[serial.Serial] = None
self.running = False
def start(self) -> None:
"""
@brief Open the serial connection to the ESP32.
If no port was specified in __init__, attempts to auto-detect
the ESP32 by looking for common USB-UART chip identifiers.
@throws RuntimeError If no port specified and auto-detect fails.
@throws RuntimeError If unable to open the serial port.
"""
# Auto-detect port if not specified
if self.port is None:
self.port = self._auto_detect_port()
if self.port is None:
raise RuntimeError(
"No serial port specified and auto-detect failed.\n"
"Use RealSerialStream.list_ports() to see available ports."
)
# Open serial connection
try:
self.serial = serial.Serial(
port=self.port,
baudrate=self.baud_rate,
timeout=self.timeout
)
self.running = True
# Clear any stale data in the buffer
self.serial.reset_input_buffer()
print(f"[SERIAL] Connected to {self.port} at {self.baud_rate} baud")
except serial.SerialException as e:
raise RuntimeError(f"Failed to open {self.port}: {e}")
def stop(self) -> None:
"""
@brief Close the serial connection.
Safe to call even if not connected.
"""
self.running = False
if self.serial and self.serial.is_open:
self.serial.close()
print(f"[SERIAL] Disconnected from {self.port}")
def readline(self) -> Optional[str]:
"""
@brief Read one line of data from the ESP32.
Blocks until a complete line is received or timeout occurs.
This matches the interface of SimulatedEMGStream.readline().
@return Line string including newline, or None if timeout/error.
@note Lines from ESP32 are in format: "timestamp_ms,ch0,ch1,ch2,ch3\\n"
"""
if not self.serial or not self.serial.is_open:
return None
try:
line_bytes = self.serial.readline()
if line_bytes:
return line_bytes.decode('utf-8', errors='ignore')
return None
except serial.SerialException:
return None
def _auto_detect_port(self) -> Optional[str]:
"""
@brief Attempt to auto-detect the ESP32 serial port.
Looks for common USB-UART bridge chips used on ESP32 dev boards:
- CP210x (Silicon Labs)
- CH340 (WCH)
- FTDI
@return Port name if found, None otherwise.
"""
ports = serial.tools.list_ports.comports()
# Known USB-UART chip identifiers
known_chips = ['cp210', 'ch340', 'ftdi', 'usb-serial', 'usb serial']
for port in ports:
description_lower = port.description.lower()
if any(chip in description_lower for chip in known_chips):
print(f"[SERIAL] Auto-detected ESP32 on {port.device}")
return port.device
# Fallback: use first available port
if ports:
print(f"[SERIAL] No ESP32 detected, using first port: {ports[0].device}")
return ports[0].device
return None
@staticmethod
def list_ports() -> List[str]:
"""
@brief List all available serial ports on the system.
Useful for finding the correct port name for your ESP32.
@return List of port names (e.g., ['COM3', 'COM4'] on Windows).
"""
ports = serial.tools.list_ports.comports()
if not ports:
print("No serial ports found.")
return []
print("\nAvailable serial ports:")
print("-" * 50)
for port in ports:
print(f" {port.device}")
print(f" Description: {port.description}")
print()
return [p.device for p in ports]
# =============================================================================
# Standalone Test
# =============================================================================
if __name__ == "__main__":
"""
@brief Quick test to verify ESP32 serial communication.
Run this file directly to test:
python serial_stream.py [port]
If port is not specified, auto-detection is attempted.
"""
import sys
print("=" * 50)
print(" ESP32 Serial Stream Test")
print("=" * 50)
print()
# Show available ports
ports = RealSerialStream.list_ports()
if not ports:
print("No ports found. Is the ESP32 plugged in?")
sys.exit(1)
# Determine which port to use
if len(sys.argv) > 1:
port = sys.argv[1]
print(f"Using specified port: {port}")
else:
port = None # Will auto-detect
print("No port specified, will auto-detect.")
print()
print("Starting stream... (Ctrl+C to stop)")
print("-" * 50)
# Create and start stream
stream = RealSerialStream(port=port)
try:
stream.start()
sample_count = 0
while True:
line = stream.readline()
if line:
line = line.strip()
# Check if this is a data line (starts with digit = timestamp)
if line and line[0].isdigit():
sample_count += 1
# Print every 500th sample to avoid flooding terminal
if sample_count % 500 == 0:
print(f" [{sample_count:6d} samples] Latest: {line}")
else:
# Print startup/info messages from ESP32
print(f" {line}")
except KeyboardInterrupt:
print("\n")
print("-" * 50)
print("Stopped by user (Ctrl+C)")
finally:
stream.stop()
print(f"Total samples received: {sample_count}")