/*
  xdrv_92_vid6608.ino - Support for VID6608 automotive gauge stepper motor driver

  Copyright (C) 2025  Petr Golovachev

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef USE_VID6608

#define XDRV_92             92
#define VID6608_MAX_DRIVES  4

/*
  VID6608 automotive gauge stepper motor driver

  This driver implements support for following driver chips for analog automotive gauges (Switec X25.168, X27.168 and clones) with microstepping support:

    * VID6606 (2 motors)
    * VID6608 (4 motors)
    * VT6608S
    * AX1201728SG
    * BY8920
    * many others

  Driver chips with microstepping is the recommended way to drive such motors,
  they provide much more relailabe and smooth movement with reduced noise and
  to avoid skipping steps.

  Driver is configured to perform 320° rotation angle with 12 steps per degree.
  Total capacity is 3840 steps for whole scale.

  Library homepage: https://github.com/petrows/arduino-vid6608

  Connection:

    * Connect IC VID6608 inputs F(scx) and CW/CCW to GPIO pins
    * Connect RESET pin of VID6608 to VCC
    * Define "VID6608 F" and "VID6608 CW" pins in Configuration / Module page

  Driver adds following commands:

    * Gauge         : returns current gauges state
    * GaugeSetX     : set gauge to absolute position in range 0..3840, where X - motor index from 1 to 4, 0 for all
    * GaugePercentX : set gauge position in percents in range 0..100, where X - motor index from 1 to 4, 0 for all
    * GaugeZeroX    : triggers gauge calibration and homing, where X - motor index from 1 to 4, 0 for all

  Performance notes:

    * ESP32: Driver uses background FreeRTOS task for impulse generation, as it requires microsecond precision
      for inpulses. ESP32 uses FreeRTOS API and movement is fast and smooth (very similar to real car gauges).
    * ESP8266: For ESP8266 driver also works, but movement is much slower (but it is still okay for slow
      changing values, i.e. temperature). On ESP8266 speed is ~2 sec per degree.

  Version history:

    * 2026-05-17 - add support for ESP RMT driver
    * 2025-11-23 - fixes related with ESP8266 performance
    * 2025-11-22 - initial release
*/

/**
 * @brief Use HW RMT driver?
 * Provides much better performance, but requires free RMT,
 * some SoC (i.e. ESP32C6) have only 2 onboard, and some may be used by other drivers
 * (like TasmotaLED and others).
 *
 * Default is disabled
 */
#if !defined(VID6608_RMT)
#define VID6608_RMT 0
#endif

#if VID6608_RMT
  #include "soc/soc_caps.h"
  #if !SOC_RMT_SUPPORTED
    #error "VID6608 RMT is not supported on this platform"
  #endif
#endif

/**
 * @brief Driver impulse mode decision here
 * ESP-32 has FreeRTOS, that allows us to perform precision inpulse control
 * Disabled in RMT mode
 */
#if defined(ESP32) && !VID6608_RMT
  #define VID6608_RTOS
#endif

/**
 * @brief Reset all drives on init?
 *
 * Disable if you dont want to perform reset/homing operation on driver init,
 * usefull for cases, where you have advanced mode (i.e. use saved values from NVRAM to restore and manual reset).
 */
#if !defined(VID6608_RESET_ON_INIT)
  #define VID6608_RESET_ON_INIT  1
#endif

/**
 * @brief Defaine steps range
 *
 * Some drives can have another steps scale:
 * Common X27-168: 320° * 12 steps
 * Bi-Axial BKA30D-R5:
 * Inner: 320° * 12 steps
 * Outer: 275° * 12 steps
 *
 * Use defines VID6608_STEPS_X to configure steps range per-drive
 */

#if !defined(VID6608_STEPS_DEFAULT)
  #define VID6608_STEPS_DEFAULT 320 * 12
#endif

#if !defined(VID6608_STEPS_1)
  #define VID6608_STEPS_1 VID6608_STEPS_DEFAULT
#endif

#if !defined(VID6608_STEPS_2)
  #define VID6608_STEPS_2 VID6608_STEPS_DEFAULT
#endif

