/******************************************************************************
 * @file        MODBUS.c
 * @brief       Modbus RTU协议通信处理模块
 * @details     本文件实现了基于Modbus RTU协议的RS485通信功能，包括：
 *              - Modbus命令帧的构建和发送
 *              - 接收数据的解析和处理
 *              - 实时数据轮询机制
 *              - 屏幕刷新命令处理
 *              - 保护定值、控制字等数据的更新
 * @author      阜阳师范大学物电学院
 * @version     V0.01
 * @date        2026-01-23
 * @note        通信协议：Modbus RTU
 *              通信接口：RS485
 *              主站地址：0x01
 ******************************************************************************/
#include "MODBUS.h"
#include <string.h>
#include "rs485.h"
#include "CRC16.h"
#include "led.h"
#include "160160D.h"
#include "filehandle.h"
/* ============================================================================
 * 命令类型宏定义
 * ============================================================================ */
#define RT_CMD          (1)  /**< 实时数据刷新命令类型 */
#define KEY_CMD         (0)  /**< 按键事件命令类型 */

/* ============================================================================
 * 图片与 Flash 宏定义
 * ============================================================================ */
#define BMP_ADDR                (0x0803F000)  /**< BMP 在 Flash 中的存储地址（扇区 126） */
#define BMP_LEN                 (3208)        /**< BMP 数据长度（字节） */

/* ============================================================================
 * 轮询与超时常量
 * ============================================================================ */
#define RS485_RT_COUNT_MAX      (55)   /**< 实时轮询计数上限，约 275ms（55×5ms） */
#define RS485_ACK_COUNT_MAX     (40)   /**< 应答超时计数上限，约 200ms（40×5ms） */
#define RS485_ACK_OVERTIME_MAX  (5)    /**< 连续应答超时次数上限，超过则重初始化 RS485 */

/* ============================================================================
 * 数据结构定义
 * ============================================================================ */

/**
 * @brief  LOGO 图片数据结构体
 * @note   用于存储从 138 端下发的 BMP 数据及尺寸，或从 Flash 读取的 LOGO
 */
typedef struct
{
    uint8_t  bmpdata[3200];   /**< BMP 点阵数据，最大 3200 字节 */
    uint32_t biWidth;         /**< 图片宽度（像素） */
    uint32_t biHeight;        /**< 图片高度（像素） */
} logo_type;

/**
 * @brief  RS485 轮询与应答状态结构体
 * @note   用于 Modbus 主站轮询、应答超时检测及命令应答标志管理
 */
typedef struct
{
    volatile uint8_t  RT_count;        /**< 实时命令计数，用于定时发送刷新命令 */
    volatile uint16_t CMD_ACK;         /**< 命令接收完成标志（SET=可发下一帧） */
    volatile uint8_t  ACK_COUNT_ABLE;  /**< 应答计时使能（发令后置位） */
    volatile uint8_t  ACK_count;       /**< 应答等待计数，超 RS485_ACK_COUNT_MAX 即超时 */
    volatile uint8_t  ACK_OverTime;    /**< 本次应答超时标志 */
    volatile uint8_t  ACK_OverTimeCnt; /**< 连续应答超时次数，超 RS485_ACK_OVERTIME_MAX 则重初始 */
} RS485_POLL_TYPE;

RS485_POLL_TYPE RS485POLL = {0};  /**< 全局轮询状态 */

/* ============================================================================
 * 私有变量定义
 * ============================================================================ */
static logo_type logo = {0};           /**< LOGO 图片数据（BMP + 宽高） */
static uint8_t Picture[3200] = {0};    /**< 接收图片缓冲，前 3200 字节为点阵数据 */

/* ============================================================================
 * Modbus 命令与屏幕刷新
 * ============================================================================ */
/**
 * @brief  发送屏幕刷新命令（特殊格式的 Modbus 命令）
 * @param  func 功能码（通常为读寄存器 READ_REGISTER = 0x03）
 * @param  type 命令类型：RT_CMD(1)=实时刷新，KEY_CMD(0)=按键事件
 * @param  key 按键值（当type=KEY_CMD时有效）
 * @note   命令帧格式（6字节）：
 *          [0] = 从站地址（Master_Address = 0x01）
 *          [1] = 功能码（func）
 *          [2] = 命令类型（type）
 *          [3] = 按键值（key）
 *          [4] = CRC校验高字节
 *          [5] = CRC校验低字节
 *          CRC校验范围：前4字节
 *          发送后重置命令应答标志
 * @retval 无
 */
