Change folder structure (multiple boards)

- add second pcb board_control, currently copy of previous board
- not enough pins -> board_control handle ui send motorcommands via uart
  board_motorctl handle motors
This commit is contained in:
jonny_jr9 2023-08-28 11:15:06 +02:00
parent 224ac47214
commit b03baa4687
69 changed files with 32730 additions and 1399 deletions

4
.gitignore vendored
View File

@ -10,8 +10,8 @@ sdkconfig.old
# React # React
react-app/build **/react-app/build
react-app/.pnp **/react-app/.pnp
.pnp.js .pnp.js
node_modules node_modules

View File

@ -0,0 +1,9 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(EXTRA_COMPONENT_DIRS "../components")
project(armchair_controlBoard)

259
board_control/main/main.cpp Normal file
View File

@ -0,0 +1,259 @@
extern "C"
{
#include <stdio.h>
#include <esp_system.h>
#include <esp_event.h>
#include <nvs_flash.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include "esp_spiffs.h"
#include "driver/uart.h"
//custom C files
//#include "wifi.h"
}
//custom C++ files
//#include "config.hpp"
//#include "control.hpp"
//#include "button.hpp"
//#include "http.hpp"
//tag for logging
static const char * TAG = "main";
// //======================================
// //============ buzzer task =============
// //======================================
// //TODO: move the task creation to buzzer class (buzzer.cpp)
// //e.g. only have function buzzer.createTask() in app_main
// void task_buzzer( void * pvParameters ){
// ESP_LOGI("task_buzzer", "Start of buzzer task...");
// //run function that waits for a beep events to arrive in the queue
// //and processes them
// buzzer.processQueue();
// }
//
//
//
// //=======================================
// //============ control task =============
// //=======================================
// //task that controls the armchair modes and initiates commands generation and applies them to driver
// void task_control( void * pvParameters ){
// ESP_LOGI(TAG, "Initializing controlledArmchair and starting handle loop");
// //start handle loop (control object declared in config.hpp)
// control.startHandleLoop();
// }
//
//
//
// //======================================
// //============ button task =============
// //======================================
// //task that handles the button interface/commands
// void task_button( void * pvParameters ){
// ESP_LOGI(TAG, "Initializing command-button and starting handle loop");
// //create button instance
// buttonCommands commandButton(&buttonJoystick, &joystick, &control, &buzzer, &motorLeft, &motorRight);
// //start handle loop
// commandButton.startHandleLoop();
// }
//
//
//
// //=======================================
// //============== fan task ===============
// //=======================================
// //task that controlls fans for cooling the drivers
// void task_fans( void * pvParameters ){
// ESP_LOGI(TAG, "Initializing fans and starting fan handle loop");
// //create fan instances with config defined in config.cpp
// controlledFan fan(configCooling, &motorLeft, &motorRight);
// //repeatedly run fan handle function in a slow loop
// while(1){
// fan.handle();
// vTaskDelay(500 / portTICK_PERIOD_MS);
// }
// }
//
//
//
// //=================================
// //========== init spiffs ==========
// //=================================
// //initialize spi flash filesystem (used for webserver)
// void init_spiffs(){
// ESP_LOGI(TAG, "init spiffs");
// esp_vfs_spiffs_conf_t esp_vfs_spiffs_conf = {
// .base_path = "/spiffs",
// .partition_label = NULL,
// .max_files = 5,
// .format_if_mount_failed = true};
// esp_vfs_spiffs_register(&esp_vfs_spiffs_conf);
//
// size_t total = 0;
// size_t used = 0;
// esp_spiffs_info(NULL, &total, &used);
//
// ESP_LOGI(TAG, "SPIFFS: total %d, used %d", total, used);
// esp_vfs_spiffs_unregister(NULL);
// }
//
//
//
// //==================================
// //======== define loglevels ========
// //==================================
// void setLoglevels(void){
// //set loglevel for all tags:
// esp_log_level_set("*", ESP_LOG_WARN);
//
// //--- set loglevel for individual tags ---
// esp_log_level_set("main", ESP_LOG_INFO);
// esp_log_level_set("buzzer", ESP_LOG_ERROR);
// //esp_log_level_set("motordriver", ESP_LOG_INFO);
// //esp_log_level_set("motor-control", ESP_LOG_DEBUG);
// //esp_log_level_set("evaluatedJoystick", ESP_LOG_DEBUG);
// //esp_log_level_set("joystickCommands", ESP_LOG_DEBUG);
// esp_log_level_set("button", ESP_LOG_INFO);
// esp_log_level_set("control", ESP_LOG_INFO);
// esp_log_level_set("fan-control", ESP_LOG_INFO);
// esp_log_level_set("wifi", ESP_LOG_INFO);
// esp_log_level_set("http", ESP_LOG_INFO);
// esp_log_level_set("automatedArmchair", ESP_LOG_DEBUG);
// //esp_log_level_set("current-sensors", ESP_LOG_INFO);
// }
static void uart_task(void *arg)
{
uart_config_t uart1_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
ESP_LOGW(TAG, "config...");
ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &uart1_config));
ESP_LOGW(TAG, "setpins...");
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, 23, 22, 0, 0));
ESP_LOGW(TAG, "init...");
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_1, 1024, 1024, 10, NULL, 0));
uint8_t *data = (uint8_t *) malloc(1024);
//SEND data to motorctl board
uint8_t count = 0;
ESP_LOGW(TAG, "startloop...");
while (1) {
vTaskDelay(500 / portTICK_PERIOD_MS);
int len = uart_read_bytes(UART_NUM_1, data, (1024 - 1), 20 / portTICK_PERIOD_MS);
uart_flush_input(UART_NUM_1);
uart_flush(UART_NUM_1);
ESP_LOGW(TAG, "received data %d", *data);
*data = 99;
uart_write_bytes(UART_NUM_1, (const char *) &count, 1);
ESP_LOGW(TAG, "sent data %d", count);
count++;
}
}
//=================================
//=========== app_main ============
//=================================
extern "C" void app_main(void) {
// //enable 5V volate regulator
// gpio_pad_select_gpio(GPIO_NUM_17);
// gpio_set_direction(GPIO_NUM_17, GPIO_MODE_OUTPUT);
// gpio_set_level(GPIO_NUM_17, 1);
//
// //---- define log levels ----
// setLoglevels();
//
// //------------------------------
// //--- create task for buzzer ---
// //------------------------------
// xTaskCreate(&task_buzzer, "task_buzzer", 2048, NULL, 2, NULL);
//
// //-------------------------------
// //--- create task for control ---
// //-------------------------------
// //task that generates motor commands depending on the current mode and sends those to motorctl task
// xTaskCreate(&task_control, "task_control", 4096, NULL, 5, NULL);
//
// //------------------------------
// //--- create task for button ---
// //------------------------------
// //task that evaluates and processes the button input and runs the configured commands
// xTaskCreate(&task_button, "task_button", 4096, NULL, 4, NULL);
//
// //-----------------------------------
// //--- create task for fan control ---
// //-----------------------------------
// //task that evaluates and processes the button input and runs the configured commands
// xTaskCreate(&task_fans, "task_fans", 2048, NULL, 1, NULL);
//
//
// //beep at startup
// buzzer.beep(3, 70, 50);
//
// //--- initialize nvs-flash and netif (needed for wifi) ---
// wifi_initNvs_initNetif();
//
// //--- initialize spiffs ---
// init_spiffs();
//
// //--- initialize and start wifi ---
// //FIXME: run wifi_init_client or wifi_init_ap as intended from control.cpp when switching state
// //currently commented out because of error "assert failed: xQueueSemaphoreTake queue.c:1549 (pxQueue->uxItemSize == 0)" when calling control->changeMode from button.cpp
// //when calling control.changeMode(http) from main.cpp it worked without error for some reason?
// ESP_LOGI(TAG,"starting wifi...");
// //wifi_init_client(); //connect to existing wifi
// wifi_init_ap(); //start access point
// ESP_LOGI(TAG,"done starting wifi");
//
//
// //--- testing http server ---
// // wifi_init_client(); //connect to existing wifi
// // vTaskDelay(2000 / portTICK_PERIOD_MS);
// // ESP_LOGI(TAG, "initializing http server");
// // http_init_server();
//
//
// //--- testing force http mode after startup ---
// //control.changeMode(controlMode_t::HTTP);
//
//
// //--- main loop ---
// //does nothing except for testing things
//TESTING UART
xTaskCreate(uart_task, "uart_task", 4096, NULL, 10, NULL);
while(1){
vTaskDelay(1000 / portTICK_PERIOD_MS);
//---------------------------------
//-------- TESTING section --------
//---------------------------------
}
}

View File

@ -5,4 +5,5 @@
cmake_minimum_required(VERSION 3.5) cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(EXTRA_COMPONENT_DIRS "../components")
project(armchair) project(armchair)

143
board_motorctl/README.md Normal file
View File

@ -0,0 +1,143 @@
Firmware for a homemade automated electric armchair.
More details about this project: https://pfusch.zone/electric-armchair
# Installation
### Install esp-idf
For this project **ESP-IDF v4.4.4** is required (with other versions it might not compile)
```bash
#download esp-idf
yay -S esp-idf #alternatively clone the esp-idf repository from github
#run installation script in installed folder
/opt/esp-idf/install.sh
```
### Clone this repo
```
git clone git@github.com:Jonny999999/armchair_fw
```
### Instal node packages
For the react app packages have to be installed with npm TODO: add this to cmake?
```
cd react-app
npm install
```
# Compilation
## react-webapp
For the webapp to work on the esp32 it has to be built.
When flashing, the folder react-app/build is flashed to siffs (which is used as webroot) onto the esp32.
The following command builds the react webapp and creates this folder
TODO: add this to flash target with cmake?
```bash
cd react-app
#compile
npm run build
#remove unwanted license file (filename too long for spiffs)
rm build/static/js/main.8f9aec76.js.LICENSE.txt
```
Note: Use `npm start` for starting the webapp locally for testing
## esp project
### Set up environment
```bash
source /opt/esp-idf/export.sh
```
(run once in terminal)
### Compile
```bash
idf.py build
```
### Upload
- connect FTDI programmer to board (VCC to VCC; TX to RX; RX to TX)
- press REST and BOOT button
- release RESET button (keep pressing boot)
- run flash command:
```bash
idf.py flash
```
- once "connecting...' successfully, BOOT button can be released
### Monitor
- connect FTDI programmer to board (VCC to VCC; TX to RX; RX to TX)
- press REST and BOOT button
- release RESET button (keep pressing boot)
- run monitor command:
```bash
idf.py monitor
```
- once connected release BOOT button
- press RESET button once for restart
# Hardware setup
## pcb
Used pcb developed in this project: https://pfusch.zone/project-work-2020
## connection plan
A diagram which shows what components are connected to which terminals of the pcb exists here:
[connection-plan.drawio.pdf](connection-plan.drawio.pdf)
# Planned Features
- More sensors:
- Accelerometer
- Lidar sensor
- GPS receiver
- Anti slip regulation
- Self driving algorithm
- Lights
- drinks holder
- improved webinterface
# Todo
**Add switch functions**
- set loglevel
- define max-speed
- calibrate joystick (min, max, center)
- testing mode / dry-run
# Usage
## Switch functions
**Currently implemented**
| Count | Type | Action | Description |
| --- | --- | --- | --- |
| 1x | configure | [JOYSTICK] **calibrate stick** | when in joystick mode: set joystick center to current joystick pos |
| 1x | control | [MASSAGE] **freeze** input | when in massage mode: lock or unlock joystick input at current position |
| 2x | toggle mode | **IDLE** <=> previous | enable/disable chair armchair e.g. enable after startup or timeout |
| 3x | switch mode | **JOYSTICK** | switch to default mode JOYSTICK |
| 4x | toggle mode | **HTTP** <=> JOYSTICK | switch to '**remote control** via web-app `http://191.168.4.1`' or back to JOYSTICK mode |
| 5x | | | |
| 6x | toggle mode | **MASSAGE** <=> JOYSTICK | switch to MASSAGE mode or back to JOYSTICK mode |
| 7x | | | |
| 8x | toggle option | **deceleration limit** | disable/enable deceleration limit (default on) => more responsive |
| | | | |
| 12x | toggle option | **alt stick mapping** | toggle between default and alternative stick mapping (reverse swapped) |
| >1s | system | **restart** | Restart the controller when pressing the button longer than 1 second |
| 1x short, 1x long | auto command | **eject** foot support | automatically go forward and reverse for certain time with no acceleration limits, so foot support ejects |
## HTTP mode
Control armchair via virtual joystick on a webinterface.
**Usage**
- Connect to wifi `armchar`, no password
- Access http://192.168.4.1 (note: **http** NOT https, some browsers automatically add https!)
**Current Features**
- Control direction and speed with joystick
**Todo**
- Set parameters
- Control other modes
- Execute preset movement commands

View File

@ -0,0 +1,20 @@
idf_component_register(
SRCS
"main.cpp"
"motordrivers.cpp"
"motorctl.cpp"
"config.cpp"
"joystick.cpp"
"buzzer.cpp"
"control.cpp"
"button.cpp"
"fan.cpp"
"wifi.c"
"http.cpp"
"auto.cpp"
"currentsensor.cpp"
INCLUDE_DIRS
"."
)
spiffs_create_partition_image(spiffs ../react-app/build FLASH_IN_PROJECT)

View File

@ -0,0 +1,88 @@
#include "auto.hpp"
#include "config.hpp"
//tag for logging
static const char * TAG = "automatedArmchair";
//=============================
//======== constructor ========
//=============================
automatedArmchair::automatedArmchair(void) {
//create command queue
commandQueue = xQueueCreate( 32, sizeof( commandSimple_t ) ); //TODO add max size to config?
}
//==============================
//====== generateCommands ======
//==============================
motorCommands_t automatedArmchair::generateCommands(auto_instruction_t * instruction) {
//reset instruction
*instruction = auto_instruction_t::NONE;
//check if previous command is finished
if ( esp_log_timestamp() > timestampCmdFinished ) {
//get next command from queue
if( xQueueReceive( commandQueue, &cmdCurrent, pdMS_TO_TICKS(500) ) ) {
ESP_LOGI(TAG, "running next command from queue...");
//copy instruction to be provided to control task
*instruction = cmdCurrent.instruction;
//set acceleration / fading parameters according to command
motorLeft.setFade(fadeType_t::DECEL, cmdCurrent.fadeDecel);
motorRight.setFade(fadeType_t::DECEL, cmdCurrent.fadeDecel);
motorLeft.setFade(fadeType_t::ACCEL, cmdCurrent.fadeAccel);
motorRight.setFade(fadeType_t::ACCEL, cmdCurrent.fadeAccel);
//calculate timestamp the command is finished
timestampCmdFinished = esp_log_timestamp() + cmdCurrent.msDuration;
//copy the new commands
motorCommands = cmdCurrent.motorCmds;
} else { //queue empty
ESP_LOGD(TAG, "no new command in queue -> set motors to IDLE");
motorCommands = motorCmds_bothMotorsIdle;
}
} else { //previous command still running
ESP_LOGD(TAG, "command still running -> no change");
}
//TODO also return instructions via call by reference
return motorCommands;
}
//============================
//======== addCommand ========
//============================
//function that adds a basic command to the queue
void automatedArmchair::addCommand(commandSimple_t command) {
//add command to queue
if ( xQueueSend( commandQueue, ( void * )&command, ( TickType_t ) 0 ) ){
ESP_LOGI(TAG, "Successfully inserted command to queue");
} else {
ESP_LOGE(TAG, "Failed to insert new command to queue");
}
}
void automatedArmchair::addCommands(commandSimple_t commands[], size_t count) {
for (int i = 0; i < count; i++) {
ESP_LOGI(TAG, "Reading command no. %d from provided array", i);
addCommand(commands[i]);
}
}
//===============================
//======== clearCommands ========
//===============================
//function that deletes all pending/queued commands
//e.g. when switching modes
motorCommands_t automatedArmchair::clearCommands() {
//clear command queue
xQueueReset( commandQueue );
ESP_LOGW(TAG, "command queue was successfully emptied");
//return commands for idling both motors
motorCommands = motorCmds_bothMotorsIdle;
return motorCmds_bothMotorsIdle;
}

View File