#if !defined(VID6608_STEPS_3)
  #define VID6608_STEPS_3 VID6608_STEPS_DEFAULT
#endif

#if !defined(VID6608_STEPS_4)
  #define VID6608_STEPS_4 VID6608_STEPS_DEFAULT
#endif

#if VID6608_RMT
  #include "esp32_vid6608_rmt.h"
  #define VID6608_CLASS esp32_vid6608_rmt
#else
  #include "vid6608.h"
  #define VID6608_CLASS vid6608
#endif

/**
 * @brief Command definition
 */
#define D_PRFX_GAUGE "Gauge"
#define D_CMND_GAUGE_SET "Set"
#define D_CMND_GAUGE_PERCENT "Percent"
#define D_CMND_GAUGE_ZERO "Zero"

const char kGaugeCommands[] PROGMEM = D_PRFX_GAUGE "|"  // Prefix
  "|" D_CMND_GAUGE_SET "|" D_CMND_GAUGE_PERCENT "|"
  D_CMND_GAUGE_ZERO
  ;

void (* const GaugeCommand[])(void) PROGMEM = {
  &CmndGauge, &CmndGaugeSet, &CmndGaugePercent,
  &CmndGaugeZero,
};

enum GaugeInternalCommand {
  GAUGE_ZERO,
  GAUGE_SET,
  GAUGE_SET_PERCENT
};

/**
 * @brief Global vars
 */
bool vid6608Present = false;
VID6608_CLASS *vid6608Drives[VID6608_MAX_DRIVES];

#if defined(VID6608_RTOS)
  /**
   * @brief Mutex for RTOS precision timing
   *
   * We have to use "real" trheads under FreeRTOS, as precision timing is required
   * for stepper motor driving. This mutex protects data access from multiple
   * threads simultaniously. Else it will lead to stepper motor glitches and random move.
   *
   */
  SemaphoreHandle_t vid6608Mutex;
  // Macro for mutexs take/give
  #define VID6608_MUTEX_TAKE   xSemaphoreTake(vid6608Mutex, portMAX_DELAY);
  #define VID6608_MUTEX_GIVE   xSemaphoreGive(vid6608Mutex);
#else
  #define VID6608_MUTEX_TAKE
  #define VID6608_MUTEX_GIVE
#endif

const uint16_t vid6608MaxSteps[VID6608_MAX_DRIVES] PROGMEM = {
    VID6608_STEPS_1,
    VID6608_STEPS_2,
    VID6608_STEPS_3,
    VID6608_STEPS_4,
};

/**
 * @brief Command Gauge
 * Displays currnt state for all Gauge drives
 */
void CmndGauge(void) {
  Response_P(PSTR("{"));
  VID6608StatusJson();
  ResponseJsonEnd();
}

/**
 * @brief Command GaugeSet, GaugeSet0 and GaugeSetX
 */
void CmndGaugeSet(void) {
  CmndGaugeCommand(GAUGE_SET, XdrvMailbox.index, XdrvMailbox.payload);
}

/**
 * @brief Command GaugePercent, GaugePercent0 and GaugePercentX
 */
void CmndGaugePercent(void) {
  CmndGaugeCommand(GAUGE_SET_PERCENT, XdrvMailbox.index, XdrvMailbox.payload);
}

/**
 * @brief Command GaugeZero, GaugeZero0 and GaugeZeroX
 */
void CmndGaugeZero(void) {
  CmndGaugeCommand(GAUGE_ZERO, XdrvMailbox.index, XdrvMailbox.payload);
}

/**
 * @brief Driver common command function
 *
 * @param command what to issue
 * @param index drive number (0 - all, 1..4 - by number)
 * @param payload command argument
 */