void RS485_RefreshScreenCMD(uint8_t func, uint8_t type, uint8_t key)
{
    uint16_t crc = 0;
    uint8_t cmdbuf[6];   
    cmdbuf[0] = 0x01;                        /* 从站地址 */
    cmdbuf[1] = func;                        /* 功能码 */
    cmdbuf[2] = type;                        /* 命令类型 */
    cmdbuf[3] = key;                         /* 按键值 */
    crc = Calculate_CRC(&cmdbuf[0],4);       /* 计算CRC校验（前4字节） */
    cmdbuf[4] = crc >> 8;                    /* CRC校验高字节 */
    cmdbuf[5] = (uint8_t) crc;               /* CRC校验低字节 */

    RS485_SendBuff(cmdbuf, 6);               /* 发送6字节命令帧 */
    RS485POLL.CMD_ACK = RESET;               /* 重置命令应答标志 */
}


/**
 * @brief  发送屏幕刷新命令并重置轮询状态
 * @param  type 命令类型：RT_CMD(1)=实时刷新，KEY_CMD(0)=按键事件
 * @param  key  按键值（type=KEY_CMD 时有效）
 * @note   向 138 端发送屏幕刷新命令；发送后重置 CMD_ACK、RT_count，
 *         使能 ACK_COUNT_ABLE 并清零 ACK_count。
 * @retval 无
 */
void RefreshScreen(uint8_t type, uint8_t key)
{ 
    RS485_RefreshScreenCMD(0x03, type, key);  /* 发送刷新屏幕命令 */

    RS485POLL.CMD_ACK = RESET;            /* 重置命令响应标志 */
    RS485POLL.RT_count = 0;               /* 重置实时召唤计数 */

    RS485POLL.ACK_COUNT_ABLE = SET;       /* 使能应答计数 */
    RS485POLL.ACK_count = 0;              /* 重置应答计数值 */
}

/* ============================================================================
 * LOGO 与 BMP 处理
 * ============================================================================ */

/**
 * @brief  在 LCD 屏幕上显示 LOGO 图片
 * @note   调用Display_BMP函数将全局变量logo中的BMP图片数据显示在LCD屏幕上
 *          显示位置：屏幕居中（由Display_BMP函数自动计算）
 *          图片尺寸：使用logo结构体中的biWidth和biHeight
 *          图片数据：使用logo结构体中的bmpdata数组
 *          注意：显示前需要确保logo数据已通过BMP_Get()从Flash读取或已初始化
 * @retval 无
 */
void LOGO_Printf(void)
{
    /* 调用Display_BMP函数显示LOGO图片 */
    /* 参数：宽度、高度、图片数据指针 */
    Display_BMP(logo.biWidth, logo.biHeight, (const uint8_t*)logo.bmpdata);
}
/**
 * @brief  保存LOGO的BMP图片信息到Flash
 * @note   将全局变量logo中的BMP图片数据保存到Flash的指定地址（BMP_ADDR）
 *          保存的数据包括：
 *          - BMP图片数据（bmpdata数组，3200字节）
 *          - 图片宽度（biWidth）
 *          - 图片高度（biHeight）
 *          注意：保存前Flash相关扇区会被自动擦除
 *          保存地址：0x0803F000（扇区126）
 * @retval 无
 */
void BMP_SAVE2False(void)
{
    /* 调用Flash写入函数，将logo结构体数据写入Flash */
    /* sizeof(logo)/2 将字节数转换为半字（16位）个数 */
    FLASH_WriteMoreData(BMP_ADDR, (uint16_t*)(&logo), (sizeof(logo)/2));
}
/**
 * @brief  从Flash中读取LOGO的BMP图片数据
 * @note   从Flash的指定地址（BMP_ADDR）读取BMP图片数据到全局变量logo中
 *          读取的数据包括：
 *          - BMP图片数据（bmpdata数组，3200字节）
 *          - 图片宽度（biWidth）
 *          - 图片高度（biHeight）
 *          读取地址：0x0803F000（扇区126）
 *          注意：使用volatile指针确保从Flash直接读取，避免缓存问题
 * @retval 无
 */
void BMP_Get(void)
{
    uint32_t i = 0;
    /* 将Flash地址转换为 volatile 指针，确保直接从Flash读取数据 */
    volatile logo_type* ptr = (volatile logo_type*)BMP_ADDR;

    /* 循环读取BMP图片数据（3200字节） */
    for(i = 0; i < 3200; i++)
    {
        logo.bmpdata[i] = ptr->bmpdata[i];
    }
    /* 读取图片宽度 */
    logo.biWidth = ptr->biWidth;
    /* 读取图片高度 */
    logo.biHeight = ptr->biHeight;
}