@ -0,0 +1,87 @@
#pragma once
extern "C"
{
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_err.h"
}
#include "freertos/queue.h"
#include <cmath>
#include "motorctl.hpp"
//--------------------------------------------
//---- struct, enum, variable declarations ---
//--------------------------------------------
//enum for special instructions / commands to be run in control task
enum class auto_instruction_t { NONE, SWITCH_PREV_MODE, SWITCH_JOYSTICK_MODE, RESET_ACCEL_DECEL, RESET_ACCEL, RESET_DECEL };
//struct for a simple command
//e.g. put motors in a certain state for certain time
typedef struct commandSimple_t{
motorCommands_t motorCmds;
uint32_t msDuration;
uint32_t fadeDecel;
uint32_t fadeAccel;
auto_instruction_t instruction;
} commandSimple_t;
//------------------------------------
//----- automatedArmchair class -----
//------------------------------------
class automatedArmchair {
public:
//--- methods ---
//constructor
automatedArmchair(void);
//function to generate motor commands
//can be also seen as handle function
//TODO: go with other approach: separate task for handling auto mode
// - receive commands with queue anyways
// - => use delay function
// - have a queue that outputs current motor state/commands -> repeatedly check the queue in control task
//function that handles automatic driving and returns motor commands
//also provides instructions to be executed in control task via pointer
motorCommands_t generateCommands(auto_instruction_t * instruction);
//function that adds a basic command to the queue
void addCommand(commandSimple_t command);
//function that adds an array of basic commands to queue
void addCommands(commandSimple_t commands[], size_t count);
//function that deletes all pending/queued commands
motorCommands_t clearCommands();
private:
//--- methods ---
//--- objects ---
//TODO: add buzzer here
//--- variables ---
//queue for storing pending commands
QueueHandle_t commandQueue = NULL;
//current running command
commandSimple_t cmdCurrent;
//timestamp current command is finished
uint32_t timestampCmdFinished = 0;
motorCommands_t motorCommands;
//command preset for idling motors
const motorCommand_t motorCmd_motorIdle = {
.state = motorstate_t::IDLE,
.duty = 0
};
const motorCommands_t motorCmds_bothMotorsIdle = {
.left = motorCmd_motorIdle,
.right = motorCmd_motorIdle
};
};

View File

@ -0,0 +1,215 @@
extern "C"
{
#include <stdio.h>
#include <esp_system.h>
#include <esp_event.h>
#include "freertos/FreeRTOS.h"
#include "esp_log.h"
}
#include "button.hpp"
//tag for logging
static const char * TAG = "button";
//-----------------------------
//-------- constructor --------
//-----------------------------
buttonCommands::buttonCommands(gpio_evaluatedSwitch * button_f, evaluatedJoystick * joystick_f, controlledArmchair * control_f, buzzer_t * buzzer_f, controlledMotor * motorLeft_f, controlledMotor * motorRight_f){
//copy object pointers
button = button_f;
joystick = joystick_f;
control = control_f;
buzzer = buzzer_f;
motorLeft = motorLeft_f;
motorRight = motorRight_f;
//TODO declare / configure evaluatedSwitch here instead of config (unnecessary that button object is globally available - only used here)?
}
//----------------------------
//--------- action -----------
//----------------------------
//function that runs commands depending on a count value
void buttonCommands::action (uint8_t count, bool lastPressLong){
//--- variable declarations ---
bool decelEnabled; //for different beeping when toggling
commandSimple_t cmds[8]; //array for commands for automatedArmchair
//--- get joystick position ---
//joystickData_t stickData = joystick->getData();
//--- actions based on count ---
switch (count){
//no such command
default:
ESP_LOGE(TAG, "no command for count=%d defined", count);
buzzer->beep(3, 400, 100);
break;
case 1:
//restart contoller when 1x long pressed
if (lastPressLong){
ESP_LOGW(TAG, "RESTART");
buzzer->beep(1,1000,1);
vTaskDelay(1000 / portTICK_PERIOD_MS);
esp_restart();
return;
}
ESP_LOGW(TAG, "cmd %d: sending button event to control task", count);
//-> define joystick center or toggle freeze input (executed in control task)
control->sendButtonEvent(count); //TODO: always send button event to control task (not just at count=1) -> control.cpp has to be changed
break;
case 2:
//run automatic commands to lift leg support when pressed 1x short 1x long
if (lastPressLong){
//define commands
cmds[0] =
{
.motorCmds = {
.left = {motorstate_t::REV, 90},
.right = {motorstate_t::REV, 90}
},
.msDuration = 1200,
.fadeDecel = 800,
.fadeAccel = 1300,
.instruction = auto_instruction_t::NONE
};
cmds[1] =
{
.motorCmds = {
.left = {motorstate_t::FWD, 70},
.right = {motorstate_t::FWD, 70}
},
.msDuration = 70,
.fadeDecel = 0,
.fadeAccel = 300,
.instruction = auto_instruction_t::NONE
};
cmds[2] =
{
.motorCmds = {
.left = {motorstate_t::IDLE, 0},
.right = {motorstate_t::IDLE, 0}
},
.msDuration = 10,
.fadeDecel = 800,
.fadeAccel = 1300,
.instruction = auto_instruction_t::SWITCH_JOYSTICK_MODE
};
//send commands to automatedArmchair command queue
armchair.addCommands(cmds, 3);
//change mode to AUTO
control->changeMode(controlMode_t::AUTO);
return;
}
//toggle idle when 2x pressed
ESP_LOGW(TAG, "cmd %d: toggle IDLE", count);
control->toggleIdle(); //toggle between idle and previous/default mode
break;
case 3:
ESP_LOGW(TAG, "cmd %d: switch to JOYSTICK", count);
control->changeMode(controlMode_t::JOYSTICK); //switch to JOYSTICK mode
break;
case 4:
ESP_LOGW(TAG, "cmd %d: toggle between HTTP and JOYSTICK", count);
control->toggleModes(controlMode_t::HTTP, controlMode_t::JOYSTICK); //toggle between HTTP and JOYSTICK mode
break;
case 6:
ESP_LOGW(TAG, "cmd %d: toggle between MASSAGE and JOYSTICK", count);
control->toggleModes(controlMode_t::MASSAGE, controlMode_t::JOYSTICK); //toggle between MASSAGE and JOYSTICK mode
break;
case 8:
//toggle deceleration fading between on and off
decelEnabled = motorLeft->toggleFade(fadeType_t::DECEL);
motorRight->toggleFade(fadeType_t::DECEL);
ESP_LOGW(TAG, "cmd %d: toggle deceleration fading to: %d", count, (int)decelEnabled);
if (decelEnabled){
buzzer->beep(3, 60, 50);
} else {
buzzer->beep(1, 1000, 1);
}
break;
case 12:
ESP_LOGW(TAG, "cmd %d: sending button event to control task", count);
//-> toggle altStickMapping (executed in control task)
control->sendButtonEvent(count); //TODO: always send button event to control task (not just at count=1)?
break;
}
}
//-----------------------------
//------ startHandleLoop ------
//-----------------------------
//this function has to be started once in a separate task
//repeatedly evaluates and processes button events then takes the corresponding action
void buttonCommands::startHandleLoop() {
while(1) {
vTaskDelay(20 / portTICK_PERIOD_MS);
//run handle function of evaluatedSwitch object
button->handle();
//--- count button presses and run action ---
switch(state) {
case inputState_t::IDLE: //wait for initial button press
if (button->risingEdge) {
count = 1;
buzzer->beep(1, 65, 0);
timestamp_lastAction = esp_log_timestamp();
state = inputState_t::WAIT_FOR_INPUT;
ESP_LOGI(TAG, "first button press detected -> waiting for further events");
}
break;
case inputState_t::WAIT_FOR_INPUT: //wait for further presses
//button pressed again
if (button->risingEdge){
count++;
buzzer->beep(1, 65, 0);
timestamp_lastAction = esp_log_timestamp();
ESP_LOGI(TAG, "another press detected -> count=%d -> waiting for further events", count);
}
//timeout
else if (esp_log_timestamp() - timestamp_lastAction > 1000) {
state = inputState_t::IDLE;
buzzer->beep(count, 50, 50);
//TODO: add optional "bool wait" parameter to beep function to delay until finished beeping
ESP_LOGI(TAG, "timeout - running action function for count=%d", count);
//--- run action function ---
//check if still pressed
bool lastPressLong = false;
if (button->state == true){
//run special case when last press was longer than timeout
lastPressLong = true;
}
//run action function with current count of button presses
action(count, lastPressLong);
}
break;
}
}
}

View File

@ -0,0 +1,53 @@
#pragma once
#include "gpio_evaluateSwitch.hpp"
#include "buzzer.hpp"
#include "control.hpp"
#include "motorctl.hpp"
#include "auto.hpp"
#include "config.hpp"
#include "joystick.hpp"
//===================================
//====== buttonCommands class =======
//===================================
//class which runs commands depending on the count a button was pressed
class buttonCommands {
public:
//--- constructor ---
buttonCommands (
gpio_evaluatedSwitch * button_f,
evaluatedJoystick * joystick_f,
controlledArmchair * control_f,
buzzer_t * buzzer_f,
controlledMotor * motorLeft_f,
controlledMotor * motorRight_f
);
//--- functions ---
//the following function has to be started once in a separate task.
//repeatedly evaluates and processes button events then takes the corresponding action
void startHandleLoop();
private:
//--- functions ---
void action(uint8_t count, bool lastPressLong);
//--- objects ---
gpio_evaluatedSwitch* button;
evaluatedJoystick* joystick;
controlledArmchair * control;
buzzer_t* buzzer;
controlledMotor * motorLeft;
controlledMotor * motorRight;
//--- variables ---
uint8_t count = 0;
uint32_t timestamp_lastAction = 0;
enum class inputState_t {IDLE, WAIT_FOR_INPUT};
inputState_t state = inputState_t::IDLE;
};

View File

