jonny_l480 4112d8653c Add dynamic-winding-length depending on targetLength
When winding short lengths e.g. 5m it does not make sense
to use the entire winding width thus getting unnecessary
wide cable ring.

Determined some thresholds while testing and implemented
those to be applied at target length change automatically:

control:
    - update winding width each time target length changes

guide-stepper:
    - add function that returns dynamic winding width depending
      on passed target length according to fixed thresholds
2024-03-17 09:41:24 +01:00

590 lines
24 KiB
C++

extern "C"
{
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_idf_version.h>
#include "freertos/queue.h"
#include "esp_system.h"
#include "esp_log.h"
#include "driver/adc.h"
#include "max7219.h"
}
#include <cmath>
#include "config.h"
#include "gpio_evaluateSwitch.hpp"
#include "gpio_adc.hpp"
#include "buzzer.hpp"
#include "vfd.hpp"
#include "display.hpp"
#include "cutter.hpp"
#include "encoder.hpp"
#include "guide-stepper.hpp"
#include "global.hpp"
#include "control.hpp"
//-----------------------------------------
//--------------- variables ---------------
//-----------------------------------------
static const char *TAG = "control"; //tag for logging
//control
const char* systemStateStr[7] = {"COUNTING", "WINDING_START", "WINDING", "TARGET_REACHED", "AUTO_CUT_WAITING", "CUTTING", "MANUAL"};
systemState_t controlState = systemState_t::COUNTING;
static uint32_t timestamp_lastStateChange = 0;
//display
static char buf_disp1[10];// 8 digits + decimal point + \0
static char buf_disp2[10];// 8 digits + decimal point + \0
static char buf_tmp[15];
//track length
static int lengthNow = 0; //length measured in mm
static int lengthTarget = 5000; //default target length in mm
static int lengthRemaining = 0; //(target - now) length needed for reaching the target
static int potiRead = 0; //voltage read from adc
static uint32_t timestamp_motorStarted = 0; //timestamp winding started
//automatic cut
static int cut_msRemaining = 0;
static uint32_t timestamp_cut_lastBeep = 0;
static uint32_t autoCut_delayMs = 2500; //TODO add this to config
static bool autoCutEnabled = false; //store state of toggle switch (no hotswitch)
//user interface
static uint32_t timestamp_lastWidthSelect = 0;
//ignore new set events for that time after last value set using poti
#define DEAD_TIME_POTI_SET_VALUE 1000
//-----------------------------------------
//--------------- functions ---------------
//-----------------------------------------
//========================
//===== change State =====
//========================
//function for changing the controlState with log output
void changeState (systemState_t stateNew) {
//only proceed when state actually changed
if (controlState == stateNew) {
return; //already at target state -> nothing to do
}
//log change
ESP_LOGW(TAG, "changed state from %s to %s", systemStateStr[(int)controlState], systemStateStr[(int)stateNew]);
//change state
controlState = stateNew;
//update timestamp
timestamp_lastStateChange = esp_log_timestamp();
}
//=================================
//===== handle Stop Condition =====
//=================================
//function that checks whether start button is released or target is reached
//and takes according action if so (used in multiple states)
//returns true when stop condition was met, false when no action required
bool handleStopCondition(handledDisplay * displayTop, handledDisplay * displayBot){
//--- stop conditions ---
//stop conditions that are checked in any mode
//target reached -> reached state, stop motor, display message
if (lengthRemaining <= 0 ) {
changeState(systemState_t::TARGET_REACHED);
vfd_setState(false);
displayTop->blink(1, 0, 1000, " S0LL ");
displayBot->blink(1, 0, 1000, "ERREICHT");
buzzer.beep(2, 100, 100);
return true;
}
//start button released -> idle state, stop motor, display message
else if (SW_START.state == false) {
changeState(systemState_t::COUNTING);
vfd_setState(false);
displayTop->blink(2, 900, 1000, "- STOP -");
displayBot->blink(2, 900, 1000, " TASTER ");
buzzer.beep(3, 200, 100);
return true;
} else {
return false;
}
}
//===================================
//===== set dynamic speed level =====
//===================================
//function that sets the vfd speed level dynamically depending on the remaining distance
//closer to target -> slower
void setDynSpeedLvl(uint8_t lvlMax = 3){
uint8_t lvl;
//define speed level according to difference
if (lengthRemaining < 40) {
lvl = 0;
} else if (lengthRemaining < 300) {
lvl = 1;
} else if (lengthRemaining < 700) {
lvl = 2;
} else { //more than last step remaining
lvl = 3;
}
//limit to max lvl
if (lvl > lvlMax) {
lvl = lvlMax;
}
//update vfd speed level
vfd_setSpeedLevel(lvl);
}
//========================
//===== control task =====
//========================
//task that controls the entire machine
void task_control(void *pvParameter)
{
//-- initialize display --
max7219_t two7SegDisplays = display_init();
//create two separate custom handled display instances
handledDisplay displayTop(two7SegDisplays, 0);
handledDisplay displayBot(two7SegDisplays, 8);
//-- display welcome msg --
//display welcome message on two 7 segment displays
//currently show name and date and scrolling 'hello'
display_ShowWelcomeMsg(two7SegDisplays);
//-- set initial winding width for default length --
guide_setWindingWidth(guide_targetLength2WindingWidth(lengthTarget));
// ##############################
// ######## control loop ########
// ##############################
// repeatedly handle the machine
while(1){
vTaskDelay(10 / portTICK_PERIOD_MS);
//------ handle switches ------
//run handle functions for all switches used here
SW_START.handle();
SW_RESET.handle();
SW_SET.handle();
SW_PRESET1.handle();
SW_PRESET2.handle();
SW_PRESET3.handle();
SW_CUT.handle();
SW_AUTO_CUT.handle();
//------ handle cutter ------
//TODO: separate task for cutter?
cutter_handle();
//------ rotary encoder ------
//get current length since last reset
lengthNow = encoder_getLenMm();
//--------- buttons ---------
//#### RESET switch ####
if (SW_RESET.risingEdge) {
//dont reset when press used for stopping pending auto-cut
if (controlState != systemState_t::AUTO_CUT_WAITING) {
guide_moveToZero(); //move axis guiding the cable to start position
encoder_reset(); //reset length measurement
lengthNow = 0;
buzzer.beep(1, 700, 100);
displayTop.blink(2, 100, 100, "1ST ");
//TODO: stop cutter with reset switch?
//cutter_stop();
}
}
//### CUT switch ####
//start cut cycle immediately
if (SW_CUT.risingEdge) {
//stop cutter if already running
if (cutter_isRunning()) {
cutter_stop();
buzzer.beep(1, 600, 0);
}
else if (controlState == systemState_t::AUTO_CUT_WAITING) {
//do nothing when press used for stopping pending auto-cut
}
//start cutter when motor not active
else if (controlState != systemState_t::WINDING_START //TODO use vfd state here?
&& controlState != systemState_t::WINDING) {
cutter_start();
buzzer.beep(1, 70, 50);
}
//error cant cut while motor is on
else {
buzzer.beep(6, 100, 50);
}
}
//#### AUTO_CUT toggle sw ####
//beep at change
if (SW_AUTO_CUT.risingEdge) {
buzzer.beep(2, 100, 50);
} else if (SW_AUTO_CUT.fallingEdge) {
buzzer.beep(1, 400, 50);
}
//update enabled state
if (SW_AUTO_CUT.state) {
//enable autocut when not in target_reached state
//(prevent immediate/unexpected cut)
if (controlState != systemState_t::TARGET_REACHED) {
autoCutEnabled = true;
}
} else {
//disable anytime (also stops countdown to auto cut)
autoCutEnabled = false;
}
//#### manual mode ####
//switch to manual motor control (2 buttons + poti)
if ( SW_PRESET2.state && (SW_PRESET1.state || SW_PRESET3.state) && controlState != systemState_t::MANUAL ) {
//enable manual control
changeState(systemState_t::MANUAL);
buzzer.beep(3, 100, 60);
}
//##### SET switch + Potentiometer #####
//## set winding-width (SET+PRESET1+POTI) ##
// set winding width (axis travel) with poti position
// when SET and PRESET1 button are pressed
if (SW_SET.state == true && SW_PRESET1.state == true) {
timestamp_lastWidthSelect = esp_log_timestamp();
//read adc
potiRead = gpio_readAdc(ADC_CHANNEL_POTI); //0-4095
//scale to target length range
uint8_t windingWidthNew = (float)potiRead / 4095 * MAX_SELECTABLE_WINDING_WIDTH_MM;
//apply hysteresis and round to whole meters //TODO optimize this
if (windingWidthNew % 5 < 2) { //round down if remainder less than 2mm
ESP_LOGD(TAG, "Poti input = %d -> rounding down", windingWidthNew);
windingWidthNew = (windingWidthNew/5 ) * 5; //round down
} else if (windingWidthNew % 5 > 4 ) { //round up if remainder more than 4mm
ESP_LOGD(TAG, "Poti input = %d -> rounding up", windingWidthNew);
windingWidthNew = (windingWidthNew/5 + 1) * 5; //round up
} else {
ESP_LOGD(TAG, "Poti input = %d -> hysteresis", windingWidthNew);
windingWidthNew = guide_getWindingWidth();
}
//update target width and beep when effectively changed
if (windingWidthNew != guide_getWindingWidth()) {
//TODO update at button release only?
guide_setWindingWidth(windingWidthNew);
ESP_LOGW(TAG, "Changed winding width to %d mm", windingWidthNew);
buzzer.beep(1, 30, 10);
}
}
//## set target length (SET+POTI) ##
//set target length to poti position when only SET button is pressed and certain dead time passed after last setWindingWidth (SET and PRESET1 button) to prevent set target at release
// FIXME: when going to edit the winding width (SET+PRESET1) sometimes the target-length also updates when initially pressing SET -> update only at actual poti change (works sometimes)
else if (SW_SET.state == true && (esp_log_timestamp() - timestamp_lastWidthSelect > DEAD_TIME_POTI_SET_VALUE)) {
//read adc
potiRead = gpio_readAdc(ADC_CHANNEL_POTI); //0-4095
//scale to target length range
int lengthTargetNew = (float)potiRead / 4095 * MAX_SELECTABLE_LENGTH_POTI_MM;
//apply hysteresis and round to whole meters //TODO optimize this
if (lengthTargetNew % 1000 < 200) { //round down if less than .2 meter
ESP_LOGD(TAG, "Poti input = %d -> rounding down", lengthTargetNew);
lengthTargetNew = (lengthTargetNew/1000 ) * 1000; //round down
} else if (lengthTargetNew % 1000 > 800 ) { //round up if more than .8 meter
ESP_LOGD(TAG, "Poti input = %d -> rounding up", lengthTargetNew);
lengthTargetNew = (lengthTargetNew/1000 + 1) * 1000; //round up
} else {
ESP_LOGD(TAG, "Poti input = %d -> hysteresis", lengthTargetNew);
lengthTargetNew = lengthTarget;
}
//update target length and beep when effectively changed
if (lengthTargetNew != lengthTarget) {
//TODO update lengthTarget only at button release?
lengthTarget = lengthTargetNew;
guide_setWindingWidth(guide_targetLength2WindingWidth(lengthTarget));
ESP_LOGI(TAG, "Changed target length to %d mm", lengthTarget);
buzzer.beep(1, 25, 10);
}
}
//beep start and end of editing
if (SW_SET.risingEdge) {
buzzer.beep(1, 70, 50);
}
if (SW_SET.fallingEdge) {
buzzer.beep(2, 70, 50);
displayBot.blink(2, 100, 100, "S0LL ");
}
//##### target length preset buttons #####
if (controlState != systemState_t::MANUAL && SW_SET.state == false) { //dont apply preset length while controlling motor with preset buttons
if (SW_PRESET1.risingEdge) {
lengthTarget = 5000;
guide_setWindingWidth(guide_targetLength2WindingWidth(lengthTarget));
buzzer.beep(lengthTarget/1000, 25, 30);
displayBot.blink(2, 100, 100, "S0LL ");
}
else if (SW_PRESET2.risingEdge) {
lengthTarget = 10000;
guide_setWindingWidth(guide_targetLength2WindingWidth(lengthTarget));
buzzer.beep(lengthTarget/1000, 25, 30);
displayBot.blink(2, 100, 100, "S0LL ");
}
else if (SW_PRESET3.risingEdge) {
lengthTarget = 15000;
guide_setWindingWidth(guide_targetLength2WindingWidth(lengthTarget));
buzzer.beep(lengthTarget/1000, 25, 30);
displayBot.blink(2, 100, 100, "S0LL ");
}
}
//---------------------------
//--------- control ---------
//---------------------------
//statemachine handling the sequential winding process
//calculate current length difference
lengthRemaining = lengthTarget - lengthNow + TARGET_LENGTH_OFFSET;
//--- statemachine ---
switch (controlState) {
case systemState_t::COUNTING: //no motor action, just show current length on display
vfd_setState(false);
//TODO check stop condition before starting - prevents motor from starting 2 cycles when already at target
//--- start winding to length ---
if (SW_START.risingEdge) {
changeState(systemState_t::WINDING_START);
vfd_setSpeedLevel(1); //start at low speed
vfd_setState(true); //start motor
timestamp_motorStarted = esp_log_timestamp(); //save time started
buzzer.beep(1, 100, 0);
}
break;
case systemState_t::WINDING_START: //wind slow for certain time
//set vfd speed depending on remaining distance
setDynSpeedLvl(1); //limit to speed lvl 1 (force slow start)
//switch to WINDING state (full speed) when 3s have passed
if (esp_log_timestamp() - timestamp_motorStarted > 3000) {
changeState(systemState_t::WINDING);
}
handleStopCondition(&displayTop, &displayBot); //stops if button released or target reached
//TODO: cancel when there was no cable movement during start time?
break;
case systemState_t::WINDING: //wind fast, slow down when close
//set vfd speed depending on remaining distance
setDynSpeedLvl(); //set motor speed, slow down when close to target
handleStopCondition(&displayTop, &displayBot); //stops if button released or target reached
//TODO: cancel when there is no cable movement anymore e.g. empty / timeout?
break;
case systemState_t::TARGET_REACHED: //prevent further motor rotation and start auto-cut
vfd_setState(false);
//switch to counting state when no longer at or above target length
if ( lengthNow < lengthTarget - TARGET_REACHED_TOLERANCE ) {
changeState(systemState_t::COUNTING);
}
//initiate countdown to auto-cut if enabled
else if ( (autoCutEnabled)
&& (esp_log_timestamp() - timestamp_lastStateChange > 300) ) { //wait for dislay msg "reached" to finish
changeState(systemState_t::AUTO_CUT_WAITING);
}
//show msg when trying to start, but target is already reached (-> reset button has to be pressed)
if (SW_START.risingEdge) {
buzzer.beep(2, 50, 30);
displayTop.blink(2, 600, 500, " S0LL ");
displayBot.blink(2, 600, 500, "ERREICHT");
}
break;
case systemState_t::AUTO_CUT_WAITING: //handle delayed start of cut
cut_msRemaining = autoCut_delayMs - (esp_log_timestamp() - timestamp_lastStateChange);
//- countdown stop conditions -
//stop with any button
if (!autoCutEnabled
|| SW_RESET.state || SW_CUT.state
|| SW_SET.state || SW_PRESET1.state
|| SW_PRESET2.state || SW_PRESET3.state) {//TODO: also stop when target not reached anymore?
changeState(systemState_t::COUNTING);
buzzer.beep(5, 100, 50);
}
//- trigger cut if delay passed -
else if (cut_msRemaining <= 0) {
cutter_start();
changeState(systemState_t::CUTTING);
}
//- beep countdown -
//time passed since last beep > time remaining / 6
else if ( (esp_log_timestamp() - timestamp_cut_lastBeep) > (cut_msRemaining / 6)
&& (esp_log_timestamp() - timestamp_cut_lastBeep) > 50 ) { //dont trigger beeps faster than beep time
buzzer.beep(1, 50, 0);
timestamp_cut_lastBeep = esp_log_timestamp();
}
break;
case systemState_t::CUTTING: //prevent any action while cutter is active
//exit when finished cutting
if (cutter_isRunning() == false) {
//TODO stop if start buttons released?
changeState(systemState_t::COUNTING);
//TODO reset automatically or wait for manual reset?
guide_moveToZero(); //move axis guiding the cable to start position
encoder_reset(); //reset length measurement
lengthNow = 0;
buzzer.beep(1, 700, 100);
}
break;
case systemState_t::MANUAL: //manually control motor via preset buttons + poti
//read poti value
potiRead = gpio_readAdc(ADC_CHANNEL_POTI); //0-4095
//scale poti to speed levels 0-3
uint8_t level = round( (float)potiRead / 4095 * 3 );
//exit manual mode if preset2 released
if ( SW_PRESET2.state == false ) {
changeState(systemState_t::COUNTING);
buzzer.beep(1, 1000, 100);
}
//P2 + P1 -> turn left
else if ( SW_PRESET1.state && !SW_PRESET3.state ) {
vfd_setSpeedLevel(level);
vfd_setState(true, REV);
sprintf(buf_disp2, "[--%02i ", level);
// 123 45 678
}
//P2 + P3 -> turn right
else if ( SW_PRESET3.state && !SW_PRESET1.state ) {
vfd_setSpeedLevel(level);
vfd_setState(true, FWD);
sprintf(buf_disp2, " %02i--]", level);
}
//no valid switch combination -> turn off motor
else {
vfd_setState(false);
sprintf(buf_disp2, " %02i ", level);
}
}
#ifdef ENCODER_TEST
//--------------------------
//------ encoder test ------
//--------------------------
//mode for calibrating the cable length measurement (determine ENCODER_STEPS_PER_METER in config.h)
//run display handle functions
displayTop.handle();
displayBot.handle();
//-- show encoder steps on display1 ---
sprintf(buf_disp1, "EN %05d", encoder_getSteps()); //count
displayTop.showString(buf_disp1);
//--- show converted distance on display2 ---
sprintf(buf_disp2, "Met %5.3f", (float)lengthNow/1000); //m
displayBot.showString(buf_disp2);
//--- beep every 0.5m ---
//note: only works precisely in forward/positive direction, in reverse it it beeps by tolerance too early
static int lengthBeeped = 0;
if (lengthNow % 500 < 50) { //with tolerance in case of missed exact value
if (fabs(lengthNow - lengthBeeped) >= 400) { //dont beep multiple times at same distance
//TODO: add case for reverse direction. currently beeps 50mm too early
if (lengthNow % 1000 < 50) // 1m beep
buzzer.beep(1, 400, 100);
else // 0.5m beep
buzzer.beep(1, 200, 100);
lengthBeeped = lengthNow;
}
}
#else //not in encoder calibration mode
//--------------------------
//-------- display1 --------
//--------------------------
//run handle function
displayTop.handle();
//indicate upcoming cut when pending
if (controlState == systemState_t::AUTO_CUT_WAITING) {
displayTop.blinkStrings(" CUT 1N ", " ", 70, 30);
}
//setting winding width: blink info message
else if (SW_SET.state && SW_PRESET1.state){
displayTop.blinkStrings("SET WIND", " WIDTH ", 900, 900);
}
//otherwise show current position
else {
sprintf(buf_tmp, "1ST %5.4f", (float)lengthNow/1000);
// 123456789
//limit length to 8 digits + decimal point (drop decimal places when it does not fit)
sprintf(buf_disp1, "%.9s", buf_tmp);
displayTop.showString(buf_disp1);
}
//--------------------------
//-------- display2 --------
//--------------------------
//run handle function
displayBot.handle();
//notify that cutter is active
if (cutter_isRunning()) {
displayBot.blinkStrings("CUTTING]", "CUTTING[", 100, 100);
}
//show ms countdown to cut when pending
else if (controlState == systemState_t::AUTO_CUT_WAITING) {
sprintf(buf_disp2, " %04d ", cut_msRemaining);
//displayBot.showString(buf_disp2); //TODO:blink "erreicht" overrides this. for now using blink as workaround
displayBot.blinkStrings(buf_disp2, buf_disp2, 100, 100);
}
//manual state: blink "manual"
else if (controlState == systemState_t::MANUAL) {
displayBot.blinkStrings(" MANUAL ", buf_disp2, 400, 800);
}
//setting winding width: blink currently set windingWidth
else if (SW_SET.state && SW_PRESET1.state){
sprintf(buf_tmp, " %03d mm", guide_getWindingWidth());
displayBot.blinkStrings(buf_tmp, " ", 300, 100);
}
//setting target length: blink target length
else if (SW_SET.state == true) {
sprintf(buf_tmp, "S0LL%5.3f", (float)lengthTarget/1000);
displayBot.blinkStrings(buf_tmp, "S0LL ", 300, 100);
}
//otherwise show target length
else {
//sprintf(buf_disp2, "%06.1f cm", (float)lengthTarget/10); //cm
sprintf(buf_tmp, "S0LL%5.3f", (float)lengthTarget/1000); //m
// 1234 5678
displayBot.showString(buf_tmp);
}
#endif // end else ifdef ENCODER_TEST
//----------------------------
//------- control lamp -------
//----------------------------
//basic functionality of lamp:
//turn on when not idling
//TODO: add switch-case for different sates
//e.g. blink with different frequencies in different states
if (controlState != systemState_t::COUNTING
&& controlState != systemState_t::TARGET_REACHED) {
gpio_set_level(GPIO_LAMP, 1);
}
else {
gpio_set_level(GPIO_LAMP, 0);
}
} //end while(1)
} //end task_control