/**
 * @brief  从 Flash 读取 LOGO 并显示，延时约 2 秒
 * @note   调用 BMP_Get 从 Flash 读取 LOGO，再 LOGO_Printf 显示；
 *         每次显示后 delay 4×500ms = 2s
 * @retval 无
 */
void NL_LOGO_Printf(void)
{
    BMP_Get();
    LOGO_Printf();
    delay_ms(500);
    delay_ms(500);
    delay_ms(500);
    delay_ms(500);
}

/* ============================================================================
 * Modbus 主处理与轮询
 * ============================================================================ */

/**
 * @brief  Modbus 通信主处理函数（远程处理图片）
 * @param  key 按键值（KEY_TYPE类型）
 * @note   采用138端处理图片，整体传上来显示的方式刷图
 *          处理流程：
 *          1. 按键处理：
 *             - 保存按键值到静态变量key_bk
 *             - 等待可发送状态时发送按键命令
 *          2. 接收新消息处理：
 *             - 设置连接标志
 *             - CRC校验通过：
 *               * 提取图片数据（3200字节）和命令信息（8字节）
 *               * 判断命令类型：
 *                 - 0x89 0x45：LOGO图片，保存并显示
 *                 - 其他：普通图片，显示图片和LED状态
 *             - CRC校验失败：
 *               * 累计错误计数
 *               * 连续3次错误后重启DMA接收
 *             - 清除控制字和应答相关标志
 *          3. 定时刷新处理：
 *             - 实时数据计数超限时发送实时刷新命令
 *          4. 应答超时处理：
 *             - 应答计数超限时设置超时标志
 *             - 连续超时次数达到上限时重启DMA接收
 *   数据帧长度:
 *       3000          8
 *   └──────┬──────┘ └─┬─┘
 *       Picture      info      
 * info 组成：
 *   info[0]info[1] info[2]info[3] info[4]info[5] 
 *   └──────┬─────┘ └───────┬────┘ └───────┬────┘ 
 *      图片宽度        图片高度    图片类型标识    
 *   info[0]info[1]info[2]info[3] 
 *   └────────────┬─────────────┘ 
 *           LED 灯状态信息  
 *  图片类型: 
 *           1. info[4] = 0x89 info[5] = 0x45  logo 图片
 *           2. 其他图片
 * @retval 无
 */