@ -0,0 +1,95 @@
#include "buzzer.hpp"
#include "config.hpp"
static const char *TAG_BUZZER = "buzzer";
//============================
//========== init ============
//============================
//define gpio pin as output, initialize queue
void buzzer_t::init(){
//define buzzer pin as output
gpio_pad_select_gpio(gpio_pin);
gpio_set_direction(gpio_pin, GPIO_MODE_OUTPUT);
//create queue
beepQueue = xQueueCreate( 20, sizeof( struct beepEntry ) );
}
//=============================
//======== constructor ========
//=============================
//copy provided config parameters to private variables, run init function
buzzer_t::buzzer_t(gpio_num_t gpio_pin_f, uint16_t msGap_f){
ESP_LOGI(TAG_BUZZER, "Initializing buzzer");
//copy configuration parameters to variables
gpio_pin = gpio_pin_f;
msGap = msGap_f;
//run init function to initialize gpio and queue
init();
};
//============================
//=========== beep ===========
//============================
//function to add a beep command to the queue
void buzzer_t::beep(uint8_t count, uint16_t msOn, uint16_t msOff){
//create entry struct with provided data
struct beepEntry entryInsert = {
count = count,
msOn = msOn,
msOff = msOff
};
// Send a pointer to a struct AMessage object. Don't block if the
// queue is already full.
//struct beepEntry *entryInsertPointer;
//entryInsertPointer = &entryInsertData;
ESP_LOGW(TAG_BUZZER, "Inserted object to queue - count=%d, msOn=%d, msOff=%d", entryInsert.count, entryInsert.msOn, entryInsert.msOff);
//xQueueGenericSend( beepQueue, ( void * ) &entryInsertPointer, ( TickType_t ) 0, queueSEND_TO_BACK );
xQueueSend( beepQueue, ( void * )&entryInsert, ( TickType_t ) 0 );
}
//==============================
//======== processQueue ========
//==============================
void buzzer_t::processQueue(){
//struct for receiving incomming events
struct beepEntry entryRead = { };
//loop forever
while(1){
ESP_LOGD(TAG_BUZZER, "processQueue: waiting for beep command");
//if queue is ready
if( beepQueue != 0 )
{
// wait for a queue entry to be available indefinetely if INCLUDE_vTaskSuspend is enabled in the FreeRTOS config
// otherwise waits for at least 7 weeks
if( xQueueReceive( beepQueue, &entryRead, portMAX_DELAY ) )
{
ESP_LOGW(TAG_BUZZER, "Read entry from queue: count=%d, msOn=%d, msOff=%d", entryRead.count, entryRead.msOn, entryRead.msOff);
//beep requested count with requested delays
for (int i = entryRead.count; i--;){
//turn on
ESP_LOGD(TAG_BUZZER, "turning buzzer on");
gpio_set_level(gpio_pin, 1);
vTaskDelay(entryRead.msOn / portTICK_PERIOD_MS);
//turn off
ESP_LOGD(TAG_BUZZER, "turning buzzer off");
gpio_set_level(gpio_pin, 0);
vTaskDelay(entryRead.msOff / portTICK_PERIOD_MS);
}
//wait for minimum gap between beep events
vTaskDelay(msGap / portTICK_PERIOD_MS);
}
}else{ //wait for queue to become available
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
}

View File

@ -0,0 +1,56 @@
#pragma once
#include <stdio.h>
extern "C"
{
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
}
#include "freertos/queue.h"
//===================================
//========= buzzer_t class ==========
//===================================
//class which blinks a gpio pin for the provided count and durations.
//- 'processQueue' has to be run in a separate task
//- uses a queue to queue up multiple beep commands
class buzzer_t {
public:
//--- constructor ---
buzzer_t(gpio_num_t gpio_pin_f, uint16_t msGap_f = 200);
//--- functions ---
void processQueue(); //has to be run once in a separate task, waits for and processes queued events
void beep(uint8_t count, uint16_t msOn, uint16_t msOff);
//void clear(); (TODO - not implemented yet)
//void createTask(); (TODO - not implemented yet)
//--- variables ---
uint16_t msGap; //gap between beep entries (when multiple queued)
private:
//--- functions ---
void init();
//--- variables ---
gpio_num_t gpio_pin;
struct beepEntry {
uint8_t count;
uint16_t msOn;
uint16_t msOff;
};
//queue for queueing up multiple events while one is still processing
QueueHandle_t beepQueue = NULL;
};

View File

@ -0,0 +1,151 @@
#include "config.hpp"
//===================================
//======= motor configuration =======
//===================================
//--- configure left motor (hardware) ---
single100a_config_t configDriverLeft = {
.gpio_pwm = GPIO_NUM_26,
.gpio_a = GPIO_NUM_16,
.gpio_b = GPIO_NUM_4,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.aEnabledPinState = false, //-> pins inverted (mosfets)
.bEnabledPinState = false,
.resolution = LEDC_TIMER_11_BIT,
.pwmFreq = 10000
};
//--- configure right motor (hardware) ---
single100a_config_t configDriverRight = {
.gpio_pwm = GPIO_NUM_27,
.gpio_a = GPIO_NUM_2,
.gpio_b = GPIO_NUM_14,
.ledc_timer = LEDC_TIMER_1,
.ledc_channel = LEDC_CHANNEL_1,
.aEnabledPinState = false, //-> pin inverted (mosfet)
.bEnabledPinState = true, //-> not inverted (direct)
.resolution = LEDC_TIMER_11_BIT,
.pwmFreq = 10000
};
//TODO add motor name string -> then use as log tag?
//--- configure left motor (contol) ---
motorctl_config_t configMotorControlLeft = {
.msFadeAccel = 1900, //acceleration of the motor (ms it takes from 0% to 100%)
.msFadeDecel = 1000, //deceleration of the motor (ms it takes from 100% to 0%)
.currentLimitEnabled = true,
.currentSensor_adc = ADC1_CHANNEL_6, //GPIO34
.currentSensor_ratedCurrent = 50,
.currentMax = 30,
.deadTimeMs = 900 //minimum time motor is off between direction change
};
//--- configure right motor (contol) ---
motorctl_config_t configMotorControlRight = {
.msFadeAccel = 1900, //acceleration of the motor (ms it takes from 0% to 100%)
.msFadeDecel = 1000, //deceleration of the motor (ms it takes from 100% to 0%)
.currentLimitEnabled = true,
.currentSensor_adc = ADC1_CHANNEL_4, //GPIO32
.currentSensor_ratedCurrent = 50,
.currentMax = 30,
.deadTimeMs = 900 //minimum time motor is off between direction change
};
//==============================
//======= control config =======
//==============================
control_config_t configControl = {
.defaultMode = controlMode_t::JOYSTICK, //default mode after startup and toggling IDLE
//--- timeout ---
.timeoutMs = 5*60*1000, //time of inactivity after which the mode gets switched to IDLE
.timeoutTolerancePer = 5, //percentage the duty can vary between timeout checks considered still inactive
//--- http mode ---
};
//===============================
//===== httpJoystick config =====
//===============================
httpJoystick_config_t configHttpJoystickMain{
.toleranceZeroX_Per = 1, //percentage around joystick axis the coordinate snaps to 0
.toleranceZeroY_Per = 6,
.toleranceEndPer = 2, //percentage before joystick end the coordinate snaps to 1/-1
.timeoutMs = 2500 //time no new data was received before the motors get turned off
};
//======================================
//======= joystick configuration =======
//======================================
joystick_config_t configJoystick = {
.adc_x = ADC1_CHANNEL_3, //GPIO39
.adc_y = ADC1_CHANNEL_0, //GPIO36
//percentage of joystick range the coordinate of the axis snaps to 0 (0-100)
.tolerance_zeroX_per = 7, //6
.tolerance_zeroY_per = 10, //7
//percentage of joystick range the coordinate snaps to -1 or 1 before configured "_max" or "_min" threshold (mechanical end) is reached (0-100)
.tolerance_end_per = 4,
//threshold the radius jumps to 1 before the stick is at max radius (range 0-1)
.tolerance_radius = 0.09,
//min and max adc values of each axis, !!!AFTER INVERSION!!! is applied:
.x_min = 1392, //=> x=-1
.x_max = 2650, //=> x=1
.y_min = 1390, //=> y=-1
.y_max = 2640, //=> y=1
//invert adc measurement
.x_inverted = true,
.y_inverted = true
};
//============================
//=== configure fan contol ===
//============================
fan_config_t configCooling = {
.gpio_fan = GPIO_NUM_13,
.dutyThreshold = 40,
.minOnMs = 1500,
.minOffMs = 3000,
.turnOffDelayMs = 5000,
};
//=================================
//===== create global objects =====
//=================================
//TODO outsource global variables to e.g. global.cpp and only config options here?
//create controlled motor instances (motorctl.hpp)
controlledMotor motorLeft(configDriverLeft, configMotorControlLeft);
controlledMotor motorRight(configDriverRight, configMotorControlRight);
//create global joystic instance (joystick.hpp)
evaluatedJoystick joystick(configJoystick);
//create global evaluated switch instance for button next to joystick
gpio_evaluatedSwitch buttonJoystick(GPIO_NUM_25, true, false); //pullup true, not inverted (switch to GND use pullup of controller)
//create buzzer object on pin 12 with gap between queued events of 100ms
buzzer_t buzzer(GPIO_NUM_12, 100);
//create global httpJoystick object (http.hpp)
httpJoystick httpJoystickMain(configHttpJoystickMain);
//create global control object (control.hpp)
controlledArmchair control(configControl, &buzzer, &motorLeft, &motorRight, &joystick, &httpJoystickMain);
//create global automatedArmchair object (for auto-mode) (auto.hpp)
automatedArmchair armchair;

View File

@ -0,0 +1,46 @@
#pragma once
#include "motordrivers.hpp"
#include "motorctl.hpp"
#include "joystick.hpp"
#include "gpio_evaluateSwitch.hpp"
#include "buzzer.hpp"
#include "control.hpp"
#include "fan.hpp"
#include "http.hpp"
#include "auto.hpp"
//in IDLE mode: set loglevel for evaluatedJoystick to DEBUG
//and repeatedly read joystick e.g. for manually calibrating / testing joystick
//#define JOYSTICK_LOG_IN_IDLE
//TODO outsource global variables to e.g. global.cpp and only config options here?
//create global controlledMotor instances for both motors
extern controlledMotor motorLeft;
extern controlledMotor motorRight;
//create global joystic instance
extern evaluatedJoystick joystick;
//create global evaluated switch instance for button next to joystick
extern gpio_evaluatedSwitch buttonJoystick;
//create global buzzer object
extern buzzer_t buzzer;
//create global control object
extern controlledArmchair control;
//create global automatedArmchair object (for auto-mode)
extern automatedArmchair armchair;
//create global httpJoystick object
extern httpJoystick httpJoystickMain;
//configuration for fans / cooling
extern fan_config_t configCooling;

View File

@ -0,0 +1,479 @@
extern "C"
{
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "esp_log.h"
#include "freertos/queue.h"
//custom C libraries
#include "wifi.h"
}
#include "config.hpp"
#include "control.hpp"
//used definitions moved from config.hpp:
//#define JOYSTICK_TEST
//tag for logging
static const char * TAG = "control";
const char* controlModeStr[7] = {"IDLE", "JOYSTICK", "MASSAGE", "HTTP", "MQTT", "BLUETOOTH", "AUTO"};
//-----------------------------
//-------- constructor --------
//-----------------------------
controlledArmchair::controlledArmchair (
control_config_t config_f,
buzzer_t * buzzer_f,
controlledMotor* motorLeft_f,
controlledMotor* motorRight_f,
evaluatedJoystick* joystick_f,
httpJoystick* httpJoystick_f
){
//copy configuration
config = config_f;
//copy object pointers
buzzer = buzzer_f;
motorLeft = motorLeft_f;
motorRight = motorRight_f;
joystick_l = joystick_f,
httpJoystickMain_l = httpJoystick_f;
//set default mode from config
modePrevious = config.defaultMode;
//TODO declare / configure controlled motors here instead of config (unnecessary that button object is globally available - only used here)?
}
//----------------------------------
//---------- Handle loop -----------
//----------------------------------
//function that repeatedly generates motor commands depending on the current mode
//also handles fading and current-limit
void controlledArmchair::startHandleLoop() {
while (1){
ESP_LOGV(TAG, "control task executing... mode=%s", controlModeStr[(int)mode]);
switch(mode) {
default:
mode = controlMode_t::IDLE;
break;
case controlMode_t::IDLE:
//copy preset commands for idling both motors
commands = cmds_bothMotorsIdle;
motorRight->setTarget(commands.right.state, commands.right.duty);
motorLeft->setTarget(commands.left.state, commands.left.duty);
vTaskDelay(200 / portTICK_PERIOD_MS);
#ifdef JOYSTICK_LOG_IN_IDLE
//get joystick data here (without using it)
//since loglevel is DEBUG, calculateion details is output
joystick_l->getData(); //get joystick data here
#endif
break;
case controlMode_t::JOYSTICK:
vTaskDelay(20 / portTICK_PERIOD_MS);
//get current joystick data with getData method of evaluatedJoystick
stickData = joystick_l->getData();
//additionaly scale coordinates (more detail in slower area)
joystick_scaleCoordinatesLinear(&stickData, 0.6, 0.35); //TODO: add scaling parameters to config
//generate motor commands
commands = joystick_generateCommandsDriving(stickData, altStickMapping);
//apply motor commands
motorRight->setTarget(commands.right.state, commands.right.duty);
motorLeft->setTarget(commands.left.state, commands.left.duty);
//TODO make motorctl.setTarget also accept motorcommand struct directly
break;
case controlMode_t::MASSAGE:
vTaskDelay(10 / portTICK_PERIOD_MS);
//--- read joystick ---
//only update joystick data when input not frozen
if (!freezeInput){
stickData = joystick_l->getData();
}
//--- generate motor commands ---
//pass joystick data from getData method of evaluatedJoystick to generateCommandsShaking function
commands = joystick_generateCommandsShaking(stickData);
//apply motor commands
motorRight->setTarget(commands.right.state, commands.right.duty);
motorLeft->setTarget(commands.left.state, commands.left.duty);
break;
case controlMode_t::HTTP:
//--- get joystick data from queue ---
//Note this function waits several seconds (httpconfig.timeoutMs) for data to arrive, otherwise Center data or NULL is returned
//TODO: as described above, when changing modes it might delay a few seconds for the change to apply
stickData = httpJoystickMain_l->getData();
//scale coordinates additionally (more detail in slower area)
joystick_scaleCoordinatesLinear(&stickData, 0.6, 0.4); //TODO: add scaling parameters to config
ESP_LOGD(TAG, "generating commands from x=%.3f y=%.3f radius=%.3f angle=%.3f", stickData.x, stickData.y, stickData.radius, stickData.angle);
//--- generate motor commands ---
//Note: timeout (no data received) is handled in getData method
commands = joystick_generateCommandsDriving(stickData, altStickMapping);
//--- apply commands to motors ---
//TODO make motorctl.setTarget also accept motorcommand struct directly
motorRight->setTarget(commands.right.state, commands.right.duty);
motorLeft->setTarget(commands.left.state, commands.left.duty);
break;
case controlMode_t::AUTO:
vTaskDelay(20 / portTICK_PERIOD_MS);
//generate commands
commands = armchair.generateCommands(&instruction);
//--- apply commands to motors ---
//TODO make motorctl.setTarget also accept motorcommand struct directly
motorRight->setTarget(commands.right.state, commands.right.duty);
motorLeft->setTarget(commands.left.state, commands.left.duty);
//process received instruction
switch (instruction) {
case auto_instruction_t::NONE:
break;
case auto_instruction_t::SWITCH_PREV_MODE:
toggleMode(controlMode_t::AUTO);
break;
case auto_instruction_t::SWITCH_JOYSTICK_MODE:
changeMode(controlMode_t::JOYSTICK);
break;
case auto_instruction_t::RESET_ACCEL_DECEL:
//enable downfading (set to default value)
motorLeft->setFade(fadeType_t::DECEL, true);
motorRight->setFade(fadeType_t::DECEL, true);
//set upfading to default value
motorLeft->setFade(fadeType_t::ACCEL, true);
motorRight->setFade(fadeType_t::ACCEL, true);
break;
case auto_instruction_t::RESET_ACCEL:
//set upfading to default value
motorLeft->setFade(fadeType_t::ACCEL, true);
motorRight->setFade(fadeType_t::ACCEL, true);
break;
case auto_instruction_t::RESET_DECEL:
//enable downfading (set to default value)
motorLeft->setFade(fadeType_t::DECEL, true);
motorRight->setFade(fadeType_t::DECEL, true);
break;
}
break;
//TODO: add other modes here
}
//--- run actions based on received button button event ---
//note: buttonCount received by sendButtonEvent method called from button.cpp
//TODO: what if variable gets set from other task during this code? -> mutex around this code
switch (buttonCount) {
case 1: //define joystick center or freeze input
if (mode == controlMode_t::JOYSTICK){
//joystick mode: calibrate joystick
joystick_l->defineCenter();
} else if (mode == controlMode_t::MASSAGE){
//massage mode: toggle freeze of input (lock joystick at current values)
freezeInput = !freezeInput;
if (freezeInput){
buzzer->beep(5, 40, 25);
} else {
buzzer->beep(1, 300, 100);
}
}
break;
case 12: //toggle alternative joystick mapping (reverse swapped)
altStickMapping = !altStickMapping;
if (altStickMapping){
buzzer->beep(6, 70, 50);
} else {
buzzer->beep(1, 500, 100);
}
break;
}
//--- reset button event --- (only one action per run)
if (buttonCount > 0){
ESP_LOGI(TAG, "resetting button event/count");
buttonCount = 0;
}
//-----------------------
//------ slow loop ------
//-----------------------
//this section is run about every 5s (+500ms)
if (esp_log_timestamp() - timestamp_SlowLoopLastRun > 5000) {
ESP_LOGV(TAG, "running slow loop... time since last run: %.1fs", (float)(esp_log_timestamp() - timestamp_SlowLoopLastRun)/1000);
timestamp_SlowLoopLastRun = esp_log_timestamp();
//run function which detects timeout (switch to idle)
handleTimeout();
}
}//end while(1)
}//end startHandleLoop
//-----------------------------------
//---------- resetTimeout -----------
//-----------------------------------
void controlledArmchair::resetTimeout(){
//TODO mutex
timestamp_lastActivity = esp_log_timestamp();
}
//------------------------------------
//--------- sendButtonEvent ----------
//------------------------------------
void controlledArmchair::sendButtonEvent(uint8_t count){
//TODO mutex - if not replaced with queue
ESP_LOGI(TAG, "setting button event");
buttonCount = count;
}
//------------------------------------
//---------- handleTimeout -----------
//------------------------------------
//percentage the duty can vary since last timeout check and still counts as incative
//TODO: add this to config
float inactivityTolerance = 10;
//local function that checks whether two values differ more than a given tolerance
bool validateActivity(float dutyOld, float dutyNow, float tolerance){
float dutyDelta = dutyNow - dutyOld;
if (fabs(dutyDelta) < tolerance) {
return false; //no significant activity detected
} else {
return true; //there was activity
}
}
//function that evaluates whether there is no activity/change on the motor duty for a certain time. If so, a switch to IDLE is issued. - has to be run repeatedly in a slow interval
void controlledArmchair::handleTimeout(){
//check for timeout only when not idling already
if (mode != controlMode_t::IDLE) {
//get current duty from controlled motor objects
float dutyLeftNow = motorLeft->getStatus().duty;
float dutyRightNow = motorRight->getStatus().duty;
//activity detected on any of the two motors
if (validateActivity(dutyLeft_lastActivity, dutyLeftNow, inactivityTolerance)
|| validateActivity(dutyRight_lastActivity, dutyRightNow, inactivityTolerance)
){
ESP_LOGD(TAG, "timeout check: [activity] detected since last check -> reset");
//reset last duty and timestamp
dutyLeft_lastActivity = dutyLeftNow;
dutyRight_lastActivity = dutyRightNow;
resetTimeout();
}
//no activity on any motor and msTimeout exceeded
else if (esp_log_timestamp() - timestamp_lastActivity > config.timeoutMs){
ESP_LOGI(TAG, "timeout check: [TIMEOUT], no activity for more than %.ds -> switch to idle", config.timeoutMs/1000);
//toggle to idle mode
toggleIdle();
}
else {
ESP_LOGD(TAG, "timeout check: [inactive], last activity %.1f s ago, timeout after %d s", (float)(esp_log_timestamp() - timestamp_lastActivity)/1000, config.timeoutMs/1000);
}
}
}
//-----------------------------------
//----------- changeMode ------------
//-----------------------------------
//function to change to a specified control mode
void controlledArmchair::changeMode(controlMode_t modeNew) {
//reset timeout timer
resetTimeout();
//exit if target mode is already active
if (mode == modeNew) {
ESP_LOGE(TAG, "changeMode: Already in target mode '%s' -> nothing to change", controlModeStr[(int)mode]);
return;
}
//copy previous mode
modePrevious = mode;
ESP_LOGW(TAG, "=== changing mode from %s to %s ===", controlModeStr[(int)mode], controlModeStr[(int)modeNew]);
//========== commands change FROM mode ==========
//run functions when changing FROM certain mode
switch(modePrevious){
default:
ESP_LOGI(TAG, "noting to execute when changing FROM this mode");
break;
#ifdef JOYSTICK_LOG_IN_IDLE
case controlMode_t::IDLE:
ESP_LOGI(TAG, "disabling debug output for 'evaluatedJoystick'");
esp_log_level_set("evaluatedJoystick", ESP_LOG_WARN); //FIXME: loglevel from config
break;
#endif
case controlMode_t::HTTP:
ESP_LOGW(TAG, "switching from http mode -> disabling http and wifi");
//stop http server
ESP_LOGI(TAG, "disabling http server...");
http_stop_server();
//FIXME: make wifi function work here - currently starting wifi at startup (see notes main.cpp)
//stop wifi
//TODO: decide whether ap or client is currently used - which has to be disabled?
//ESP_LOGI(TAG, "deinit wifi...");
//wifi_deinit_client();
//wifi_deinit_ap();
ESP_LOGI(TAG, "done stopping http mode");
break;
case controlMode_t::MASSAGE:
ESP_LOGW(TAG, "switching from MASSAGE mode -> restoring fading, reset frozen input");
//TODO: fix issue when downfading was disabled before switching to massage mode - currently it gets enabled again here...
//enable downfading (set to default value)
motorLeft->setFade(fadeType_t::DECEL, true);
motorRight->setFade(fadeType_t::DECEL, true);
//set upfading to default value
motorLeft->setFade(fadeType_t::ACCEL, true);
motorRight->setFade(fadeType_t::ACCEL, true);
//reset frozen input state
freezeInput = false;
break;
case controlMode_t::AUTO:
ESP_LOGW(TAG, "switching from AUTO mode -> restoring fading to default");
//TODO: fix issue when downfading was disabled before switching to auto mode - currently it gets enabled again here...
//enable downfading (set to default value)
motorLeft->setFade(fadeType_t::DECEL, true);
motorRight->setFade(fadeType_t::DECEL, true);
//set upfading to default value
motorLeft->setFade(fadeType_t::ACCEL, true);
motorRight->setFade(fadeType_t::ACCEL, true);
break;
}
//========== commands change TO mode ==========
//run functions when changing TO certain mode
switch(modeNew){
default:
ESP_LOGI(TAG, "noting to execute when changing TO this mode");
break;
case controlMode_t::IDLE:
buzzer->beep(1, 1500, 0);
#ifdef JOYSTICK_LOG_IN_IDLE
esp_log_level_set("evaluatedJoystick", ESP_LOG_DEBUG);
#endif
break;
case controlMode_t::HTTP:
ESP_LOGW(TAG, "switching to http mode -> enabling http and wifi");
//start wifi
//TODO: decide wether ap or client should be started
ESP_LOGI(TAG, "init wifi...");
//FIXME: make wifi function work here - currently starting wifi at startup (see notes main.cpp)
//wifi_init_client();
//wifi_init_ap();
//wait for wifi
//ESP_LOGI(TAG, "waiting for wifi...");
//vTaskDelay(1000 / portTICK_PERIOD_MS);
//start http server
ESP_LOGI(TAG, "init http server...");
http_init_server();
ESP_LOGI(TAG, "done initializing http mode");
break;
case controlMode_t::MASSAGE:
ESP_LOGW(TAG, "switching to MASSAGE mode -> reducing fading");
uint32_t shake_msFadeAccel = 500; //TODO: move this to config
//disable downfading (max. deceleration)
motorLeft->setFade(fadeType_t::DECEL, false);
motorRight->setFade(fadeType_t::DECEL, false);
//reduce upfading (increase acceleration)
motorLeft->setFade(fadeType_t::ACCEL, shake_msFadeAccel);
motorRight->setFade(fadeType_t::ACCEL, shake_msFadeAccel);
break;
}
//--- update mode to new mode ---
//TODO: add mutex
mode = modeNew;
}
//TODO simplify the following 3 functions? can be replaced by one?
//-----------------------------------
//----------- toggleIdle ------------
//-----------------------------------
//function to toggle between IDLE and previous active mode
void controlledArmchair::toggleIdle() {
//toggle between IDLE and previous mode
toggleMode(controlMode_t::IDLE);
}
//------------------------------------
//----------- toggleModes ------------
//------------------------------------
//function to toggle between two modes, but prefer first argument if entirely different mode is currently active
void controlledArmchair::toggleModes(controlMode_t modePrimary, controlMode_t modeSecondary) {
//switch to secondary mode when primary is already active
if (mode == modePrimary){
ESP_LOGW(TAG, "toggleModes: switching from primaryMode %s to secondarMode %s", controlModeStr[(int)mode], controlModeStr[(int)modeSecondary]);
buzzer->beep(2,200,100);
changeMode(modeSecondary); //switch to secondary mode
}
//switch to primary mode when any other mode is active
else {
ESP_LOGW(TAG, "toggleModes: switching from %s to primary mode %s", controlModeStr[(int)mode], controlModeStr[(int)modePrimary]);
buzzer->beep(4,200,100);
changeMode(modePrimary);
}
}
//-----------------------------------
//----------- toggleMode ------------
//-----------------------------------
//function that toggles between certain mode and previous mode
void controlledArmchair::toggleMode(controlMode_t modePrimary){
//switch to previous mode when primary is already active
if (mode == modePrimary){
ESP_LOGW(TAG, "toggleMode: switching from primaryMode %s to previousMode %s", controlModeStr[(int)mode], controlModeStr[(int)modePrevious]);
//buzzer->beep(2,200,100);
changeMode(modePrevious); //switch to previous mode
}
//switch to primary mode when any other mode is active
else {
ESP_LOGW(TAG, "toggleModes: switching from %s to primary mode %s", controlModeStr[(int)mode], controlModeStr[(int)modePrimary]);
//buzzer->beep(4,200,100);
changeMode(modePrimary);
}
}

View File

@ -0,0 +1,130 @@
#pragma once
#include "motordrivers.hpp"
#include "motorctl.hpp"
#include "buzzer.hpp"
#include "http.hpp"
#include "auto.hpp"
//--------------------------------------------
//---- struct, enum, variable declarations ---
//--------------------------------------------
//enum that decides how the motors get controlled
enum class controlMode_t {IDLE, JOYSTICK, MASSAGE, HTTP, MQTT, BLUETOOTH, AUTO};
//string array representing the mode enum (for printing the state as string)
extern const char* controlModeStr[7];
//--- control_config_t ---
//struct with config parameters
typedef struct control_config_t {
controlMode_t defaultMode; //default mode after startup and toggling IDLE
//timeout options
uint32_t timeoutMs; //time of inactivity after which the mode gets switched to IDLE
float timeoutTolerancePer; //percentage the duty can vary between timeout checks considered still inactive
} control_config_t;
//==================================
//========= control class ==========
//==================================
//controls the mode the armchair operates
//repeatedly generates the motor commands corresponding to current mode and sends those to motorcontrol
class controlledArmchair {
public:
//--- constructor ---
controlledArmchair (
control_config_t config_f,
buzzer_t* buzzer_f,
controlledMotor* motorLeft_f,
controlledMotor* motorRight_f,
evaluatedJoystick* joystick_f,
httpJoystick* httpJoystick_f
);
//--- functions ---
//task that repeatedly generates motor commands depending on the current mode
void startHandleLoop();
//function that changes to a specified control mode
void changeMode(controlMode_t modeNew);
//function that toggle between IDLE and previous active mode (or default if not switched to certain mode yet)
void toggleIdle();
//function that toggles between two modes, but prefers first argument if entirely different mode is currently active
void toggleModes(controlMode_t modePrimary, controlMode_t modeSecondary);
//toggle between certain mode and previous mode
void toggleMode(controlMode_t modePrimary);
//function that restarts timer which initiates the automatic timeout (switch to IDLE) after certain time of inactivity
void resetTimeout();
//function for sending a button event (e.g. from button task at event) to control task
//TODO: use queue instead?
void sendButtonEvent(uint8_t count);
private:
//--- functions ---
//function that evaluates whether there is no activity/change on the motor duty for a certain time, if so a switch to IDLE is issued. - has to be run repeatedly in a slow interval
void handleTimeout();
//--- objects ---
buzzer_t* buzzer;
controlledMotor* motorLeft;
controlledMotor* motorRight;
httpJoystick* httpJoystickMain_l;
evaluatedJoystick* joystick_l;
//---variables ---
//struct for motor commands returned by generate functions of each mode
motorCommands_t commands;
//struct with config parameters
control_config_t config;
//store joystick data
joystickData_t stickData;
bool altStickMapping; //alternative joystick mapping (reverse mapped differently)
//variables for http mode
uint32_t http_timestamp_lastData = 0;
//variables for MASSAGE mode
bool freezeInput = false;
//variables for AUTO mode
auto_instruction_t instruction = auto_instruction_t::NONE; //variable to receive instructions from automatedArmchair
//variable to store button event
uint8_t buttonCount = 0;
//definition of mode enum
controlMode_t mode = controlMode_t::IDLE;
//variable to store mode when toggling IDLE mode
controlMode_t modePrevious; //default mode
//command preset for idling motors
const motorCommand_t cmd_motorIdle = {
.state = motorstate_t::IDLE,
.duty = 0
};
const motorCommands_t cmds_bothMotorsIdle = {
.left = cmd_motorIdle,
.right = cmd_motorIdle
};
//variable for slow loop
uint32_t timestamp_SlowLoopLastRun = 0;
//variables for detecting timeout (switch to idle, after inactivity)
float dutyLeft_lastActivity = 0;
float dutyRight_lastActivity = 0;
uint32_t timestamp_lastActivity = 0;
};

View File

@ -0,0 +1,75 @@
extern "C" {
#include "hal/timer_types.h"
#include "esp_log.h"
}
#include "currentsensor.hpp"
//tag for logging
static const char * TAG = "current-sensors";
//--------------------------
//------- getVoltage -------
//--------------------------
//local function to get average voltage from adc
float getVoltage(adc1_channel_t adc, uint32_t samples){
//measure voltage
int measure = 0;
for (int j=0; j<samples; j++){
measure += adc1_get_raw(adc);
ets_delay_us(50);
}
return (float)measure / samples / 4096 * 3.3;
}
//=============================
//======== constructor ========
//=============================
currentSensor::currentSensor (adc1_channel_t adcChannel_f, float ratedCurrent_f){
//copy config
adcChannel = adcChannel_f;
ratedCurrent = ratedCurrent_f;
//init adc
adc1_config_width(ADC_WIDTH_BIT_12); //max resolution 4096
adc1_config_channel_atten(adcChannel, ADC_ATTEN_DB_11); //max voltage
//calibrate
calibrateZeroAmpere();
}
//============================
//=========== read ===========
//============================
float currentSensor::read(void){
//measure voltage
voltage = getVoltage(adcChannel, 30);
//scale voltage to current
if (voltage < centerVoltage){
current = (1 - voltage / centerVoltage) * -ratedCurrent;
} else if (voltage > centerVoltage){
current = (voltage - centerVoltage) / (3.3 - centerVoltage) * ratedCurrent;
}else {
current = 0;
}
ESP_LOGI(TAG, "read sensor adc=%d: voltage=%.3fV, centerVoltage=%.3fV => current=%.3fA", (int)adcChannel, voltage, centerVoltage, current);
return current;
}
//===============================
//===== calibrateZeroAmpere =====
//===============================
void currentSensor::calibrateZeroAmpere(void){
//measure voltage
float prev = centerVoltage;
centerVoltage = getVoltage(adcChannel, 100);
ESP_LOGW(TAG, "defined centerVoltage (0A) to %.3f (previous %.3f)", centerVoltage, prev);
}

View File

@ -0,0 +1,20 @@
#include <driver/adc.h>
//supported current sensor working method:
//0V = -ratedCurrent
//centerVoltage = 0A
//3.3V = ratedCurrent
class currentSensor{
public:
currentSensor (adc1_channel_t adcChannel_f, float ratedCurrent);
void calibrateZeroAmpere(void); //set current voltage to voltage representing 0A
float read(void); //get current ampere
private:
adc1_channel_t adcChannel;
float ratedCurrent;
uint32_t measure;
float voltage;
float current;
float centerVoltage = 3.3/2;
};

View File

@ -0,0 +1,82 @@
extern "C"
{
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
}
#include "fan.hpp"
//tag for logging
static const char * TAG = "fan-control";
//-----------------------------
//-------- constructor --------
//-----------------------------
controlledFan::controlledFan (fan_config_t config_f, controlledMotor* motor1_f, controlledMotor* motor2_f ){
//copy config
config = config_f;
//copy pointer to motor objects
motor1 = motor1_f;
motor2 = motor2_f;
//initialize gpio pin
gpio_pad_select_gpio(config.gpio_fan);
gpio_set_direction(config.gpio_fan, GPIO_MODE_OUTPUT);
}
//--------------------------
//--------- handle ---------
//--------------------------
void controlledFan::handle(){
//get current state of the motor (motorctl.cpp)
motor1Status = motor1->getStatus();
motor2Status = motor2->getStatus();
//--- handle duty threshold ---
//update timestamp if any threshold exceeded
if (motor1Status.duty > config.dutyThreshold
|| motor2Status.duty > config.dutyThreshold){ //TODO add temperature threshold
if (!needsCooling){
timestamp_needsCoolingSet = esp_log_timestamp();
needsCooling = true;
}
timestamp_lastThreshold = esp_log_timestamp();
} else {
needsCooling = false;
}
//--- turn off condition ---
if (fanRunning
&& !needsCooling //no more cooling required
&& (motor1Status.duty == 0) && (motor2Status.duty == 0) //both motors are off
//-> keeps fans running even when lower than threshold already, however turnOffDelay already started TODO: start turn off delay after motor stop only?
&& (esp_log_timestamp() - timestamp_lastThreshold) > config.turnOffDelayMs){ //turn off delay passed
fanRunning = false;
gpio_set_level(config.gpio_fan, 0);
timestamp_turnedOff = esp_log_timestamp();
ESP_LOGI(TAG, "turned fan OFF gpio=%d, minOnMs=%d, WasOnMs=%d", (int)config.gpio_fan, config.minOnMs, esp_log_timestamp()-timestamp_turnedOn);
}
//--- turn on condition ---
if (!fanRunning
&& needsCooling
&& ((esp_log_timestamp() - timestamp_turnedOff) > config.minOffMs) //fans off long enough
&& ((esp_log_timestamp() - timestamp_needsCoolingSet) > config.minOnMs)){ //motors on long enough
fanRunning = true;
gpio_set_level(config.gpio_fan, 1);
timestamp_turnedOn = esp_log_timestamp();
ESP_LOGI(TAG, "turned fan ON gpio=%d, minOffMs=%d, WasOffMs=%d", (int)config.gpio_fan, config.minOffMs, esp_log_timestamp()-timestamp_turnedOff);
}
//TODO Add statemachine for more specific control? Exponential average?
//TODO idea: try other aproach? increment a variable with certain weights e.g. integrate over duty, then turn fans on and decrement the variable again
ESP_LOGD(TAG, "fanState=%d, duty1=%f, duty2=%f, needsCooling=%d", fanRunning, motor1Status.duty, motor2Status.duty, needsCooling);
}

View File

@ -0,0 +1,49 @@
#pragma once
extern "C"
{
#include "driver/gpio.h"
}
#include "motorctl.hpp"
//--- fan_config_t ---
//struct with all config parameters for a fan
typedef struct fan_config_t {
gpio_num_t gpio_fan;
float dutyThreshold;
uint32_t minOnMs;
uint32_t minOffMs;
uint32_t turnOffDelayMs;
} fan_config;
//==================================
//====== controlledFan class =======
//==================================
class controlledFan {
public:
//--- constructor ---
controlledFan (fan_config_t config_f, controlledMotor* motor1_f, controlledMotor* motor2_f );
//--- functions ---
void handle(); //has to be run repeatedly in a slow loop
private:
//--- variables ---
bool fanRunning = false;
bool needsCooling = false;
uint32_t timestamp_needsCoolingSet;
uint32_t timestamp_lastThreshold = 0;
uint32_t timestamp_turnedOn = 0;
uint32_t timestamp_turnedOff = 0;
fan_config_t config;
controlledMotor * motor1;
controlledMotor * motor2;
motorCommand_t motor1Status;
motorCommand_t motor2Status;
};

View File

@ -0,0 +1,270 @@
extern "C"
{
#include <stdio.h>
#include "mdns.h"
#include "cJSON.h"
#include "esp_spiffs.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "freertos/queue.h"
}
#include "http.hpp"
#include "config.hpp"
//tag for logging
static const char * TAG = "http";
static httpd_handle_t server = NULL;
//==============================
//===== start mdns service =====
//==============================
//TODO: test this, not working?
//function that initializes and starts mdns server for host discovery
void start_mdns_service()
{
//init queue for sending joystickdata from http endpoint to control task
mdns_init();
mdns_hostname_set("armchair");
mdns_instance_name_set("electric armchair");
}
//===========================
//======= default url =======
//===========================
//serve requested files from spiffs
static esp_err_t on_default_url(httpd_req_t *req)
{
ESP_LOGI(TAG, "Opening page for URL: %s", req->uri);
esp_vfs_spiffs_conf_t esp_vfs_spiffs_conf = {
.base_path = "/spiffs",
.partition_label = NULL,
.max_files = 5,
.format_if_mount_failed = true};
esp_vfs_spiffs_register(&esp_vfs_spiffs_conf);
char path[600];
if (strcmp(req->uri, "/") == 0)
strcpy(path, "/spiffs/index.html");
else
sprintf(path, "/spiffs%s", req->uri);
char *ext = strrchr(path, '.');
if (ext == NULL || strncmp(ext, ".local", strlen(".local")) == 0)
{
httpd_resp_set_status(req, "301 Moved Permanently");
httpd_resp_set_hdr(req, "Location", "/");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
if (strcmp(ext, ".css") == 0)
httpd_resp_set_type(req, "text/css");
if (strcmp(ext, ".js") == 0)
httpd_resp_set_type(req, "text/javascript");
if (strcmp(ext, ".png") == 0)
httpd_resp_set_type(req, "image/png");
FILE *file = fopen(path, "r");
if (file == NULL)
{
httpd_resp_send_404(req);
esp_vfs_spiffs_unregister(NULL);
return ESP_OK;
}
char lineRead[256];
while (fgets(lineRead, sizeof(lineRead), file))
{
httpd_resp_sendstr_chunk(req, lineRead);
}
httpd_resp_sendstr_chunk(req, NULL);
esp_vfs_spiffs_unregister(NULL);
return ESP_OK;
}
//==============================
//===== httpJoystick class =====
//==============================
//-----------------------
//----- constructor -----
//-----------------------
httpJoystick::httpJoystick( httpJoystick_config_t config_f ){
//copy config struct
config = config_f;
}
//--------------------------
//---- receiveHttpData -----
//--------------------------
//joystick endpoint - function that is called when data is received with post request at /api/joystick
esp_err_t httpJoystick::receiveHttpData(httpd_req_t *req){
//--- add header ---
//to allow cross origin (otherwise browser fails when app is running on another host)
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
//--- get data from http request ---
char buffer[100];
memset(&buffer, 0, sizeof(buffer));
httpd_req_recv(req, buffer, req->content_len);
ESP_LOGD(TAG, "/api/joystick: received data: %s", buffer);
//--- parse received json string to json object ---
cJSON *payload = cJSON_Parse(buffer);
ESP_LOGV(TAG, "parsed json: \n %s", cJSON_Print(payload));
//--- extract relevant items from json object ---
cJSON *x_json = cJSON_GetObjectItem(payload, "x");
cJSON *y_json = cJSON_GetObjectItem(payload, "y");
//--- save items to struct ---
joystickData_t data = { };
//note cjson can only interpret values as numbers when there are no quotes around the values in json (are removed from json on client side)
//convert json to double to float
data.x = static_cast<float>(x_json->valuedouble);
data.y = static_cast<float>(y_json->valuedouble);
//log received and parsed values
ESP_LOGI(TAG, "received values: x=%.3f y=%.3f",
data.x, data.y);
// scaleCoordinate(input, min, max, center, tolerance_zero_per, tolerance_end_per)
data.x = scaleCoordinate(data.x+1, 0, 2, 1, config.toleranceZeroX_Per, config.toleranceEndPer);
data.y = scaleCoordinate(data.y+1, 0, 2, 1, config.toleranceZeroY_Per, config.toleranceEndPer);
//--- calculate radius with new/scaled coordinates ---
data.radius = sqrt(pow(data.x,2) + pow(data.y,2));
//TODO: radius tolerance? (as in original joystick func)
//limit radius to 1
if (data.radius > 1) {
data.radius = 1;
}
//--- calculate angle ---
data.angle = (atan(data.y/data.x) * 180) / 3.141;
//--- evaluate position ---
data.position = joystick_evaluatePosition(data.x, data.y);
//log processed values
ESP_LOGI(TAG, "processed values: x=%.3f y=%.3f radius=%.3f angle=%.3f pos=%s",
data.x, data.y, data.radius, data.angle, joystickPosStr[(int)data.position]);
//--- free memory ---
cJSON_Delete(payload);
//--- send data to control task via queue ---
//xQueueSend( joystickDataQueue, ( void * )&data, ( TickType_t ) 0 );
//changed to length = 1 -> overwrite - older values are no longer relevant
xQueueOverwrite( joystickDataQueue, ( void * )&data );
//--- return http response ---
httpd_resp_set_status(req, "204 NO CONTENT");
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
//-------------------
//----- getData -----
//-------------------
//wait for and return joystick data from queue, if timeout return NULL
joystickData_t httpJoystick::getData(){
//--- get joystick data from queue ---
if( xQueueReceive( joystickDataQueue, &dataRead, pdMS_TO_TICKS(config.timeoutMs) ) ) {
ESP_LOGD(TAG, "getData: received data (from queue): x=%.3f y=%.3f radius=%.3f angle=%.3f",
dataRead.x, dataRead.y, dataRead.radius, dataRead.angle);
}
//--- timeout ---
//no new data received within configured timeout
else {
//send error message when last received data did NOT result in CENTER position
if (dataRead.position != joystickPos_t::CENTER) {
//change data to "joystick center" data to stop the motors
dataRead = dataCenter;
ESP_LOGE(TAG, "TIMEOUT - no data received for 3s -> set to center");
}
}
return dataRead;
}
//--------------------------------------------
//--- receiveHttpData for httpJoystickMain ---
//--------------------------------------------
//function that wraps pointer to member function of httpJoystickMain instance in a "normal" function which the webserver can run on joystick URL
//declare pointer to receiveHttpData method of httpJoystick class
esp_err_t (httpJoystick::*pointerToReceiveFunc)(httpd_req_t *req) = &httpJoystick::receiveHttpData;
esp_err_t on_joystick_url(httpd_req_t *req){
//run pointer to receiveHttpData function of httpJoystickMain instance
return (httpJoystickMain.*pointerToReceiveFunc)(req);
}
//============================
//===== init http server =====
//============================
//function that initializes http server and configures available urls
void http_init_server()
{
//---- configure webserver ----
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.uri_match_fn = httpd_uri_match_wildcard;
//---- start webserver ----
ESP_ERROR_CHECK(httpd_start(&server, &config));
//----- define URLs -----
httpd_uri_t joystick_url;
joystick_url.uri = "/api/joystick";
joystick_url.method = HTTP_POST;
joystick_url.handler = on_joystick_url;
httpd_register_uri_handler(server, &joystick_url);
httpd_uri_t default_url;
default_url.uri = "/*";
default_url.method = HTTP_GET;
default_url.handler = on_default_url;
httpd_register_uri_handler(server, &default_url);
//previous approach with sockets:
// httpd_uri_t socket_joystick_url = {
// .uri = "/ws-api/joystick",
// .method = HTTP_GET,
// .handler = on_socket_joystick_url,
// .is_websocket = true};
// httpd_register_uri_handler(server, &socket_joystick_url);
}
//============================
//===== stop http server =====
//============================
//function that destroys the http server
void http_stop_server()
{
printf("stopping http\n");
httpd_stop(server);
}

View File

@ -0,0 +1,70 @@
#pragma once
extern "C"
{
#include "esp_http_server.h"
}
#include "joystick.hpp"
//============================
//===== init http server =====
//============================
//function that initializes http server and configures available urls
void http_init_server();
//==============================
//===== start mdns service =====
//==============================
//function that initializes and starts mdns server for host discovery
void start_mdns_service();
//============================
//===== stop http server =====
//============================
//function that destroys the http server
void http_stop_server();
//==============================
//===== httpJoystick class =====
//==============================
//class that receices that from a HTTP post request, generates and scales joystick data and provides the data in a queue
//struct with configuration parameters
typedef struct httpJoystick_config_t {
float toleranceZeroX_Per;//percentage around joystick axis the coordinate snaps to 0
float toleranceZeroY_Per;
float toleranceEndPer; //percentage before joystick end the coordinate snaps to 1/-1
uint32_t timeoutMs; //time no new data was received before the motors get turned off
} httpJoystick_config_t;
class httpJoystick{
public:
//--- constructor ---
httpJoystick( httpJoystick_config_t config_f );
//--- functions ---
joystickData_t getData(); //wait for and return joystick data from queue, if timeout return CENTER
esp_err_t receiveHttpData(httpd_req_t *req); //function that is called when data is received with post request at /api/joystick
private:
//--- variables ---
httpJoystick_config_t config;
QueueHandle_t joystickDataQueue = xQueueCreate( 1, sizeof( struct joystickData_t ) );
//struct for receiving data from http function, and storing data of last update
joystickData_t dataRead;
const joystickData_t dataCenter = {
.position = joystickPos_t::CENTER,
.x = 0,
.y = 0,
.radius = 0,
.angle = 0
};
};

View File

@ -0,0 +1,572 @@
extern "C" {
#include "hal/timer_types.h"
}
#include "joystick.hpp"
//definition of string array to be able to convert state enum to readable string
const char* joystickPosStr[7] = {"CENTER", "Y_AXIS", "X_AXIS", "TOP_RIGHT", "TOP_LEFT", "BOTTOM_LEFT", "BOTTOM_RIGHT"};
//tags for logging
static const char * TAG = "evaluatedJoystick";
static const char * TAG_CMD = "joystickCommands";
//-----------------------------
//-------- constructor --------
//-----------------------------
//copy provided struct with all configuration and run init function
evaluatedJoystick::evaluatedJoystick(joystick_config_t config_f){
config = config_f;
init();
}
//----------------------------
//---------- init ------------
//----------------------------
void evaluatedJoystick::init(){
ESP_LOGI(TAG, "initializing joystick");
//initialize adc
adc1_config_width(ADC_WIDTH_BIT_12); //=> max resolution 4096
//FIXME: the following two commands each throw error
//"ADC: adc1_lock_release(419): adc1 lock release called before acquire"
//note: also happens for each get_raw for first call of readAdc function
//when run in main function that does not happen -> move init from constructor to be called in main
adc1_config_channel_atten(config.adc_x, ADC_ATTEN_DB_11); //max voltage
adc1_config_channel_atten(config.adc_y, ADC_ATTEN_DB_11); //max voltage
//define joystick center from current position
defineCenter(); //define joystick center from current position
}
//-----------------------------
//--------- readAdc -----------
//-----------------------------
//function for multisampling an anlog input
int evaluatedJoystick::readAdc(adc1_channel_t adc_channel, bool inverted) {
//make multiple measurements
int adc_reading = 0;
for (int i = 0; i < 16; i++) {
adc_reading += adc1_get_raw(adc_channel);
ets_delay_us(50);
}
adc_reading = adc_reading / 16;
//return original or inverted result
if (inverted) {
return 4095 - adc_reading;
} else {
return adc_reading;
}
}
//-------------------------------
//---------- getData ------------
//-------------------------------
//function that reads the joystick, calculates values and returns a struct with current data
joystickData_t evaluatedJoystick::getData() {
//get coordinates
//TODO individual tolerances for each axis? Otherwise some parameters can be removed
//TODO duplicate code for each axis below:
ESP_LOGV(TAG, "getting X coodrdinate...");
uint32_t adcRead;
adcRead = readAdc(config.adc_x, config.x_inverted);
float x = scaleCoordinate(readAdc(config.adc_x, config.x_inverted), config.x_min, config.x_max, x_center, config.tolerance_zeroX_per, config.tolerance_end_per);
data.x = x;
ESP_LOGD(TAG, "X: adc-raw=%d \tadc-conv=%d \tmin=%d \t max=%d \tcenter=%d \tinverted=%d => x=%.3f",
adc1_get_raw(config.adc_x), adcRead, config.x_min, config.x_max, x_center, config.x_inverted, x);
ESP_LOGV(TAG, "getting Y coodrinate...");
adcRead = readAdc(config.adc_y, config.y_inverted);
float y = scaleCoordinate(adcRead, config.y_min, config.y_max, y_center, config.tolerance_zeroY_per, config.tolerance_end_per);
data.y = y;
ESP_LOGD(TAG, "Y: adc-raw=%d \tadc-conv=%d \tmin=%d \t max=%d \tcenter=%d \tinverted=%d => y=%.3lf",
adc1_get_raw(config.adc_y), adcRead, config.y_min, config.y_max, y_center, config.y_inverted, y);
//calculate radius
data.radius = sqrt(pow(data.x,2) + pow(data.y,2));
if (data.radius > 1-config.tolerance_radius) {
data.radius = 1;
}
//calculate angle
data.angle = (atan(data.y/data.x) * 180) / 3.141;
//define position
data.position = joystick_evaluatePosition(x, y);
ESP_LOGD(TAG, "X=%.2f Y=%.2f radius=%.2f angle=%.2f", data.x, data.y, data.radius, data.angle);
return data;
}
//----------------------------
//------ defineCenter --------
//----------------------------
//function that defines the current position of the joystick as center position
void evaluatedJoystick::defineCenter(){
//read voltage from adc
x_center = readAdc(config.adc_x, config.x_inverted);
y_center = readAdc(config.adc_y, config.y_inverted);
ESP_LOGW(TAG, "defined center to x=%d, y=%d", x_center, y_center);
}
//==============================
//====== scaleCoordinate =======
//==============================
//function that scales an input value (e.g. from adc pin) to a value from -1 to 1 using the given thresholds and tolerances
float scaleCoordinate(float input, float min, float max, float center, float tolerance_zero_per, float tolerance_end_per) {
float coordinate = 0;
//convert tolerance percentages to actual values of range
double tolerance_zero = (max-min) * tolerance_zero_per / 100;
double tolerance_end = (max-min) * tolerance_end_per / 100;
//define coordinate value considering the different tolerances
//--- center ---
if ((input < center+tolerance_zero) && (input > center-tolerance_zero) ) { //adc value is inside tolerance around center threshold
coordinate = 0;
}
//--- maximum ---
else if (input > max-tolerance_end) {
coordinate = 1;
}
//--- minimum ---
else if (input < min+tolerance_end) {
coordinate = -1;
}
//--- positive area ---
else if (input > center) {
float range = max - center - tolerance_zero - tolerance_end;
coordinate = (input - center - tolerance_zero) / range;
}
//--- negative area ---
else if (input < center) {
float range = (center - min - tolerance_zero - tolerance_end);
coordinate = -(center-input - tolerance_zero) / range;
}
ESP_LOGD(TAG, "scaling: in=%.3f coordinate=%.3f, tolZero=%.3f, tolEnd=%.3f", input, coordinate, tolerance_zero, tolerance_end);
//return coordinate (-1 to 1)
return coordinate;
}
//===========================================
//====== joystick_scaleCoordinatesExp =======
//===========================================
//local function that scales the absolute value of a variable exponentionally
float scaleExp(float value, float exponent){
float result = powf(fabs(value), exponent);
if (value >= 0) {
return result;
} else {
return -result;
}
}
//function that updates a joystickData object with exponentionally scaling applied to coordinates
void joystick_scaleCoordinatesExp(joystickData_t * data, float exponent){
//scale x and y coordinate
data->x = scaleExp(data->x, exponent);
data->y = scaleExp(data->y, exponent);
//re-calculate radius
data->radius = sqrt(pow(data->x,2) + pow(data->y,2));
if (data->radius > 1-0.07) {//FIXME hardcoded radius tolerance
data->radius = 1;
}
}
//==============================================
//====== joystick_scaleCoordinatesLinear =======
//==============================================
//local function that scales value from -1-1 to -1-1 with two different slopes before and after a specified point
//slope1: for value from 0 to pointX -> scale linear from 0 to pointY
//slope2: for value from pointX to 1 -> scale linear from pointY to 1
float scaleLinPoint(float value, float pointX, float pointY){
float result;
if (fabs(value) <= pointX) {
//--- scale on line from 0 to point ---
result = fabs(value) * (pointY/pointX);
} else {
//--- scale on line from point to 1 ---
float m = (1-pointY) / (1-pointX);
result = fabs(value) * m + (1 - m);
}
//--- return result with same sign as input ---
if (value >= 0) {
return result;
} else {
return -result;
}
}
//function that updates a joystickData object with linear scaling applied to coordinates
//e.g. use to use more joystick resolution for lower speeds
//TODO rename this function to more general name (scales not only coordinates e.g. adjusts radius, in future angle...)
void joystick_scaleCoordinatesLinear(joystickData_t * data, float pointX, float pointY){
// --- scale x and y coordinate --- DISABLED
/*
data->x = scaleLinPoint(data->x, pointX, pointY);
data->y = scaleLinPoint(data->y, pointX, pointY);
//re-calculate radius
data->radius = sqrt(pow(data->x,2) + pow(data->y,2));
if (data->radius > 1-0.1) {//FIXME hardcoded radius tolerance
data->radius = 1;
}
*/
//note: issue with scaling X, Y coordinates:
// - messed up radius calculation - radius never gets 1 at diagonal positions
//==> only scaling radius as only speed should be more acurate at low radius:
//TODO make that clear and rename function, since it does not scale coordinates - just radius
//--- scale radius ---
data-> radius = scaleLinPoint(data->radius, pointX, pointY);
}
//=============================================
//========= joystick_evaluatePosition =========
//=============================================
//function that defines and returns enum joystickPos from x and y coordinates
joystickPos_t joystick_evaluatePosition(float x, float y){
//define position
//--- center ---
if((fabs(x) == 0) && (fabs(y) == 0)){
return joystickPos_t::CENTER;
}
//--- x axis ---
else if(fabs(y) == 0){
return joystickPos_t::X_AXIS;
}
//--- y axis ---
else if(fabs(x) == 0){
return joystickPos_t::Y_AXIS;
}
//--- top right ---
else if(x > 0 && y > 0){
return joystickPos_t::TOP_RIGHT;
}
//--- top left ---
else if(x < 0 && y > 0){
return joystickPos_t::TOP_LEFT;
}
//--- bottom left ---
else if(x < 0 && y < 0){
return joystickPos_t::BOTTOM_LEFT;
}
//--- bottom right ---
else if(x > 0 && y < 0){
return joystickPos_t::BOTTOM_RIGHT;
}
//--- other ---
else {
return joystickPos_t::CENTER;
}
}
//============================================
//========= joystick_CommandsDriving =========
//============================================
//function that generates commands for both motors from the joystick data
motorCommands_t joystick_generateCommandsDriving(joystickData_t data, bool altStickMapping){
//struct with current data of the joystick
//typedef struct joystickData_t {
// joystickPos_t position;
// float x;
// float y;
// float radius;
// float angle;
//} joystickData_t;
//--- variables ---
motorCommands_t commands;
float dutyMax = 90; //TODO add this to config, make changeable during runtime
float dutyOffset = 5; //immediately starts with this duty, TODO add this to config
float dutyRange = dutyMax - dutyOffset;
float ratio = fabs(data.angle) / 90; //90degree = x=0 || 0degree = y=0
//--- snap ratio to max at angle threshold ---
//(-> more joystick area where inner wheel is off when turning)
/*
//FIXME works, but armchair unsusable because of current bug with motor driver (inner motor freezes after turn)
float ratioClipThreshold = 0.3;
if (ratio < ratioClipThreshold) ratio = 0;
else if (ratio > 1-ratioClipThreshold) ratio = 1;
//TODO subtract this clip threshold from available joystick range at ratio usage
*/
//--- experimental alternative control mode ---
if (altStickMapping == true){
//swap BOTTOM_LEFT and BOTTOM_RIGHT
if (data.position == joystickPos_t::BOTTOM_LEFT){
data.position = joystickPos_t::BOTTOM_RIGHT;
}
else if (data.position == joystickPos_t::BOTTOM_RIGHT){
data.position = joystickPos_t::BOTTOM_LEFT;
}
}
//--- handle all positions ---
//define target direction and duty according to position
switch (data.position){
case joystickPos_t::CENTER:
commands.left.state = motorstate_t::IDLE;
commands.right.state = motorstate_t::IDLE;
commands.left.duty = 0;
commands.right.duty = 0;
break;
case joystickPos_t::Y_AXIS:
if (data.y > 0){
commands.left.state = motorstate_t::FWD;
commands.right.state = motorstate_t::FWD;
} else {
commands.left.state = motorstate_t::REV;
commands.right.state = motorstate_t::REV;
}
commands.left.duty = fabs(data.y) * dutyRange + dutyOffset;
commands.right.duty = commands.left.duty;
break;
case joystickPos_t::X_AXIS:
if (data.x > 0) {
commands.left.state = motorstate_t::FWD;
commands.right.state = motorstate_t::REV;
} else {
commands.left.state = motorstate_t::REV;
commands.right.state = motorstate_t::FWD;
}
commands.left.duty = fabs(data.x) * dutyRange + dutyOffset;
commands.right.duty = commands.left.duty;
break;
case joystickPos_t::TOP_RIGHT:
commands.left.state = motorstate_t::FWD;
commands.right.state = motorstate_t::FWD;
commands.left.duty = data.radius * dutyRange + dutyOffset;
commands.right.duty = data.radius * dutyRange - (data.radius*dutyRange + dutyOffset)*(1-ratio) + dutyOffset;
break;
case joystickPos_t::TOP_LEFT:
commands.left.state = motorstate_t::FWD;
commands.right.state = motorstate_t::FWD;
commands.left.duty = data.radius * dutyRange - (data.radius*dutyRange + dutyOffset)*(1-ratio) + dutyOffset;
commands.right.duty = data.radius * dutyRange + dutyOffset;
break;
case joystickPos_t::BOTTOM_LEFT:
commands.left.state = motorstate_t::REV;
commands.right.state = motorstate_t::REV;
commands.left.duty = data.radius * dutyRange + dutyOffset;
commands.right.duty = data.radius * dutyRange - (data.radius*dutyRange + dutyOffset)*(1-ratio) + dutyOffset;
break;
case joystickPos_t::BOTTOM_RIGHT:
commands.left.state = motorstate_t::REV;
commands.right.state = motorstate_t::REV;
commands.left.duty = data.radius * dutyRange - (data.radius*dutyRange + dutyOffset)*(1-ratio) + dutyOffset;
commands.right.duty = data.radius * dutyRange + dutyOffset;
break;
}
ESP_LOGI(TAG_CMD, "generated commands from data: state=%s, angle=%.3f, ratio=%.3f/%.3f, radius=%.2f, x=%.2f, y=%.2f",
joystickPosStr[(int)data.position], data.angle, ratio, (1-ratio), data.radius, data.x, data.y);
ESP_LOGI(TAG_CMD, "motor left: state=%s, duty=%.3f", motorstateStr[(int)commands.left.state], commands.left.duty);
ESP_LOGI(TAG_CMD, "motor right: state=%s, duty=%.3f", motorstateStr[(int)commands.right.state], commands.right.duty);
return commands;
}
//============================================
//========= joystick_CommandsShaking =========
//============================================
//--- variable declarations ---
uint32_t shake_timestamp_turnedOn = 0;
uint32_t shake_timestamp_turnedOff = 0;
bool shake_state = false;
joystickPos_t lastStickPos = joystickPos_t::CENTER;
//stick position quadrant only with "X_AXIS and Y_AXIS" as hysteresis
joystickPos_t stickQuadrant = joystickPos_t::CENTER;
//--- configure shake mode --- TODO: move this to config
uint32_t shake_msOffMax = 80;
uint32_t shake_msOnMax = 120;
float dutyShake = 60;
//function that generates commands for both motors from the joystick data
motorCommands_t joystick_generateCommandsShaking(joystickData_t data){
//--- handle pulsing shake variable ---
//TODO remove this, make individual per mode?
//TODO only run this when not CENTER anyways?
motorCommands_t commands;
float ratio = fabs(data.angle) / 90; //90degree = x=0 || 0degree = y=0
//calculate on/off duration
uint32_t msOn = shake_msOnMax * data.radius;
uint32_t msOff = shake_msOffMax * data.radius;
//evaluate state (on/off)
if (data.radius > 0 ){
//currently off
if (shake_state == false){
//off long enough
if (esp_log_timestamp() - shake_timestamp_turnedOff > msOff) {
//turn on
shake_state = true;
shake_timestamp_turnedOn = esp_log_timestamp();
}
}
//currently on
else {
//on long enough
if (esp_log_timestamp() - shake_timestamp_turnedOn > msOn) {
//turn off
shake_state = false;
shake_timestamp_turnedOff = esp_log_timestamp();
}
}
}
//joystick is at center
else {
shake_state = false;
shake_timestamp_turnedOff = esp_log_timestamp();
}
//struct with current data of the joystick
//typedef struct joystickData_t {
// joystickPos_t position;
// float x;
// float y;
// float radius;
// float angle;
//} joystickData_t;
//--- evaluate stick position ---
//4 quadrants and center only - with X and Y axis as hysteresis
switch (data.position){
case joystickPos_t::CENTER:
//immediately set to center at center
stickQuadrant = joystickPos_t::CENTER;
break;
case joystickPos_t::Y_AXIS:
//when moving from center to axis initially start in a certain quadrant
if (stickQuadrant == joystickPos_t::CENTER) {
if (data.y > 0){
stickQuadrant = joystickPos_t::TOP_RIGHT;
} else {
stickQuadrant = joystickPos_t::BOTTOM_RIGHT;
}
}
break;
case joystickPos_t::X_AXIS:
//when moving from center to axis initially start in a certain quadrant
if (stickQuadrant == joystickPos_t::CENTER) {
if (data.x > 0){
stickQuadrant = joystickPos_t::TOP_RIGHT;
} else {
stickQuadrant = joystickPos_t::TOP_LEFT;
}
}
break;
case joystickPos_t::TOP_RIGHT:
case joystickPos_t::TOP_LEFT:
case joystickPos_t::BOTTOM_LEFT:
case joystickPos_t::BOTTOM_RIGHT:
//update/change evaluated pos when in one of the 4 quadrants
stickQuadrant = data.position;
//TODO: maybe beep when switching mode? (difficult because beep object has to be passed to function)
break;
}
//--- handle different modes (joystick in any of 4 quadrants) ---
switch (stickQuadrant){
case joystickPos_t::CENTER:
case joystickPos_t::X_AXIS: //never true
case joystickPos_t::Y_AXIS: //never true
commands.left.state = motorstate_t::IDLE;
commands.right.state = motorstate_t::IDLE;
commands.left.duty = 0;
commands.right.duty = 0;
ESP_LOGI(TAG_CMD, "generate shake commands: CENTER -> idle");
return commands;
break;
//4 different modes
case joystickPos_t::TOP_RIGHT:
commands.left.state = motorstate_t::FWD;
commands.right.state = motorstate_t::FWD;
break;
case joystickPos_t::TOP_LEFT:
commands.left.state = motorstate_t::REV;
commands.right.state = motorstate_t::REV;
break;
case joystickPos_t::BOTTOM_LEFT:
commands.left.state = motorstate_t::REV;
commands.right.state = motorstate_t::FWD;
break;
case joystickPos_t::BOTTOM_RIGHT:
commands.left.state = motorstate_t::FWD;
commands.right.state = motorstate_t::REV;
break;
}
//--- turn motors on/off depending on pulsing shake variable ---
if (shake_state == true){
//set duty to shake
commands.left.duty = dutyShake;
commands.right.duty = dutyShake;
//directions are defined above depending on mode
} else {
commands.left.state = motorstate_t::IDLE;
commands.right.state = motorstate_t::IDLE;
commands.left.duty = 0;
commands.right.duty = 0;
}
ESP_LOGI(TAG_CMD, "generated commands from data: state=%s, angle=%.3f, ratio=%.3f/%.3f, radius=%.2f, x=%.2f, y=%.2f",
joystickPosStr[(int)data.position], data.angle, ratio, (1-ratio), data.radius, data.x, data.y);
ESP_LOGI(TAG_CMD, "motor left: state=%s, duty=%.3f", motorstateStr[(int)commands.left.state], commands.left.duty);
ESP_LOGI(TAG_CMD, "motor right: state=%s, duty=%.3f", motorstateStr[(int)commands.right.state], commands.right.duty);
return commands;
}

View File

@ -0,0 +1,153 @@
#pragma once
extern "C"
{
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/adc.h"
#include "esp_log.h"
#include "esp_err.h"
}
#include <cmath>
#include "motorctl.hpp" //for declaration of motorCommands_t struct
//======================================
//========= evaluated Joystick =========
//======================================
//class which evaluates a joystick with 2 analog signals
// - scales the adc input to coordinates with detailed tolerances
// - calculates angle and radius
// - defines an enum with position information
//--------------------------------------------
//---- struct, enum, variable declarations ---
//--------------------------------------------
//struct with all required configuration parameters
typedef struct joystick_config_t {
//analog inputs the axis are connected
adc1_channel_t adc_x;
adc1_channel_t adc_y;
//percentage of joystick range the coordinate of the axis snaps to 0 (0-100)
int tolerance_zeroX_per;
int tolerance_zeroY_per;
//percentage of joystick range the coordinate snaps to -1 or 1 before configured "_max" or "_min" threshold (mechanical end) is reached (0-100)
int tolerance_end_per;
//threshold the radius jumps to 1 before the stick is at max radius (range 0-1)
float tolerance_radius;
//min and max adc values of each axis
int x_min;
int x_max;
int y_min;
int y_max;
//invert adc measurement (e.g. when moving joystick up results in a decreasing voltage)
bool x_inverted;
bool y_inverted;
} joystick_config_t;
//enum for describing the position of the joystick
enum class joystickPos_t {CENTER, Y_AXIS, X_AXIS, TOP_RIGHT, TOP_LEFT, BOTTOM_LEFT, BOTTOM_RIGHT};
extern const char* joystickPosStr[7];
//struct with current data of the joystick
typedef struct joystickData_t {
joystickPos_t position;
float x;
float y;
float radius;
float angle;
} joystickData_t;
//------------------------------------
//----- evaluatedJoystick class -----
//------------------------------------
class evaluatedJoystick {
public:
//--- constructor ---
evaluatedJoystick(joystick_config_t config_f);
//--- functions ---
joystickData_t getData(); //read joystick, calculate values and return the data in a struct
void defineCenter(); //define joystick center from current position
private:
//--- functions ---
//initialize adc inputs, define center
void init();
//read adc while making multiple samples with option to invert the result
int readAdc(adc1_channel_t adc_channel, bool inverted = false);
//--- variables ---
joystick_config_t config;
int x_center;
int y_center;
joystickData_t data;
float x;
float y;
};
//============================================
//========= joystick_CommandsDriving =========
//============================================
//function that generates commands for both motors from the joystick data
//motorCommands_t joystick_generateCommandsDriving(evaluatedJoystick joystick);
motorCommands_t joystick_generateCommandsDriving(joystickData_t data, bool altStickMapping = false);
//============================================
//========= joystick_CommandsShaking =========
//============================================
//function that generates commands for both motors from the joystick data
//motorCommands_t joystick_generateCommandsDriving(evaluatedJoystick joystick);
motorCommands_t joystick_generateCommandsShaking(joystickData_t data );
//==============================
//====== scaleCoordinate =======
//==============================
//function that scales an input value (e.g. from adc pin) to a value from -1 to 1 using the giben thresholds and tolerances
float scaleCoordinate(float input, float min, float max, float center, float tolerance_zero_per, float tolerance_end_per);
//===========================================
//====== joystick_scaleCoordinatesExp =======
//===========================================
//function that updates a joystickData object with exponentionally scaling applied to coordinates
//e.g. use to use more joystick resolution for lower speeds
void joystick_scaleCoordinatesExp(joystickData_t * data, float exponent);
//==============================================
//====== joystick_scaleCoordinatesLinear =======
//==============================================
//function that updates a joystickData object with linear scaling applied to coordinates
//scales coordinates with two different slopes before and after a specified point
//slope1: for value from 0 to pointX -> scale linear from 0 to pointY
//slope2: for value from pointX to 1 -> scale linear from pointY to 1
//=> best to draw the lines and point in a graph
//e.g. use to use more joystick resolution for lower speeds
void joystick_scaleCoordinatesLinear(joystickData_t * data, float pointX, float pointY);
//=============================================
//========= joystick_evaluatePosition =========
//=============================================
//function that defines and returns enum joystickPos from x and y coordinates
joystickPos_t joystick_evaluatePosition(float x, float y);

View File

@ -0,0 +1,330 @@
#include "motorctl.hpp"
//tag for logging
static const char * TAG = "motor-control";
//=============================
//======== constructor ========
//=============================
//constructor, simultaniously initialize instance of motor driver 'motor' and current sensor 'cSensor' with provided config (see below lines after ':')
controlledMotor::controlledMotor(single100a_config_t config_driver, motorctl_config_t config_control):
motor(config_driver),
cSensor(config_control.currentSensor_adc, config_control.currentSensor_ratedCurrent) {
//copy parameters for controlling the motor
config = config_control;
//copy configured default fading durations to actually used variables
msFadeAccel = config.msFadeAccel;
msFadeDecel = config.msFadeDecel;
init();
//TODO: add currentsensor object here
//currentSensor cSensor(config.currentSensor_adc, config.currentSensor_ratedCurrent);
}
//============================
//========== init ============
//============================
void controlledMotor::init(){
commandQueue = xQueueCreate( 1, sizeof( struct motorCommand_t ) );
//cSensor.calibrateZeroAmpere(); //currently done in currentsensor constructor TODO do this regularly e.g. in idle?
}
//----------------
//----- fade -----
//----------------
//local function that fades a variable
//- increments a variable (pointer) by given value
//- sets to target if already closer than increment
//TODO this needs testing
void fade(float * dutyNow, float dutyTarget, float dutyIncrement){
float dutyDelta = dutyTarget - *dutyNow;
if ( fabs(dutyDelta) > fabs(dutyIncrement) ) { //check if already close to target
*dutyNow = *dutyNow + dutyIncrement;
}
//already closer to target than increment
else {
*dutyNow = dutyTarget;
}
}
//----------------------------
//----- getStateFromDuty -----
//----------------------------
//local function that determines motor the direction from duty range -100 to 100
motorstate_t getStateFromDuty(float duty){
if(duty > 0) return motorstate_t::FWD;
if (duty < 0) return motorstate_t::REV;
return motorstate_t::IDLE;
}
//==============================
//=========== handle ===========
//==============================
//function that controls the motor driver and handles fading/ramp, current limit and deadtime
void controlledMotor::handle(){
//TODO: History: skip fading when motor was running fast recently / alternatively add rot-speed sensor
//--- receive commands from queue ---
if( xQueueReceive( commandQueue, &commandReceive, ( TickType_t ) 0 ) )
{
ESP_LOGD(TAG, "Read command from queue: state=%s, duty=%.2f", motorstateStr[(int)commandReceive.state], commandReceive.duty);
state = commandReceive.state;
dutyTarget = commandReceive.duty;
//--- convert duty ---
//define target duty (-100 to 100) from provided duty and motorstate
//this value is more suitable for the fading algorithm
switch(commandReceive.state){
case motorstate_t::BRAKE:
//update state
state = motorstate_t::BRAKE;
dutyTarget = 0;
break;
case motorstate_t::IDLE:
dutyTarget = 0;
break;
case motorstate_t::FWD:
dutyTarget = fabs(commandReceive.duty);
break;
case motorstate_t::REV:
dutyTarget = - fabs(commandReceive.duty);
break;
}
}
//--- calculate increment ---
//calculate increment for fading UP with passed time since last run and configured fade time
int64_t usPassed = esp_timer_get_time() - timestampLastRunUs;
if (msFadeAccel > 0){
dutyIncrementAccel = ( usPassed / ((float)msFadeAccel * 1000) ) * 100; //TODO define maximum increment - first run after startup (or long) pause can cause a very large increment
} else {
dutyIncrementAccel = 100;
}
//calculate increment for fading DOWN with passed time since last run and configured fade time
if (msFadeDecel > 0){
dutyIncrementDecel = ( usPassed / ((float)msFadeDecel * 1000) ) * 100;
} else {
dutyIncrementDecel = 100;
}
//--- BRAKE ---
//brake immediately, update state, duty and exit this cycle of handle function
if (state == motorstate_t::BRAKE){
motor.set(motorstate_t::BRAKE, 0);
dutyNow = 0;
return; //no need to run the fade algorithm
}
//--- calculate difference ---
dutyDelta = dutyTarget - dutyNow;
//positive: need to increase by that value
//negative: need to decrease
//----- FADING -----
//fade duty to target (up and down)
//TODO: this needs optimization (can be more clear and/or simpler)
if (dutyDelta > 0) { //difference positive -> increasing duty (-100 -> 100)
if (dutyNow < 0) { //reverse, decelerating
fade(&dutyNow, dutyTarget, dutyIncrementDecel);
}
else if (dutyNow >= 0) { //forward, accelerating
fade(&dutyNow, dutyTarget, dutyIncrementAccel);
}
}
else if (dutyDelta < 0) { //difference negative -> decreasing duty (100 -> -100)
if (dutyNow <= 0) { //reverse, accelerating
fade(&dutyNow, dutyTarget, - dutyIncrementAccel);
}
else if (dutyNow > 0) { //forward, decelerating
fade(&dutyNow, dutyTarget, - dutyIncrementDecel);
}
}
//----- CURRENT LIMIT -----
if ((config.currentLimitEnabled) && (dutyDelta != 0)){
currentNow = cSensor.read();
if (fabs(currentNow) > config.currentMax){
float dutyOld = dutyNow;
//adaptive decrement:
//Note current exceeded twice -> twice as much decrement: TODO: decrement calc needs finetuning, currently random values
dutyIncrementDecel = (currentNow/config.currentMax) * ( usPassed / ((float)msFadeDecel * 1500) ) * 100;
float currentLimitDecrement = ( (float)usPassed / ((float)1000 * 1000) ) * 100; //1000ms from 100 to 0
if (dutyNow < -currentLimitDecrement) {
dutyNow += currentLimitDecrement;
} else if (dutyNow > currentLimitDecrement) {
dutyNow -= currentLimitDecrement;
}
ESP_LOGW(TAG, "current limit exceeded! now=%.3fA max=%.1fA => decreased duty from %.3f to %.3f", currentNow, config.currentMax, dutyOld, dutyNow);
}
}
//--- define new motorstate --- (-100 to 100 => direction)
state=getStateFromDuty(dutyNow);
//--- DEAD TIME ----
//ensure minimum idle time between direction change to prevent driver overload
//FWD -> IDLE -> FWD continue without waiting
//FWD -> IDLE -> REV wait for dead-time in IDLE
//TODO check when changed only?
if ( //not enough time between last direction state
( state == motorstate_t::FWD && (esp_log_timestamp() - timestampsModeLastActive[(int)motorstate_t::REV] < config.deadTimeMs))
|| (state == motorstate_t::REV && (esp_log_timestamp() - timestampsModeLastActive[(int)motorstate_t::FWD] < config.deadTimeMs))
){
ESP_LOGD(TAG, "waiting dead-time... dir change %s -> %s", motorstateStr[(int)statePrev], motorstateStr[(int)state]);
if (!deadTimeWaiting){ //log start
deadTimeWaiting = true;
ESP_LOGW(TAG, "starting dead-time... %s -> %s", motorstateStr[(int)statePrev], motorstateStr[(int)state]);
}
//force IDLE state during wait
state = motorstate_t::IDLE;
dutyNow = 0;
} else {
if (deadTimeWaiting){ //log end
deadTimeWaiting = false;
ESP_LOGW(TAG, "dead-time ended - continue with %s", motorstateStr[(int)state]);
}
ESP_LOGV(TAG, "deadtime: no change below deadtime detected... dir=%s, duty=%.1f", motorstateStr[(int)state], dutyNow);
}
//--- save current actual motorstate and timestamp ---
//needed for deadtime
timestampsModeLastActive[(int)getStateFromDuty(dutyNow)] = esp_log_timestamp();
//(-100 to 100 => direction)
statePrev = getStateFromDuty(dutyNow);
//--- apply new target to motor ---
motor.set(state, fabs(dutyNow));
//note: BRAKE state is handled earlier
//--- update timestamp ---
timestampLastRunUs = esp_timer_get_time(); //update timestamp last run with current timestamp in microseconds
}
//===============================
//========== setTarget ==========
//===============================
//function to set the target mode and duty of a motor
//puts the provided command in a queue for the handle function running in another task
void controlledMotor::setTarget(motorstate_t state_f, float duty_f){
commandSend = {
.state = state_f,
.duty = duty_f
};
ESP_LOGD(TAG, "Inserted command to queue: state=%s, duty=%.2f", motorstateStr[(int)commandSend.state], commandSend.duty);
//send command to queue (overwrite if an old command is still in the queue and not processed)
xQueueOverwrite( commandQueue, ( void * )&commandSend);
//xQueueSend( commandQueue, ( void * )&commandSend, ( TickType_t ) 0 );
}
//===============================
//========== getStatus ==========
//===============================
//function which returns the current status of the motor in a motorCommand_t struct
motorCommand_t controlledMotor::getStatus(){
motorCommand_t status = {
.state = state,
.duty = dutyNow
};
//TODO: mutex
return status;
};
//===============================
//=========== setFade ===========
//===============================
//function for editing or enabling the fading/ramp of the motor control
//set/update fading duration/amount
void controlledMotor::setFade(fadeType_t fadeType, uint32_t msFadeNew){
//TODO: mutex for msFade variable also used in handle function
switch(fadeType){
case fadeType_t::ACCEL:
msFadeAccel = msFadeNew;
break;
case fadeType_t::DECEL:
msFadeDecel = msFadeNew;
break;
}
}
//enable (set to default value) or disable fading
void controlledMotor::setFade(fadeType_t fadeType, bool enabled){
uint32_t msFadeNew = 0; //define new fade time as default disabled
if(enabled){ //enable
//set to default/configured value
switch(fadeType){
case fadeType_t::ACCEL:
msFadeNew = config.msFadeAccel;
break;
case fadeType_t::DECEL:
msFadeNew = config.msFadeDecel;
break;
}
}
//apply new Fade value
setFade(fadeType, msFadeNew);
}
//==================================
//=========== toggleFade ===========
//==================================
//toggle fading between OFF and default value
bool controlledMotor::toggleFade(fadeType_t fadeType){
uint32_t msFadeNew = 0;
bool enabled = false;
switch(fadeType){
case fadeType_t::ACCEL:
if (msFadeAccel == 0){
msFadeNew = config.msFadeAccel;
enabled = true;
} else {
msFadeNew = 0;
}
break;
case fadeType_t::DECEL:
if (msFadeDecel == 0){
msFadeNew = config.msFadeAccel;
enabled = true;
} else {
msFadeNew = 0;
}
break;
}
//apply new Fade value
setFade(fadeType, msFadeNew);
//return new state (fading enabled/disabled)
return enabled;
}

View File

@ -0,0 +1,104 @@
#pragma once
extern "C"
{
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_timer.h"
}
#include "motordrivers.hpp"
#include "currentsensor.hpp"
//=======================================
//====== struct/type declarations ======
//=======================================
//struct for sending command for one motor in the queue
struct motorCommand_t {
motorstate_t state;
float duty;
};
//struct containing commands for two motors
typedef struct motorCommands_t {
motorCommand_t left;
motorCommand_t right;
} motorCommands_t;
//struct with all config parameters for a motor regarding ramp and current limit
typedef struct motorctl_config_t {
uint32_t msFadeAccel; //acceleration of the motor (ms it takes from 0% to 100%)
uint32_t msFadeDecel; //deceleration of the motor (ms it takes from 100% to 0%)
bool currentLimitEnabled;
adc1_channel_t currentSensor_adc;
float currentSensor_ratedCurrent;
float currentMax;
uint32_t deadTimeMs; //time motor stays in IDLE before direction change
} motorctl_config_t;
//enum fade type (acceleration, deceleration)
//e.g. used for specifying which fading should be modified with setFade, togleFade functions
enum class fadeType_t {ACCEL, DECEL};
//===================================
//====== controlledMotor class ======
//===================================
class controlledMotor {
public:
//--- functions ---
controlledMotor(single100a_config_t config_driver, motorctl_config_t config_control); //constructor with structs for configuring motordriver and parameters for control TODO: add configuration for currentsensor
void handle(); //controls motor duty with fade and current limiting feature (has to be run frequently by another task)
void setTarget(motorstate_t state_f, float duty_f = 0); //adds target command to queue for handle function
motorCommand_t getStatus(); //get current status of the motor (returns struct with state and duty)
void setFade(fadeType_t fadeType, bool enabled); //enable/disable acceleration or deceleration fading
void setFade(fadeType_t fadeType, uint32_t msFadeNew); //set acceleration or deceleration fade time
bool toggleFade(fadeType_t fadeType); //toggle acceleration or deceleration on/off
//TODO set current limit method
private:
//--- functions ---
void init(); //creates currentsensor objects, motordriver objects and queue
//--- objects ---
//motor driver
single100a motor;
//queue for sending commands to the separate task running the handle() function very fast
QueueHandle_t commandQueue = NULL;
//current sensor
currentSensor cSensor;
//--- variables ---
//struct for storing control specific parameters
motorctl_config_t config;
motorstate_t state = motorstate_t::IDLE;
float currentMax;
float currentNow;
float dutyTarget;
float dutyNow;
float dutyIncrementAccel;
float dutyIncrementDecel;
float dutyDelta;
uint32_t msFadeAccel;
uint32_t msFadeDecel;
uint32_t ramp;
int64_t timestampLastRunUs;
bool deadTimeWaiting = false;
uint32_t timestampsModeLastActive[4] = {};
motorstate_t statePrev = motorstate_t::FWD;
struct motorCommand_t commandReceive = {};
struct motorCommand_t commandSend = {};
};

View File

@ -0,0 +1,129 @@
#include "motordrivers.hpp"
//TODO: move from ledc to mcpwm?
//https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32/api-reference/peripherals/mcpwm.html#
//https://github.com/espressif/esp-idf/tree/v4.3/examples/peripherals/mcpwm/mcpwm_basic_config
//Note fade functionality provided by LEDC would be very useful but unfortunately is not usable here because:
//"Due to hardware limitations, there is no way to stop a fade before it reaches its target duty."
//definition of string array to be able to convert state enum to readable string
const char* motorstateStr[4] = {"IDLE", "FWD", "REV", "BRAKE"};
//tag for logging
static const char * TAG = "motordriver";
//====================================
//===== single100a motor driver ======
//====================================
//-----------------------------
//-------- constructor --------
//-----------------------------
//copy provided struct with all configuration and run init function
single100a::single100a(single100a_config_t config_f){
config = config_f;
init();
}
//----------------------------
//---------- init ------------
//----------------------------
//function to initialize pwm output, gpio pins and calculate maxDuty
void single100a::init(){
//--- configure ledc timer ---
ledc_timer_config_t ledc_timer;
ledc_timer.speed_mode = LEDC_HIGH_SPEED_MODE;
ledc_timer.timer_num = config.ledc_timer;
ledc_timer.duty_resolution = config.resolution; //13bit gives max 5khz freq
ledc_timer.freq_hz = config.pwmFreq;
ledc_timer.clk_cfg = LEDC_AUTO_CLK;
//apply configuration
ledc_timer_config(&ledc_timer);
//--- configure ledc channel ---
ledc_channel_config_t ledc_channel;
ledc_channel.channel = config.ledc_channel;
ledc_channel.duty = 0;
ledc_channel.gpio_num = config.gpio_pwm;
ledc_channel.speed_mode = LEDC_HIGH_SPEED_MODE;
ledc_channel.hpoint = 0;
ledc_channel.timer_sel = config.ledc_timer;
ledc_channel.intr_type = LEDC_INTR_DISABLE;
ledc_channel.flags.output_invert = 0; //TODO: add config option to invert the pwm output?
//apply configuration
ledc_channel_config(&ledc_channel);
//--- define gpio pins as outputs ---
gpio_pad_select_gpio(config.gpio_a);
gpio_set_direction(config.gpio_a, GPIO_MODE_OUTPUT);
gpio_pad_select_gpio(config.gpio_b);
gpio_set_direction(config.gpio_b, GPIO_MODE_OUTPUT);
//--- calculate max duty according to selected resolution ---
dutyMax = pow(2, ledc_timer.duty_resolution) -1;
ESP_LOGI(TAG, "initialized single100a driver");
ESP_LOGI(TAG, "resolution=%dbit, dutyMax value=%d, resolution=%.4f %%", ledc_timer.duty_resolution, dutyMax, 100/(float)dutyMax);
}
//---------------------------
//----------- set -----------
//---------------------------
//function to put the h-bridge module in the desired state and duty cycle
void single100a::set(motorstate_t state_f, float duty_f){
//scale provided target duty in percent to available resolution for ledc
uint32_t dutyScaled;
if (duty_f > 100) { //target duty above 100%
dutyScaled = dutyMax;
} else if (duty_f <= 0) { //target at or below 0%
state_f = motorstate_t::IDLE;
dutyScaled = 0;
} else { //target duty 0-100%
//scale duty to available resolution
dutyScaled = duty_f / 100 * dutyMax;
}
//put the single100a h-bridge module in the desired state update duty-cycle
switch (state_f){
case motorstate_t::IDLE:
ledc_set_duty(LEDC_HIGH_SPEED_MODE, config.ledc_channel, dutyScaled);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, config.ledc_channel);
//TODO: to fix bugged state of h-bridge module when idle and start again, maybe try to leave pwm signal on for some time before updating a/b pins?
//no brake: (freewheel)
//gpio_set_level(config.gpio_a, config.aEnabledPinState);
//gpio_set_level(config.gpio_b, !config.bEnabledPinState);
gpio_set_level(config.gpio_a, config.aEnabledPinState);
gpio_set_level(config.gpio_b, config.bEnabledPinState);
break;
case motorstate_t::BRAKE:
ledc_set_duty(LEDC_HIGH_SPEED_MODE, config.ledc_channel, 0);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, config.ledc_channel);
//brake:
gpio_set_level(config.gpio_a, !config.aEnabledPinState);
gpio_set_level(config.gpio_b, !config.bEnabledPinState);
break;
case motorstate_t::FWD:
ledc_set_duty(LEDC_HIGH_SPEED_MODE, config.ledc_channel, dutyScaled);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, config.ledc_channel);
//forward:
gpio_set_level(config.gpio_a, config.aEnabledPinState);
gpio_set_level(config.gpio_b, !config.bEnabledPinState);
break;
case motorstate_t::REV:
ledc_set_duty(LEDC_HIGH_SPEED_MODE, config.ledc_channel, dutyScaled);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, config.ledc_channel);
//reverse:
gpio_set_level(config.gpio_a, !config.aEnabledPinState);
gpio_set_level(config.gpio_b, config.bEnabledPinState);
break;
}
ESP_LOGD(TAG, "set module to state=%s, duty=%d/%d, duty_input=%.3f%%", motorstateStr[(int)state_f], dutyScaled, dutyMax, duty_f);
}