void CmndGaugeCommand(int32_t command, uint32_t index, int32_t payload) {
  VID6608_MUTEX_TAKE
  Response_P(PSTR("{\"" D_PRFX_GAUGE "\":{"));
  bool isFirstItem = true;
  for (uint8_t x = 0; x < VID6608_MAX_DRIVES; x++) {
    if (index == 0 || index == (x+1)) {
      VID6608_CLASS *driver = vid6608Drives[x];
      if (driver) {
        if (!isFirstItem) {
          ResponseAppend_P(PSTR(","));
        }
        ResponseAppend_P(PSTR("\"%d\":{"), (int32_t)(x+1));
        switch (command) {
          case GAUGE_ZERO:
            driver->zero(payload);
            ResponseAppend_P(PSTR("\"cmd\":\"zero\",\"pos\":0"));
            break;
          case GAUGE_SET:
            driver->moveTo(payload);
            ResponseAppend_P(PSTR("\"cmd\":\"set\",\"pos\":%d"), payload);
            break;
          case GAUGE_SET_PERCENT:
            uint16_t maxSteps = 0;
            memcpy_P(&maxSteps, &vid6608MaxSteps[x], 2);
            float moveSteps = (float)maxSteps * ( (float)payload / 100.0 );
            driver->moveTo(moveSteps);
            ResponseAppend_P(PSTR("\"cmd\":\"perc\",\"perc\":%d,\"pos\":%d"), payload, (int32_t)moveSteps);
            break;
        }
        ResponseAppend_P(PSTR("}"), x+1);
        isFirstItem = false;
      }
    }
  }
  ResponseAppend_P(PSTR("}}"));
  VID6608_MUTEX_GIVE
}

/**
 * @brief Function to display current Gauge state in JSON format
 * Prefix (i.e. "," or "{") should be added externally
 */
void VID6608StatusJson() {
  ResponseAppend_P(PSTR("\"" D_PRFX_GAUGE "\":{"));
  bool isFirstItem = true;
  VID6608_MUTEX_TAKE
  for (uint8_t x = 0; x < VID6608_MAX_DRIVES; x++) {
    VID6608_CLASS *driver = vid6608Drives[x];
    if (driver) {
      if (!isFirstItem) {
        ResponseAppend_P(PSTR(","));
      }
      ResponseAppend_P(PSTR("\"%d\":{\"pos\":%d}"), (int32_t)(x+1), (int32_t)driver->getPosition());
      isFirstItem = false;
    }
  }
  VID6608_MUTEX_GIVE
  ResponseJsonEnd();
}

#ifdef USE_WEBSERVER
/**
 * @brief Function to display current Gauge state in HTML format
 * Displayes HTML table for web browser
 */
void VID6608StatusWeb() {
  WSContentSend_PD(HTTP_TABLE100);
  VID6608_MUTEX_TAKE
  for (uint8_t x = 0; x < VID6608_MAX_DRIVES; x++) {
    VID6608_CLASS *driver = vid6608Drives[x];
    if (driver) {
      WSContentSend_PD(PSTR("<tr><th>Gauge %d</th><td>%d</td></tr>"), (int32_t)(x+1), (int32_t)driver->getPosition());
    }
  }
  VID6608_MUTEX_GIVE
  WSContentSend_PD(PSTR("</table>"));
}
#endif

#if defined(VID6608_RTOS)
/**
 * @brief FreeRTOS background process function
 * This function is required to handle movement with precision timings.
 * Used in ESP-32 only, the ESP8266 uses classical loop() thread.
 */
void VID6608XvTask(void *) {
  while(true) {
    bool needToMove = false;
    VID6608_MUTEX_TAKE
    for (uint8_t x = 0; x < VID6608_MAX_DRIVES; x++) {
      VID6608_CLASS *driver = vid6608Drives[x];
      if (driver) {
        driver->loop();
        if (driver->isMoving()) {
          needToMove = true;
        }
      }
    }
    VID6608_MUTEX_GIVE
    /*
      If we dont need to move any -> go sleep.
      This will delay next move begin up to 500ms, but freeds up CPU a lot.
    */
    if (!needToMove) {
      vTaskDelay(500 / portTICK_PERIOD_MS);
    }
  }
}
#endif // VID6608_RTOS

/**
 * @brief Driver initalisation
 * Checks the defined pins and creates Drives objects, if pins found.
 */