uint8_t RS485_Process(KEY_TYPE key, uint8_t flag)
{
    static KEY_TYPE key_bk = KEY_NONE;    /* 按键备份（静态变量，保持状态） */
    static uint8_t LED_BUFF[4];          /* LED状态缓冲区（静态变量） */
    static uint8_t info[8];               /* 命令信息缓冲区（静态变量） */
    static uint32_t CRC_ERR_COUNT = 0;     /* CRC错误计数（静态变量） */
    uint8_t ConnectFlg = flag;
  
    /* 按键处理：保存按键值 */
    if(key != KEY_NONE)
    {
        if(key_bk != key)                   /* 按键值变化时更新备份 */
        {
            key_bk = key;
        }
    }

    if(RS485REG.NewMessageFlag)           /* 接收到新消息 */
    {
        RS485REG.NewMessageFlag = RESET;                 /* 清除控制字 */
        
        ConnectFlg = 1;                  /* 设置连接标志 */
        if(Check_CRC((uint8_t*)&RS485REG.DR[0], UART_RX_LEN) == TRUE)  /* CRC校验通过 */
        {
            CRC_ERR_COUNT = 0;                /* 重置CRC错误计数 */
            memcpy(Picture, (uint8_t*)RS485REG.DR, 3200);        /* 提取图片数据（前3200字节） */
            memcpy(info, (uint8_t*)RS485REG.DR + 3200, 8);        /* 提取命令信息（后8字节） */
          
            if(info[4] == 0x89 && info[5] == 0x45)      /* LOGO图片命令（特殊标识） */
            {
                memcpy(logo.bmpdata, Picture, 3200);    /* 复制图片到LOGO结构体 */
                logo.biWidth = info[0] << 8 | info[1];    /* 解析图片宽度（高字节左移8位 + 低字节） */
                logo.biHeight = info[2] << 8 | info[3];   /* 解析图片高度（高字节左移8位 + 低字节） */
                BMP_SAVE2False();                             /* 保存 BMP 图片到 Flash */

                /* 显示LOGO */
                BackLight_ON();                         /* 开启背光 */
                ClearScreen();                          /* 清屏 */
                LOGO_Printf();                          /* 显示LOGO */
                delay_ms(500);                          /* 延时500ms */
                delay_ms(500);                          /* 延时500ms */
                delay_ms(500);                          /* 延时500ms */
                delay_ms(500);                          /* 延时500ms（总共2秒） */
            }
            else                                        /* 普通图片命令 */
            {
                memcpy(LED_BUFF, (uint8_t*)RS485REG.DR + 3200, 4);  /* 提取LED状态（后4字节） */
                LED_Printf(LED_BUFF);                   /* 显示LED状态 */
                ScreenPrintf(Picture);                  /* 显示图片 */
            }
        }
        else                                    /* CRC校验失败 */
        {
            CRC_ERR_COUNT++;                       /* CRC错误计数递增 */
            if(CRC_ERR_COUNT >= 3)                   /* 连续3次CRC错误 */
            {
                /* 接收错位，重新初始化接收 */
                RS485_DMA_init();                     /* 重新初始化*/
                CRC_ERR_COUNT = 0;                    /* 重置CRC错误计数 */
            }
        }

        /* 清除控制字和应答相关标志 */
        RS485POLL.ACK_OverTime = RESET;           /* 重置应答超时标志 */
        RS485POLL.ACK_COUNT_ABLE = RESET;         /* 禁用应答计数 */
        RS485POLL.ACK_count = 0;                  /* 重置应答计数 */
        RS485POLL.ACK_OverTimeCnt = 0;            /* 重置应答超时计数 */
        RS485POLL.CMD_ACK = SET;                  /* 设置接收完成标志 */
    }
    if((RS485POLL.CMD_ACK)||(RS485POLL.ACK_OverTime)) /* 可以应答或者超时*/
    {
        /* 按键命令发送：有按键且处于可发送状态 */
        if(key_bk != KEY_NONE)                         /* 有按键 */
        {
            RefreshScreen(KEY_CMD, key_bk);           /* 发送按键刷新命令 */
            key_bk = KEY_NONE;                        /* 清除按键备份 */
        }
        /* 定时刷新：实时数据计数超限且处于可发送状态 */
        if(RS485POLL.RT_count > RS485_RT_COUNT_MAX)  /* 定时到*/
        {
            RefreshScreen(RT_CMD, key_bk);           /* 发送实时刷新命令 */
            key_bk = KEY_NONE;                        /* 清除按键备份 */
        }
    }
    
    /* 应答超时处理：应答计数超限 */
    if(RS485POLL.ACK_count > RS485_ACK_COUNT_MAX)  /* 应答超时 */
    {
        RS485POLL.ACK_OverTime = SET;             /* 设置应答超时标志 */
        RS485POLL.ACK_count = 0;                  /* 重置应答计数 */
        RS485POLL.ACK_OverTimeCnt++;              /* 应答超时计数递增 */
    }

    /* 连续超时处理：应答超时次数达到上限 */
    if((RS485POLL.ACK_OverTimeCnt >= RS485_ACK_OVERTIME_MAX))  /* 连续超时次数达到上限 */
    {
        ConnectFlg = 0;
        RS485POLL.ACK_OverTimeCnt = RS485_ACK_OVERTIME_MAX;  /* 限制最大计数（防止溢出） */
        RS485_DMA_init();                         /* 重新初始化RS485 */                     
        RS485POLL.CMD_ACK = SET;                  /* 设置命令应答标志（允许重新发送） */
    }
    return ConnectFlg;
}

/**
 * @brief  轮询计数处理（需在 5ms 周期定时器中调用）
 * @note   RT_count 递增，用于定时发实时刷新命令；若 ACK_COUNT_ABLE 置位，
 *         则 ACK_count 递增，用于应答超时判断。
 * @retval 无
 */
void Process_Count(void)
{
    RS485POLL.RT_count++;
    if(RS485POLL.ACK_COUNT_ABLE)
    {
        RS485POLL.ACK_count++;
    }
}

/**
 * @brief  Modbus 轮询与应答相关变量初始化
 * @note   清零 ACK_OverTime、ACK_count、ACK_OverTimeCnt，关闭 ACK_COUNT_ABLE，
 *         置位 CMD_ACK 表示可发送。上电或通信异常恢复后调用。
 * @retval 无
 */
void Process_Init(void)
{
    /* 清除超时与应答相关标志 */
    RS485POLL.ACK_OverTime = RESET;           /* 重置应答超时标志 */
    RS485POLL.ACK_COUNT_ABLE = RESET;         /* 禁用应答计数 */
    RS485POLL.ACK_count = 0;                  /* 重置应答计数 */
    RS485POLL.ACK_OverTimeCnt = 0;            /* 重置应答超时计数 */
    RS485POLL.CMD_ACK = SET;                  /* 设置接收完成标志 */
}