View File

@ -0,0 +1,65 @@
#pragma once
extern "C"
{
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "driver/ledc.h"
#include "esp_err.h"
}
#include <cmath>
//====================================
//===== single100a motor driver ======
//====================================
//--------------------------------------------
//---- struct, enum, variable declarations ---
//--------------------------------------------
//class which controls a motor using a 'single100a' h-bridge module
enum class motorstate_t {IDLE, FWD, REV, BRAKE};
//definition of string array to be able to convert state enum to readable string (defined in motordrivers.cpp)
extern const char* motorstateStr[4];
//struct with all config parameters for single100a motor driver
typedef struct single100a_config_t {
gpio_num_t gpio_pwm;
gpio_num_t gpio_a;
gpio_num_t gpio_b;
ledc_timer_t ledc_timer;
ledc_channel_t ledc_channel;
bool aEnabledPinState;
bool bEnabledPinState;
ledc_timer_bit_t resolution;
int pwmFreq;
} single100a_config_t;
//--------------------------------
//------- single100a class -------
//--------------------------------
class single100a {
public:
//--- constructor ---
single100a(single100a_config_t config_f); //provide config struct (see above)
//--- functions ---
void set(motorstate_t state, float duty_f = 0); //set mode and duty of the motor (see motorstate_t above)
//TODO: add functions to get the current state and duty
private:
//--- functions ---
void init(); //initialize pwm and gpio outputs, calculate maxDuty
//--- variables ---
single100a_config_t config;
uint32_t dutyMax;
motorstate_t state = motorstate_t::IDLE;
};