void VID6608Init() {
  AddLog(LOG_LEVEL_INFO, PSTR("VID: Init"));
  for (uint32_t x = 0; x < VID6608_MAX_DRIVES; x++) {
    if (PinUsed(GPIO_VID6608_F, x) && PinUsed(GPIO_VID6608_CW, x)) {
      // We have motor defined at number x
      uint32_t pinStep = Pin(GPIO_VID6608_F, x);
      uint32_t pinDir = Pin(GPIO_VID6608_CW, x);
      uint16_t maxSteps = 0;
      memcpy_P(&maxSteps, &vid6608MaxSteps[x], 2);
      AddLog(LOG_LEVEL_DEBUG, PSTR("VID: Detected drive %d at pin %d, %d, steps %d"), x, pinStep, pinDir, (uint32_t)maxSteps);
      #if VID6608_RMT
        AddLog(LOG_LEVEL_DEBUG, PSTR("VID: Using hardware RMT driver"));
        esp32_vid6608_rmt::Config cfg;
        cfg.stepPin = (gpio_num_t)pinStep;
        cfg.dirPin = (gpio_num_t)pinDir;
        cfg.maxSteps = maxSteps;
        vid6608Drives[x] = new esp32_vid6608_rmt(cfg);
      #else
        AddLog(LOG_LEVEL_DEBUG, PSTR("VID: Using software driver"));
        vid6608Drives[x] = new vid6608(pinStep, pinDir, maxSteps);
      #endif

      // Perform homing operation
      if (VID6608_RESET_ON_INIT) {
        vid6608Drives[x]->zero();
        AddLog(LOG_LEVEL_DEBUG, PSTR("VID: Zero %d done"), x);
      } else {
        AddLog(LOG_LEVEL_DEBUG, PSTR("VID: Zero %d skipped"), x);
      }
      vid6608Present = true;
    } else {
      vid6608Drives[x] = nullptr;
    }
  }
  // If no drives present -> skip rest of initalization
  if (!vid6608Present) {
    return;
  }
#if defined(VID6608_RTOS)
  AddLog(LOG_LEVEL_DEBUG, PSTR("VID: Using FreeRTOS mode"));
  // Create mutex for RTOS thread safety
  vid6608Mutex = xSemaphoreCreateMutex();
  // Start background RTOS thread -> required for precision timing
  xTaskCreate(
    VID6608XvTask,                /* Function to implement the task */
    "VID6608XvTask",              /* Name of the task */
    1024,                         /* Stack size in words */
    NULL,                         /* Task input parameter */
    0,                            /* Priority of the task, lowest */
    NULL                          /* Task handle. */
  );
#endif // VID6608_RTOS
}

// Classical loop implementation
#if !defined(VID6608_RTOS) && !VID6608_RMT
/**
 * @brief Non-FreeRTOS background process function
 * ESP8266 classical loop() thread function, used where is no FreeRTOS.
 */
bool VID6608Loop() {
  for (uint8_t x = 0; x < VID6608_MAX_DRIVES; x++) {
    VID6608_CLASS *driver = vid6608Drives[x];
    if (driver) {
      driver->loop();
    }
  }
  return true;
}
#endif // VID6608_RTOS

/*********************************************************************************************\
 * Interface
\*********************************************************************************************/

bool Xdrv92(uint32_t function) {
  // Driver initalization
  if (FUNC_INIT == function) {
    VID6608Init();
    return false;
  }
  // We are not initilized?
  if (!vid6608Present) {
    return false;
  }
  // Normal callbacks
  bool result = false;

  switch (function) {
    case FUNC_LOOP:
#if !defined(VID6608_RTOS) && !VID6608_RMT
      // 1. ESP32 uses FreeRTOS to manage moving tasks, as it requires precision timings
      // Others should use regular loop -> slower, but still works
      // 2. ESP32 RMT version does not need loop
      result = VID6608Loop();
#else
      result = true;
#endif
      break;
    case FUNC_COMMAND:
      result = DecodeCommand(kGaugeCommands, GaugeCommand);
      break;
    case FUNC_JSON_APPEND:
      ResponseAppend_P(PSTR(","));
      VID6608StatusJson();
      result = true;
      break;
#ifdef USE_WEBSERVER
    case FUNC_WEB_SENSOR:
      VID6608StatusWeb();
      result = true;
      break;
#endif // USE_WEBSERVER
    case FUNC_ACTIVE:
      result = true;
      break;
  }

  return result;
}

#endif  // USE_VID6608