265
board_motorctl/main/wifi.c Normal file
View File

@ -0,0 +1,265 @@
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_mac.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "wifi.h"
//--- variables used for ap and wifi ---
static const char *TAG = "wifi";
static esp_event_handler_instance_t instance_any_id;
//============================================
//============ init nvs and netif ============
//============================================
//initialize nvs-flash and netif (needed for both AP and CLIENT)
//has to be run once at startup
void wifi_initNvs_initNetif(){
//Initialize NVS (needed for wifi)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
}
//===========================================
//============ init access point ============
//===========================================
//--------------------------------------------
//------ configuration / declarations --------
//--------------------------------------------
#define EXAMPLE_ESP_WIFI_SSID_AP "armchair"
#define EXAMPLE_ESP_WIFI_PASS_AP ""
#define EXAMPLE_ESP_WIFI_CHANNEL_AP 1
#define EXAMPLE_MAX_STA_CONN_AP 4
static esp_netif_t *ap;
static void wifi_event_handler_ap(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
MAC2STR(event->mac), event->aid);
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
MAC2STR(event->mac), event->aid);
}
}
//-----------------------
//------ init ap --------
//-----------------------
void wifi_init_ap(void)
{
ap = esp_netif_create_default_wifi_ap();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler_ap,
NULL,
&instance_any_id));
wifi_config_t wifi_config = {
.ap = {
.ssid = EXAMPLE_ESP_WIFI_SSID_AP,
.ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID_AP),
.channel = EXAMPLE_ESP_WIFI_CHANNEL_AP,
.password = EXAMPLE_ESP_WIFI_PASS_AP,
.max_connection = EXAMPLE_MAX_STA_CONN_AP,
.authmode = WIFI_AUTH_WPA_WPA2_PSK
},
};
if (strlen(EXAMPLE_ESP_WIFI_PASS_AP) == 0) {
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
}
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d",
EXAMPLE_ESP_WIFI_SSID_AP, EXAMPLE_ESP_WIFI_PASS_AP, EXAMPLE_ESP_WIFI_CHANNEL_AP);
}
//=============================
//========= deinit AP =========
//=============================
void wifi_deinit_ap(void)
{
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
esp_wifi_stop();
esp_wifi_deinit();
esp_netif_destroy_default_wifi(ap);
}
//===========================================
//=============== init client ===============
//===========================================
//--------------------------------------------
//------ configuration / declarations --------
//--------------------------------------------
#define EXAMPLE_ESP_WIFI_SSID_CLIENT "BKA-network"
#define EXAMPLE_ESP_WIFI_PASS_CLIENT "airwaveslogitech410"
#define EXAMPLE_ESP_MAXIMUM_RETRY_CLIENT 10
static esp_netif_t *sta;
static esp_event_handler_instance_t instance_got_ip;
/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;
/* The event group allows multiple bits for each event, but we only care about two events:
* - we are connected to the AP with an IP
* - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static int s_retry_num = 0;
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY_CLIENT) {
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "retry to connect to the AP");
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(TAG,"connect to the AP fail");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
//---------------------------
//------ init client --------
//---------------------------
void wifi_init_client(void)
{
s_wifi_event_group = xEventGroupCreate();
sta = esp_netif_create_default_wifi_sta();
//set static ip
esp_netif_dhcpc_stop(sta);
esp_netif_ip_info_t ip_info;
IP4_ADDR(&ip_info.ip, 10, 0, 0, 66);
IP4_ADDR(&ip_info.gw, 10, 0, 0, 1);
IP4_ADDR(&ip_info.netmask, 255, 255, 0, 0);
esp_netif_set_ip_info(sta, &ip_info);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_ESP_WIFI_SSID_CLIENT,
.password = EXAMPLE_ESP_WIFI_PASS_CLIENT,
/* Setting a password implies station will connect to all security modes including WEP/WPA.
* However these modes are deprecated and not advisable to be used. Incase your Access point
* doesn't support WPA2, these mode can be enabled by commenting below line */
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );
ESP_LOGI(TAG, "wifi_init_sta finished.");
/* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
* number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
/* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
* happened. */
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
EXAMPLE_ESP_WIFI_SSID_CLIENT, EXAMPLE_ESP_WIFI_PASS_CLIENT);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
EXAMPLE_ESP_WIFI_SSID_CLIENT, EXAMPLE_ESP_WIFI_PASS_CLIENT);
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}
// /* The event will not be processed after unregister */
// ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip));
// ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
// vEventGroupDelete(s_wifi_event_group);
}
//=================================
//========= deinit client =========
//=================================
void wifi_deinit_client(void)
{
/* The event will not be processed after unregister */
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip));
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
vEventGroupDelete(s_wifi_event_group);
esp_wifi_stop();
esp_wifi_deinit();
esp_netif_destroy_default_wifi(sta);
}

View File

@ -0,0 +1,22 @@
#pragma once
//TODO: currently wifi names and passwords are configured in wifi.c -> move this to config?
//initialize nvs-flash and netif (needed for both AP and CLIENT)
//has to be run once at startup
//Note: this cant be put in wifi_init functions because this may not be in deinit functions
void wifi_initNvs_initNetif();
//function to start an access point
void wifi_init_ap(void);
//function to disable/deinit access point
void wifi_deinit_ap(void);
//function to connect to existing wifi network
void wifi_init_client(void);
//function to disable/deinit client
void wifi_deinit_client(void);

View File

@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
spiffs, data, spiffs, , 1M
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, , 0x6000,
4 phy_init, data, phy, , 0x1000,
5 factory, app, factory, , 1M,
6 spiffs, data, spiffs, , 1M

28358
board_motorctl/react-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
{
"name": "react-new",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-joystick-component": "^4.0.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4",
"websocket": "^1.0.34"
},
"scripts": {
"start": "react-scripts start",
"build": "GENERATE_SOURCEMAP=false react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Armchair control webapp"
/>
<title>armchair ctl</title>
</head>
<body style="background-color:#001427; color:white;">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

245
board_motorctl/react-app/src/App.js vendored Normal file
View File

@ -0,0 +1,245 @@
import { Joystick } from 'react-joystick-component';
import React, { useState} from 'react';
//import { w3cwebsocket as W3CWebSocket } from "websocket";
function App() {
//declare variables that can be used and updated in html
const [x_html, setX_html] = useState(0);
const [y_html, setY_html] = useState(0);
const [ip, setIp] = useState("10.0.0.66");
//===============================
//=========== config ============
//===============================
const decimalPlaces = 3;
const joystickSize = 250; //affects scaling of coordinates and size of joystick on website
const throttle = 300; //throtthe interval the joystick sends data while moving (ms)
const toleranceSnapToZeroPer = 20;//percentage of moveable range the joystick can be moved from the axix and value stays at 0
//-------------------------------------------------
//------- Scale coordinate, apply tolerance -------
//-------------------------------------------------
//function that:
// - scales the coodinate to a range of -1 to 1
// - snaps 0 zero for a given tolerance in percent
// - rounds value do given decimal places
// - TODO: add threshold it snaps to 1 / -1 (100%) toleranceEnd
const ScaleCoordinateTolerance = (input) => {
//calc tolerance threshold and available range
const tolerance = joystickSize/2 * toleranceSnapToZeroPer/100;
const range = joystickSize/2 - tolerance;
let result = 0;
//console.log("value:",input,"tolerance:",tolerance," range:",range);
//input positive and above 'snap to zero' threshold
if ( input > 0 && input > tolerance ){
result = ((input-tolerance)/range).toFixed(decimalPlaces);
}
//input negative and blow 'snap to zero' threshold
else if ( input < 0 && input < -tolerance ){
result = ((input+tolerance)/range).toFixed(decimalPlaces);
}
//inside threshold around zero
else {
result = 0;
}
//return result
//console.log("result:", result, "\n");
return result;
}
//----------------------------------------
//----------- Scale coordinate -----------
//----------------------------------------
//simply scale coordinate from joystick to a value of -1 to 1 without applying any tolerances
const ScaleCoordinate = (input) => {
return ( input / (joystickSize/2) ).toFixed(decimalPlaces);
}
//-------------------------------------------
//------- Senda data via POST request -------
//-------------------------------------------
//function that sends an object as json to the esp32 with a http post request
const httpSendObject = async (object_data) => {
//debug log
console.log("Sending:", object_data);
let json = JSON.stringify(object_data);
//console.log("json string:", json);
//remove quotes around numbers:
//so cJSON parses the values as actua[l numbers than strings
const regex2 = /"(-?[0-9]+\.{0,1}[0-9]*)"/g
json = json.replace(regex2, '$1')
//console.log("json removed quotes:", json);
//--- API url / ip ---
//await fetch("http://10.0.1.69/api/joystick", {
//await fetch("http://10.0.1.72/api/joystick", {
await fetch("api/joystick", {
method: "POST",
//apparently browser sends OPTIONS request before actual POST request, this OPTIONS request was not handled by esp32
//also the custom set Access-Control-Allow-Origin header in esp32 url header was not read because of that
//changed content type to text/plain to workaround this
//https://stackoverflow.com/questions/1256593/why-am-i-getting-an-options-request-instead-of-a-get-request
headers: {
//"Content-Type": "application/json",
"Content-Type": "text/plain",
},
body: json,
})
//.then((response) => console.log(response));
};
//---------------------------------------
//--- function when joystick is moved ---
//---------------------------------------
//function that is run for each move event
//evaluate coordinates and send to esp32
const handleMove = (e) => {
//console.log("data from joystick-element X:" + e.x + " Y:" + e.y + " distance:" + e.distance);
//--- convert coordinates ---
//Note: tolerance (snap to zero) now handled by controller -> send raw coordinates
//const x = ScaleCoordinateTolerance(e.x);
//const y = ScaleCoordinateTolerance(e.y);
const x = ScaleCoordinate(e.x);
const y = ScaleCoordinate(e.y);
//create object with necessary data
const joystick_data={
x: x,
y: y
}
//send object with joystick data as json to controller
httpSendObject(joystick_data);
//update variables for html
setX_html(joystick_data.x);
setY_html(joystick_data.y);
};
//------------------------------------------
//--- function when joystick is released ---
//------------------------------------------
const handleStop = (e) => {
//create object with all values 0
const joystick_data={
x: 0,
y: 0,
}
//update variables for html
setX_html(0);
setY_html(0);
//send object with joystick data as json to controller
httpSendObject(joystick_data);
};
//=============================
//======== return html ========
//=============================
return (
<>
<div style={{display:'flex', justifyContent:'center', alignItems:'center', height:'100vh'}}>
<div>
<div style={{position: 'absolute', top: '0', left: '0', width: '100%'}}>
<h1 style={{width:'100%', textAlign:'center'}}>Armchair ctl</h1>
</div>
<Joystick
size={joystickSize}
sticky={false}
baseColor="#8d0801"
stickColor="#708d81"
throttle={throttle}
move={handleMove}
stop={handleStop}
>
</Joystick>
<ul>
<li> x={x_html} </li>
<li> y={y_html} </li>
</ul>
</div>
</div>
{/*
buttons for changing the api IP
<div>
<a>current ip used: {ip}</a>
<button onClick={() => {setIp("10.0.0.66")}} >10.0.0.66 (BKA-network)</button>
</div>
*/}
</>
);
}
export default App;
//del, testing, unused code
//---------------------------------------------
//--------- Send data via websocket -----------
//---------------------------------------------
//moved to normal POST request since websocket connection was unreliable on esp32
// //create websocket
// const websocket = useRef(null);
// //const socketUrl = "ws://" + window.location.host + "/ws-api/servo";
// const socketUrl = "ws://10.0.1.69/ws-api/joystick";
// useEffect(() => {
// websocket.current = new W3CWebSocket(socketUrl);
// websocket.current.onmessage = (message) => {
// console.log('got reply! ', message);
// };
// websocket.current.onopen = (event) => {
// console.log('OPENED WEBSOCKET', event);
// //sendJoystickData(0, 0, 0, 0);
// websocket.current.send("");
// };
// websocket.current.onclose = (event) => {
// console.log('CLOSED WEBSOCKET', event);
// };
// return () => websocket.current.close();
// }, [])
//
//
//
// //function for sending joystick data (provided as parameters) to controller via websocket
// const sendJoystickDataWebsocket = (x, y, radius, angle) => {
// //debug log
// console.log("Sending:\n X:" + x + "\n Y:" + y + "\n radius:" + radius + "\n angle: " + angle);
//
// websocket.current.send(
// JSON.stringify({
// x: x,
// y: y,
// radius: radius,
// angle: angle
// })
// );
// }

14
board_motorctl/react-app/src/index.js vendored Normal file
View File

@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals

1397
sdkconfig

File diff suppressed because it is too large Load Diff