Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom HMI Communication with grblHAL #186

Open
datdadev opened this issue Jul 27, 2024 · 26 comments
Open

Custom HMI Communication with grblHAL #186

datdadev opened this issue Jul 27, 2024 · 26 comments

Comments

@datdadev
Copy link

datdadev commented Jul 27, 2024

I am currently working on integrating an HMI (Human-Machine Interface) to set and get values through the MCU's custom register map. Specifically, I am looking to achieve the following:

  1. Set Values: Set parameters such as speed through the HMI to the MCU.
  2. Get Real-time Positions: Retrieve real-time X, Y, and Z positions from the MCU.
  3. G-code File Selection: Select G-code files from the HMI that are already saved in the MCU's flash memory. The HMI will choose a specific file, and the MCU will handle the G-code parsing and execution of the stepper motor commands.

Given that the G-code files are limited in number, I have opted to store them directly on the MCU. My goal is for the MCU to manage all the G-code parsing and execution internally once a file is selected via the HMI.

Summary of Requirements:

  • Implement a custom communication protocol to set and get values between the HMI and the MCU.
  • Allow the HMI to select G-code files stored on the MCU.
  • Ensure the MCU internally communicates with the grblHAL library to parse and run the selected G-code files.

Questions:

  1. How can I implement an external main function to handle this custom communication with the HMI?
  2. What would be the best procedure to integrate this with the grblHAL library to ensure smooth operation?
  3. Is this approach recommended, or should I consider using two MCUs: one for handling communication and another dedicated to running grblHAL as the controller? Alternatively, should I send the G-code files and set commands directly from the HMI to grblHAL, considering this project only needs to store a few G-code files?

Any guidance or solutions on how to implement this would be greatly appreciated. Thank you!

@terjeio
Copy link
Contributor

terjeio commented Jul 30, 2024

Implement a custom communication protocol to set and get values between the HMI and the MCU.

Why? The current protocol cannot be used?

  1. my_plugin_init() is a weak function, when implemented by custom code it will be called early in the startup sequence.
  2. See 1. above.
  3. The approach is ok as long as it does not add too much overhead. If the driver supports littlefs you can store the files using that, alternatively use a SD card or embed the files (requires recompiling/reflashing if they needs changing).

There are a number of example plugins that hooks into the core in order to provide custom functionality, take a look at them.

@datdadev
Copy link
Author

In main.c:

#include "main.h"
#include "driver.h"
#include "grbl/grbllib.h"

#include "Modbus.h"

static void SystemClock_Config (void);
static void MX_GPIO_Init (void);

/* EDITED */
UART_HandleTypeDef huart2;

/* USER CODE BEGIN PV */
uint8_t RxData[256];
uint8_t TxData[256];
extern uint16_t Holding_Registers_Database[50];
extern Coils_Database[25];
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,uint16_t Size){
	if (RxData[0]==SLAVE_ID){
		switch (RxData[1]){
		case 0x01:
			readCoils ();
			break;
		case 0x03:
			readHoldingRegs();
			break;
		case 0x04:
			readHoldingRegs();
			break;
		case 0x05:
			writeSingleCoil();
			break;
		case 0x06:
			writeSingleReg();
			break;
		case 15:
			writeMultiCoils();
			break;
		case 16:
			writeHoldingRegs();
			break;
		default:
			break;
		}
	}
	  HAL_UARTEx_ReceiveToIdle_IT(&huart2, RxData, 256);
}

int main (void)
{
    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();

    /* Configure the system clock */
    SystemClock_Config();

    /* Initialize all configured peripherals */
    MX_GPIO_Init();

    HAL_UARTEx_ReceiveToIdle_IT(&huart2, RxData, 256);

    if(!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) {
        CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
        DWT->CYCCNT = 0;
        DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    }

    grbl_enter();
}

In Src/my_plugin.c:

#include "driver.h"

static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;

// Define the register address and values
#define REGISTER_ADDRESS 0x3
#define START_COMMAND 0x05

// Define the base address and offset for the register
#define PERIPHERAL_BASE_ADDRESS  0x40000000  // FIX_ME
#define REGISTER_OFFSET          0x00000010  // FIX_ME

// Function to read register value
static uint8_t read_register(uint8_t reg_address) {
    volatile uint8_t* register_address;

    // Calculate the register address
    register_address = (volatile uint8_t*)(PERIPHERAL_BASE_ADDRESS + REGISTER_OFFSET + reg_address);

    // Read and return the value from the register
    return *register_address;
}

// Function to send a sequence of G-code commands
static void send_gcode_sequence(void) {
    hal.stream.write("G1 X10 Y10 F1000\n"); // Example G-code command
    hal.stream.write("G2 X20 Y20 I10 J10\n"); // Another example G-code command
}

// Check the register value and send G-code commands if needed
static void check_register_and_execute_gcode(sys_state_t state) {
    // Check the register value
    uint8_t reg_value = read_register(REGISTER_ADDRESS);

    if (reg_value == START_COMMAND) {
        send_gcode_sequence();
    }

    // Continue with any other real-time execution tasks
    on_execute_realtime(state);
}

// Add info about our plugin to the $I report.
static void on_report_my_options(bool newopt) {
    if (on_report_options) {
        on_report_options(newopt);
    }

    if (!newopt) {
        hal.stream.write("[PLUGIN:Register Monitor v1.00]" ASCII_EOL);
    }
}

void my_plugin_init(void) {
    // Add info about our plugin to the $I report.
    on_report_options = grbl.on_report_options;
    grbl.on_report_options = on_report_my_options;

    // Add check register and execute G-code function to grblHAL foreground process
    on_execute_realtime = grbl.on_execute_realtime;
    grbl.on_execute_realtime = check_register_and_execute_gcode;
}

I haven’t tested it yet, but it built successfully. I’m not sure if it’s correct. I am trying to implement a while loop to check the register for a signal to start drawing based on HMI commands that change the STM32 register value. This my_plugin_init will also check the register stage, run endlessly alongside the GRBL loop. Am I right?

@terjeio
Copy link
Contributor

terjeio commented Jul 30, 2024

Better to move your code in main.c to my_plugin.c.

This my_plugin_init will also check the register stage, run endlessly alongside the GRBL loop. Am I right?

Not really, check_register_and_execute_gcode() will.

FYI send_gcode_sequence() will output data to a connected sender, not deliver it to input processing. To do that redirect hal.stream.read to your own function that will read from a buffer containing the commands. The macros plugin does that.

@datdadev
Copy link
Author

I wonder if I can block the default UART2 connection of GRBL defined in my_machine_map.h to prevent overlapping data. Can I create a new UART2 initialization in my_plugin.c? Am I using normal initialization, or do I need a special function to initialize the GPIOs?

I have a question. I tried this and it didn’t work. What is the purpose of using SERIAL1_PORT?

#define SERIAL_PORT     1 // 2   // GPIOA: TX = 2, RX = 3
#define SERIAL1_PORT    2   // GPIOA: TX = 2, RX = 3

When debugging the variable RxData, using the Hercules app to send data through UART2, the value remained unchanged in the live expression. This indicates that the HAL_UARTEx_RxEventCallback function is not working or UART2 is not initialized properly. Am I right?

@datdadev
Copy link
Author

datdadev commented Jul 31, 2024

This is my latest modified my_plugin.c, and I can't figure out why the callback function is not being called when there is a signal on UART2:

#include "driver.h"
#include "Modbus.h"
#include <string.h>
#include <stdio.h>

static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;

// UART variables
UART_HandleTypeDef huart2;

uint8_t RxData[256];
uint8_t TxData[256];
extern uint16_t Holding_Registers_Database[50];
extern uint8_t Coils_Database[25];

// Buffer for storing G-code commands
#define GCODE_BUFFER_SIZE 256
static char gcode_buffer[GCODE_BUFFER_SIZE];
static uint16_t gcode_buffer_index = 0;
static uint16_t gcode_buffer_length = 0;

// G-code file strings
const char* gcode_file_1 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G1 X20 Y10 ; Draw line\n"
"G1 X20 Y20 ; Draw line\n"
"G1 X10 Y20 ; Draw line\n"
"G1 X10 Y10 ; Draw line\n"
"M2 ; End of program\n";

const char* gcode_file_2 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G2 X20 Y20 I10 J0 ; Draw clockwise arc\n"
"G2 X10 Y10 I-10 J0 ; Complete the circle\n"
"M2 ; End of program\n";

const char* gcode_file_3 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G1 X20 Y10 ; Draw line\n"
"G1 X10 Y20 ; Draw line\n"
"G1 X20 Y20 ; Draw line\n"
"G1 X10 Y30 ; Draw line\n"
"G1 X20 Y30 ; Draw line\n"
"M2 ; End of program\n";

// Function prototypes
static void send_gcode_sequence(int selected_index);
static int16_t get_macro_char(void);
void SystemClock_Config(void);
void MX_GPIO_Init(void);
void MX_USART2_UART_Init(void);

// UART callback function
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        if (RxData[0] == SLAVE_ID) {
            switch (RxData[1]) {
                case 0x01:
                    readCoils();
                    break;
                case 0x03:
                    readHoldingRegs();
                    break;
                case 0x04:
                    readHoldingRegs();
                    break;
                case 0x05:
                    writeSingleCoil();
                    break;
                case 0x06:
                    writeSingleReg();
                    break;
                case 15:
                    writeMultiCoils();
                    break;
                case 16:
                    writeHoldingRegs();
                    break;
                default:
                    break;
            }
        }
        HAL_UART_Receive_IT(&huart2, RxData, 256);
    }
}

// Function to send a sequence of G-code commands to the buffer
static void send_gcode_sequence(int selected_index) {
    const char* selected_gcode;

    // Select the appropriate G-code file
    switch (selected_index) {
        case 0:
            selected_gcode = gcode_file_1;
            break;
        case 1:
            selected_gcode = gcode_file_2;
            break;
        case 2:
            selected_gcode = gcode_file_3;
            break;
        default:
            return;  // Invalid index, do nothing
    }

    // Copy the selected G-code into the buffer
    strncpy(gcode_buffer, selected_gcode, GCODE_BUFFER_SIZE - 1);
    gcode_buffer[GCODE_BUFFER_SIZE - 1] = '\0';  // Ensure null-termination

    gcode_buffer_length = strlen(gcode_buffer);
    gcode_buffer_index = 0;

    // Redirect hal.stream.read to our buffer reader function
    hal.stream.read = get_macro_char;
    hal.stream.file = NULL;  // Input stream is not file based
}

// Function to read a character from the G-code buffer
static int16_t get_macro_char(void) {
    if (gcode_buffer_index < gcode_buffer_length) {
        return gcode_buffer[gcode_buffer_index++];
    } else {
        return 0;  // No more data
    }
}

// Check the Holding_Registers_Database and send G-code commands if needed
static void check_register_and_execute_gcode(sys_state_t state) {
    // Get data from Holding_Registers_Database
    uint16_t number_to_write = Holding_Registers_Database[10];
    uint16_t text_size = Holding_Registers_Database[12];

    // Check if the values are within the required range
    if (number_to_write < 0 || number_to_write > 9 || text_size < 1 || text_size > 10) {
        return;  // Values out of range, do nothing
    }

    // Select which G-code file to use based on some criteria
    int selected_index = 0; // Example: you might have a function to determine this

    send_gcode_sequence(selected_index);

    // Continue with any other real-time execution tasks
    on_execute_realtime(state);
}

// Add info about our plugin to the $I report
static void on_report_my_options(bool newopt) {
    if (on_report_options) {
        on_report_options(newopt);
    }

    if (!newopt) {
        hal.stream.write("[PLUGIN:Register Monitor v1.00]" ASCII_EOL);
    }
}

void my_plugin_init(void) {
    // Add info about our plugin to the $I report
    on_report_options = grbl.on_report_options;
    grbl.on_report_options = on_report_my_options;

    // Initialize UART2
    MX_USART2_UART_Init();

    // Start UART receive interrupt
    HAL_UART_Receive_IT(&huart2, RxData, 256);

    // Add check register and execute G-code function to grblHAL foreground process
    on_execute_realtime = grbl.on_execute_realtime;
    grbl.on_execute_realtime = check_register_and_execute_gcode;
}

void MX_USART2_UART_Init(void)
{
    huart2.Instance = USART2;
    huart2.Init.BaudRate = 115200;
    huart2.Init.WordLength = UART_WORDLENGTH_8B;
    huart2.Init.StopBits = UART_STOPBITS_1;
    huart2.Init.Parity = UART_PARITY_NONE;
    huart2.Init.Mode = UART_MODE_TX_RX;
    huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart2.Init.OverSampling = UART_OVERSAMPLING_16;
    if (HAL_UART_Init(&huart2) != HAL_OK)
    {
        // Initialization Error
        Error_Handler();
    }

    // Configure GPIOs for USART2
    MX_GPIO_Init();
}

void MX_GPIO_Init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // Configure GPIO pins for USART2 TX and RX
    GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

//void Error_Handler(void)
//{
//    // User can add their own implementation to report the HAL error return state
//    while (1)
//    {
//    }
//}

@datdadev
Copy link
Author

To clarify my current task, I aim to use the HMI as the host via UART2, but not for sending GRBL-based commands or parameter configurations. Instead, the HMI will modify specific registers inside the MCU based on button presses on the touch screen. My plan is to reassign the SERIAL_PORT in the map file to make space for UART2, allowing the HMI to adjust register values directly.

My first step is to implement my_plugin.c to read registers. Since I've disabled the UART SERIAL_PORT in the map file, I need to reinitialize it to handle interrupts within the my_plugin_init() function and create an interrupt callback to process data from the HMI. However, it seems this isn't working as expected, possibly due to the way GRBLHAL is structured.

If successful, I'll modify the Holding_Registers_Database variable. For this project, Holding_Registers_Database[10] represents the file number to select, and Holding_Registers_Database[12] is the scale factor for the G-code file. I will handle this function later.

I would greatly appreciate your guidance on this. Also, if you have time, please check out my forked GRBLHAL project here. Thank you!

@terjeio
Copy link
Contributor

terjeio commented Aug 1, 2024

get_macro_char() should return -1 ( or SERIAL_NO_DATA) when no data is available.

I am not familiar with the UART HAL, to me it seems the callback is not entered before the specified amount of data is received. Are your modbus messages 256 bytes long?

FYI you may use the grblHAL serial API instead of the ST UART HAL - see the client modbus code.

@datdadev
Copy link
Author

datdadev commented Aug 1, 2024

For my test project, I initially created the project in STM32CubeIDE with the selected STM32F407VET6 MCU. I copied these two Modbus.c and Modbus.h files for Modbus RTU communication, then initialized UART2 for interrupts. I used these files in main.c without making any modifications or additions. It worked as expected; the interrupt callback executed when sending just one byte of data through Hercules.

For the grblHAL serial API, could you write me an example for doing this kind of task? The code you linked seems quite overwhelming for using another Modbus library for the spindle.

To clarify my current task, I aim to use the HMI as the host via UART2, but not for sending GRBL-based commands or parameter configurations. Instead, the HMI will modify specific registers inside the MCU based on button presses on the touch screen.

@terjeio
Copy link
Contributor

terjeio commented Aug 1, 2024

It worked as expected; the interrupt callback executed when sending just one byte of data through Hercules.

Odd, the callback is only called when all bytes has been received if I read the code correctly.

For the grblHAL serial API, could you write me an example for doing this kind of task?

You only need the parts that claims the stream. Poll for input calling stream.get_tx_buffer_count() until a complete message has been received, then read it into the local rx buffer with stream.read() before submitting it for processing. Use stream.write() to send a message.

Note that if using the STM HAL you should typically not submit received messages for processing in the callback since this is called from an interrupt context. To avoid that post it for processing via task_add_immediate() instead, this ensures it will be handled by the foreground process.

@datdadev
Copy link
Author

datdadev commented Aug 2, 2024

I’ve implemented a basic data transmission and reception system with a 256-byte buffer, as you suggested. The idea is that data received via UART should be stored sequentially starting from the beginning of RxData. However, I'm experiencing issues where data is inconsistently received. When data is received, some bytes are occasionally lost, and the order of the bytes can become scrambled. Could you help me review this?

#include "driver.h"
#include <string.h>
#include <stdio.h>

static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;

#define RX_DATA_SIZE 256
static uint8_t RxData[RX_DATA_SIZE];
static uint16_t rx_index = 0;

// Function prototypes
static void poll_for_received_data(void);

// Function to poll for received data, store it in RxData, and write it back
static void poll_for_received_data(void) {
    int16_t c;

    // Read data from the stream
    while ((c = hal.stream.read()) != -1) {
        if (rx_index < RX_DATA_SIZE) {
            RxData[rx_index++] = (uint8_t)c;  // Store received character in RxData
        } else {
            rx_index = 0;
        }
        hal.stream.write((uint8_t)c);      // Echo the received character
        hal.stream.write(ASCII_EOL);      // Write ASCII end-of-line character
    }
}

// Check the Holding_Registers_Database and send G-code commands if needed
static void check_register_and_execute_gcode(sys_state_t state) {
    // Poll for and process received data
    poll_for_received_data();

    // Continue with any other real-time execution tasks
    on_execute_realtime(state);
}

// Add info about our plugin to the $I report
static void on_report_my_options(bool newopt) {
    if (on_report_options) {
        on_report_options(newopt);
    }

    if (!newopt) {
        // Add ASCII_EOL after the plugin information string
        const char plugin_info[] = "[PLUGIN:Register Monitor v1.00]" ASCII_EOL;
        hal.stream.write((uint8_t*)plugin_info);
    }
}

void my_plugin_init(void) {
    // Add info about our plugin to the $I report
    on_report_options = grbl.on_report_options;
    grbl.on_report_options = on_report_my_options;

    // Add check register and execute G-code function to grblHAL foreground process
    on_execute_realtime = grbl.on_execute_realtime;
    grbl.on_execute_realtime = check_register_and_execute_gcode;
}

@terjeio
Copy link
Contributor

terjeio commented Aug 2, 2024

You should wait until a complete message is received before reading:

static void poll_for_received_data(void) {
    int16_t c;

    // Read data from the stream
  if(hal.stream.get_rx_buffer_count() == RX_DATA_SIZE) {
    while ((c = hal.stream.read()) != -1) {
        if (rx_index < RX_DATA_SIZE) {
            RxData[rx_index++] = (uint8_t)c;  // Store received character in RxData
        } else {
            rx_index = 0;
        }
        hal.stream.write((uint8_t)c);      // Echo the received character
        hal.stream.write(ASCII_EOL);      // Write ASCII end-of-line character
    }
  }
}

If variable length messages then read enough to parse the length then wait for the rest.

Note that the default serial stream is also read by the main protocol loop, it will be random who gets the data. You need to read from another stream by claiming one like how it is done in the modbus_rtu code. Or if you know which stream instance to use you can claim it explicitly by calling stream_open_instance(), this returns a pointer to the functions used to access it. An example can be found in this Trinamic UART driver code (do not disable rx though). Replace TRINAMIC_STREAM with the stream instance - likely 1 since the default stream is UART based.

Looking at the modbus code I see that there is no CRC check of the received data, only range checks. If reception goes out of sync you should flush the input buffer before reading the next message - possibly after a short delay.

@datdadev
Copy link
Author

datdadev commented Aug 6, 2024

After trying for a while without success, I tested the method you suggested, and while it works, it still doesn't meet my needs for a custom receive interrupt. Please note that the project involves a single communication stream with the HMI.

To clarify, the simple program in main.c uses HAL_UARTEx_ReceiveToIdle_IT(&huart2, RxData, 256); in idle line mode for data reception. This mode allows me to gather sufficient data but still requires further handling, which is more convenient for communication with the HMI. After receiving data in the interrupt callback, the functions for reading and writing coils and holding registers are defined in Modbus.c and Modbus.h, using extern UART_HandleTypeDef huart2; for communication, primarily to send data to the HMI. These files are quite straightforward for communication purposes, so please take a look at them.

I want to implement this feature and am unsure if it's acceptable or possible to write it in my_plugin.c. I really need your assistance with this.

@terjeio
Copy link
Contributor

terjeio commented Aug 7, 2024

it still doesn't meet my needs for a custom receive interrupt.

What are your needs that is not met?

I want to implement this feature and am unsure if it's acceptable or possible to write it in my_plugin.c

All your code can be placed in my_plugin.c (and by adding other files as needed), you can then easily upgrade to newer releases without applying modifications to the ones from the repo.

@datdadev
Copy link
Author

datdadev commented Aug 7, 2024

You mentioned that it was possible to place all the code from the three files I provided above, similar to what I attempted a week ago as referenced here. However, despite numerous attempts, I have not achieved the desired results. You also advised against using the ST HAL library for UART, but suggested that all my code could be placed in my_plugin.c, which seems quite unusual to me.

@datdadev
Copy link
Author

datdadev commented Aug 9, 2024

I think the code I'm developing is receiving characters correctly, but not as hex values. I suspect that hal.stream.read() is only reading ASCII characters.

The current code
#include "driver.h"
#include <string.h>
#include <stdio.h>

#include "Modbus.h"

static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;

#define RX_DATA_SIZE 16
uint8_t RxData[RX_DATA_SIZE];
static uint16_t rx_index = 0;

uint8_t TxData[RX_DATA_SIZE];

// Function prototypes
static void poll_for_received_data(void);
static void flush_input_buffer(void);
static void modbus_proces(void);

static void modbus_process(void) {
	if (RxData[0]==SLAVE_ID) {
		if (RxData[1] == 0x03 || RxData[1] == 0x04) {
			readHoldingRegs();
		}
	}
}

// Function to flush the input buffer
static void flush_input_buffer(void) {
    int16_t c;
    // Read and discard all data from the stream until the buffer is empty
    while ((c = hal.stream.read()) != -1) {
        // Discard data
    }
}

// Function to poll for received data, store it in RxData, and write it back
static void poll_for_received_data(void) {
    int16_t c;
    uint16_t rx_buffer_count;
    uint32_t idle_start_time;
    const uint32_t IDLE_TIMEOUT_MS = 1000;  // Timeout for idle detection in milliseconds

    // Initialize the start time for idle detection
    idle_start_time = HAL_GetTick();  // Assuming HAL_GetTick() returns milliseconds since system start

    while (1) {
        rx_buffer_count = hal.stream.get_rx_buffer_count();

        // Check if there is data in the buffer
        if (rx_buffer_count > 0) {
            // Reset the idle timer
            idle_start_time = HAL_GetTick();

            // Reset index for new message
            rx_index = 0;

            // Read data from the stream
            while (rx_buffer_count > 0 && rx_index < RX_DATA_SIZE) {
                if ((c = hal.stream.read()) != -1) {
                    RxData[rx_index++] = c;  // Store received character in RxData
                    rx_buffer_count--;
                }
            }

            while (rx_index < RX_DATA_SIZE) {
				RxData[rx_index++] = 0x00;
			}

            modbus_process();
        } else {
            // Check for idle timeout
            if ((HAL_GetTick() - idle_start_time) > IDLE_TIMEOUT_MS) {

                break;
            }
        }

        // Delay before next check
        HAL_Delay(10);  // Adjust delay as necessary
    }
}

// Check the Holding_Registers_Database and send G-code commands if needed
static void check_register_and_execute_gcode(sys_state_t state) {
    // Poll for and process received data
    poll_for_received_data();

    // Continue with any other real-time execution tasks
    on_execute_realtime(state);
}

// Add info about our plugin to the $I report
static void on_report_my_options(bool newopt) {
    if (on_report_options) {
        on_report_options(newopt);
    }

    if (!newopt) {
        // Add ASCII_EOL after the plugin information string
        const char plugin_info[] = "[PLUGIN:Register Monitor v1.00]" ASCII_EOL;
        hal.stream.write((uint8_t*)plugin_info);
    }
}

void my_plugin_init(void) {
    // Add info about our plugin to the $I report
    on_report_options = grbl.on_report_options;
    grbl.on_report_options = on_report_my_options;

    // Add check register and execute G-code function to grblHAL foreground process
    on_execute_realtime = grbl.on_execute_realtime;
    grbl.on_execute_realtime = check_register_and_execute_gcode;
}

@terjeio
Copy link
Contributor

terjeio commented Aug 9, 2024

hal.stream.read() returns int16, -1 if nothing was read else an 8-bit character (byte).
By default most control characters and top bit set characters are stripped on reception, some are sent to the real-time command handler. If they are needed a function that keeps them has to be registered with the stream, e.g. like this:

my_stream->set_enqueue_rt_handler(stream_buffer_all);

@datdadev
Copy link
Author

datdadev commented Aug 9, 2024

Oh, it is working well. However, when transmitting back those same received hex bytes using hal.stream.write(), do I need to add a function to send the hex data instead of ASCII characters?

@terjeio
Copy link
Contributor

terjeio commented Aug 9, 2024

do I need to add a function to send the hex data instead of ASCII characters?

If you need each hex data (byte) to be encoded by sending multiple ASCII characters per byte then yes.
FYI the stream is 8-bit and an 8-bit char is typically represented in the same way as a byte (int8_t or uint8_t) and thus interchangeable in code:

typedef signed char __int8_t ;
typedef unsigned char __uint8_t ;

@datdadev
Copy link
Author

datdadev commented Aug 9, 2024

I am not converting those hex values to ASCII before sending; I want to send the raw hex values back to the host (in this case, the HMI is the master and the microcontroller is the slave).

This is the code that processes RxData and prepares TxData for sending. However, I am encountering a problem where TxData contains the data shown in Image 1. When I request to send out the data, only the beginning of TxData is transmitted, while the 0x00 bytes in between and the last bytes are ignored shown in Image 2:

void sendData (uint8_t *data, int size)
{
  // we will calculate the CRC in this function itself
  uint16_t crc = crc16(data, size);
  data[size] = crc&0xFF;   // CRC LOW
  data[size+1] = (crc>>8)&0xFF;  // CRC HIGH

// HAL_UART_Transmit(&huart2, data, size+2, 1000);

  uint8_t *buff = (uint8_t *)malloc(size + 2);
  if (buff == NULL) {
	  // Handle memory allocation failure
	  return;
  }

  // Copy data to buffer
  memcpy(buff, data, size+2);

  // Send the buffer
  hal.stream.write(buff);

  // Free the allocated buffer
  free(buff);
}

image

image

@terjeio
Copy link
Contributor

terjeio commented Aug 9, 2024

hal.stream.write() is for sending a null terminated string, use hal.stream.write_n() if the data contains null characters (0s).

@datdadev
Copy link
Author

datdadev commented Aug 9, 2024

Thank you, this works pretty well. I have one more question: How can I put a sequence of commands into the read buffer and then send them to the machine for processing? For example, I have a string of G-code like this:

const char* gcode_file_1 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G1 X20 Y10 ; Draw line\n"
"G1 X20 Y20 ; Draw line\n"
"G1 X10 Y20 ; Draw line\n"
"G1 X10 Y10 ; Draw line\n"
"M2 ; End of program\n";

@datdadev
Copy link
Author

datdadev commented Aug 9, 2024

My idea is to poll for data via idle line detection and then check whether to start or stop the G-code execution based on the status. However, I am unsure why the status_checker() function is not being called. The intended approach is to first save the hal.stream.read pointer, which reads from the serial port, then switch to send_gcode for sending data. After the sending is complete, the hal.stream.read pointer should be restored to the backup.

The current code of `my_plugin.c`
#include "driver.h"
#include <string.h>
#include <stdio.h>

#include "Modbus.h"

static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;
static int16_t* org_read_ptr;

#define RX_DATA_SIZE 16
#define TX_DATA_SIZE 16
uint8_t RxData[RX_DATA_SIZE];
static uint16_t rx_index = 0;

uint8_t TxData[TX_DATA_SIZE];

extern uint16_t Holding_Registers_Database[50];
extern uint8_t Coils_Database[25];

const char* gcode_file_1 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G1 X20 Y10 ; Draw line\n"
"G1 X20 Y20 ; Draw line\n"
"G1 X10 Y20 ; Draw line\n"
"G1 X10 Y10 ; Draw line\n"
"M2 ; End of program\n";

const char* gcode_file_2 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G2 X20 Y20 I10 J0 ; Draw clockwise arc\n"
"G2 X10 Y10 I-10 J0 ; Complete the circle\n"
"M2 ; End of program\n";

const char* gcode_file_3 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G1 X20 Y10 ; Draw line\n"
"G1 X10 Y20 ; Draw line\n"
"G1 X20 Y20 ; Draw line\n"
"G1 X10 Y30 ; Draw line\n"
"G1 X20 Y30 ; Draw line\n"
"M2 ; End of program\n";

// Function prototypes
static void poll_for_received_data(void);
static void status_checker(void);
static void flush_input_buffer(void);
static void modbus_proces(void);

static void modbus_process(void) {
	if (RxData[0]==SLAVE_ID) {
		if (RxData[1] == 0x03 || RxData[1] == 0x04) {
			readHoldingRegs();
		} else if (RxData[1] == 16) {
			writeHoldingRegs();
		} else if (RxData[1] == 0x01) {
			readCoils ();
		} else if (RxData[1] == 0x05) {
			writeSingleCoil();
		}
	}
}

// Function to flush the input buffer
static void flush_input_buffer(void) {
    int16_t c;
    // Read and discard all data from the stream until the buffer is empty
    while ((c = hal.stream.read()) != -1) {
        // Discard data
    }
}

static const char* gcode = NULL;

// Function to send G-code commands via the communication stream
static int16_t send_gcode(void) {
    static bool eol_ok = false;

    if (gcode == NULL) {
        return SERIAL_NO_DATA;
    }

    if (*gcode == '\0') {          // End of the G-code string?
        if (eol_ok) {
            gcode = NULL;          // Reset the G-code pointer
            return SERIAL_NO_DATA; // No more data to send
        }
        eol_ok = true;
        return ASCII_LF;           // Send a linefeed character
    }

    char c = *gcode++;             // Get the next character

    if ((eol_ok = (c == '|'))) {   // If the character is a vertical bar '|'
        c = ASCII_LF;              // Replace it with a linefeed character
    }

    return (int16_t)c;
}

// Function to poll for received data, store it in RxData, and write it back
static void poll_for_received_data(void) {
    int16_t c;
    uint16_t rx_buffer_count;
    uint32_t idle_start_time;
    const uint32_t IDLE_TIMEOUT_MS = 500;  // Adjusted based on T3.5
    const uint32_t POLLING_INTERVAL_MS = 200;  // Faster than T1.5

    // Initialize the start time for idle detection
    idle_start_time = HAL_GetTick();  // Assuming HAL_GetTick() returns milliseconds since system start

    // Ensure that all characters are kept by registering the appropriate handler
    hal.stream.set_enqueue_rt_handler(stream_buffer_all);

    while (1) {
        rx_buffer_count = hal.stream.get_rx_buffer_count();

        // Check if there is data in the buffer
        if (rx_buffer_count > 0) {
            // Reset the idle timer
            idle_start_time = HAL_GetTick();

            // Reset index for new message
            rx_index = 0;

            // Read data from the stream
            while (rx_buffer_count > 0 && rx_index < RX_DATA_SIZE) {
                if ((c = hal.stream.read()) != -1) {
                    RxData[rx_index++] = (uint8_t)c;  // Store received character in RxData
                    rx_buffer_count--;
                }
            }

            // Fill the remaining buffer space with zeros (optional, depending on your needs)
            while (rx_index < RX_DATA_SIZE) {
                RxData[rx_index++] = 0x00;
            }

            // Process the received data
            modbus_process();
        } else {
            // Check for idle timeout
            if ((HAL_GetTick() - idle_start_time) > IDLE_TIMEOUT_MS) {
                // Handle idle timeout if necessary
                break;
            }
        }

        // Delay before next check (polling interval)
        HAL_Delay(POLLING_INTERVAL_MS);  // Adjust delay as necessary
    }
}

static void status_checker(void) {
	if (Coils_Database[0] != 0) {
		// Decide which G-code file to execute based on Holding_Registers_Database[10]
		const char* gcode_to_send = NULL;
		switch (Holding_Registers_Database[10]) {
			case 1:
				gcode_to_send = gcode_file_1;
				break;
			case 2:
				gcode_to_send = gcode_file_2;
				break;
			case 3:
				gcode_to_send = gcode_file_3;
				break;
			default:
				// No valid G-code file selection
				break;
		}

		// If a valid G-code file is selected, send it
		if (gcode_to_send != NULL) {
			gcode = gcode_to_send;  // Set the G-code to be sent
			org_read_ptr = hal.stream.read;  // Backup original read pointer
			hal.stream.read = send_gcode;    // Set read pointer to our G-code sender

			while (send_gcode() != SERIAL_NO_DATA) {
				uint8_t c = send_gcode();
			}

			hal.stream.read = org_read_ptr;  // Restore original read pointer

			Coils_Database[0] = 0;  // Optionally reset the start/stop status
		}
	}
}

// Check the Holding_Registers_Database and send G-code commands if needed
static void check_register_and_execute_gcode(sys_state_t state) {
    // Poll for and process received data
    poll_for_received_data();

    status_checker();

    // Continue with any other real-time execution tasks
    on_execute_realtime(state);
}

// Add info about our plugin to the $I report
static void on_report_my_options(bool newopt) {
    if (on_report_options) {
        on_report_options(newopt);
    }

    if (!newopt) {
        // Add ASCII_EOL after the plugin information string
        const char plugin_info[] = "[PLUGIN:Register Monitor v1.00]" ASCII_EOL;
        hal.stream.write((uint8_t*)plugin_info);
    }
}

void my_plugin_init(void) {
    // Add info about our plugin to the $I report
    on_report_options = grbl.on_report_options;
    grbl.on_report_options = on_report_my_options;

    // Add check register and execute G-code function to grblHAL foreground process
    on_execute_realtime = grbl.on_execute_realtime;
    grbl.on_execute_realtime = check_register_and_execute_gcode;
}

@terjeio
Copy link
Contributor

terjeio commented Aug 9, 2024

You have a while(1) { } and while (send_gcode() != SERIAL_NO_DATA) {} loops in poll_for_received_data() which means it never returns - it should or the core will stall. FYI poll_for_received_data() will be called repeatedly by the core at a high frequency.

A serious issue with your code is that you use the default stream for reading the modbus commands, it is better to claim another stream for that - or you will have to stall the main loop from reading from the input while waiting for data. It is possible but not advisable - if you stall the main loop while gcode is executing motion just stops, you will have to delay stalling it until the gcode has finished running to avoid that.

hal.stream.set_enqueue_rt_handler(stream_buffer_all); should be called in the init function.

@datdadev
Copy link
Author

datdadev commented Aug 10, 2024

I recently switched to a new UART2 stream, as I changed the default to UART1 in my_machine_map.h. This is because I will primarily communicate with the HMI using the UART2 stream with the non-used main stream of UART1. I also added an extra serial port for the HMI stream.

#define BOARD_NAME "MY MACHINE"
//#define HAS_BOARD_INIT

#define SERIAL_PORT     1   // GPIOA: TX = 2, RX = 3
#define SERIAL1_PORT    2   // GPIOA: TX = 2, RX = 3
// ...

And this is my source code for my_plugin.c. Am I doing it right by initializing the stream like this and restoring the stream after the G-code execution is finished?

The current code of `my_plugin.c`
#include "driver.h"
#include <string.h>
#include <stdio.h>
#include "Modbus.h"

// Function pointers
static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;
static int16_t* org_read_ptr;

// Buffer sizes
#define RX_DATA_SIZE 16
#define TX_DATA_SIZE 16

// Global buffers and indices
uint8_t RxData[RX_DATA_SIZE];
static uint16_t rx_index = 0;
uint8_t TxData[TX_DATA_SIZE];

// External variables
extern uint16_t Holding_Registers_Database[50];
extern uint8_t Coils_Database[25];

// G-code files
const char* gcode_file_1 = "G-code string here";
const char* gcode_file_2 = "G-code string here";
const char* gcode_file_3 = "G-code string here";

// Function prototypes
static void poll_for_received_data(void);
static void status_checker(void);
static void flush_input_buffer(void);
static void modbus_process(void);

// Define and open a separate stream for Modbus communication
static io_stream_t modbus_stream;

static void modbus_process(void) {
    if (RxData[0] == SLAVE_ID) {
        if (RxData[1] == 0x03 || RxData[1] == 0x04) {
            readHoldingRegs();
        } else if (RxData[1] == 16) {
            writeHoldingRegs();
        } else if (RxData[1] == 0x01) {
            readCoils();
        } else if (RxData[1] == 0x05) {
            writeSingleCoil();
        }
    }
}

// Function to flush the input buffer
static void flush_input_buffer(void) {
    int16_t c;
    while ((c = modbus_stream.read()) != -1) {
        // Discard data
    }
}

static const char* gcode = NULL;

// Function to send G-code commands via the communication stream
static int16_t send_gcode(void) {
    static bool eol_ok = false;

    if (gcode == NULL) {
        return SERIAL_NO_DATA;
    }

    if (*gcode == '\0') {
        if (eol_ok) {
            gcode = NULL;
            return SERIAL_NO_DATA;
        }
        eol_ok = true;
        return ASCII_LF;
    }

    char c = *gcode++;
    if ((eol_ok = (c == '|'))) {
        c = ASCII_LF;
    }

    return (int16_t)c;
}

// Function to poll for received data, store it in RxData, and process it
static void poll_for_received_data(void) {
    int16_t c;
    uint32_t idle_start_time = HAL_GetTick();
    const uint32_t IDLE_TIMEOUT_MS = 500;
    const uint32_t POLLING_INTERVAL_MS = 200;

    rx_index = 0;

    while (1) {
        // Check if data is available in the Modbus stream
        if (modbus_stream.get_rx_buffer_count() > 0) {
            idle_start_time = HAL_GetTick();

            while (rx_index < RX_DATA_SIZE && (c = modbus_stream.read()) != -1) {
                RxData[rx_index++] = (uint8_t)c;
            }

            modbus_process();
        } else if ((HAL_GetTick() - idle_start_time) > IDLE_TIMEOUT_MS) {
            break;
        }

        HAL_Delay(POLLING_INTERVAL_MS);
    }
}

// Function to check the status and execute G-code commands if needed
static void status_checker(void) {
    if (Coils_Database[0] != 0) {
        const char* gcode_to_send = NULL;

        switch (Holding_Registers_Database[10]) {
            case 1:
                gcode_to_send = gcode_file_1;
                break;
            case 2:
                gcode_to_send = gcode_file_2;
                break;
            case 3:
                gcode_to_send = gcode_file_3;
                break;
            default:
                break;
        }

        if (gcode_to_send != NULL) {
            gcode = gcode_to_send;
            org_read_ptr = modbus_stream.read;
            modbus_stream.read = send_gcode;

            while (send_gcode() != SERIAL_NO_DATA) {
                // Process G-code
            }

            modbus_stream.read = org_read_ptr;
            Coils_Database[0] = 0;
        }
    }
}

// Real-time execution check
static void check_register_and_execute_gcode(sys_state_t state) {
    poll_for_received_data();
    status_checker();
    on_execute_realtime(state);
}

// Add plugin information to the report
static void on_report_my_options(bool newopt) {
    if (on_report_options) {
        on_report_options(newopt);
    }

    if (!newopt) {
        const char plugin_info[] = "[PLUGIN:Register Monitor v1.00]" ASCII_EOL;
        modbus_stream.write((uint8_t*)plugin_info);
    }
}

void my_plugin_init(void) {
    // Initialize the Modbus stream
    io_stream_t const *stream;
    if ((stream = stream_open_instance(2, 115200, NULL, "Modbus UART")) == NULL) {
        stream = stream_null_init(115200);
    }

    memcpy(&modbus_stream, stream, sizeof(io_stream_t));

    // Set up the plugin
    modbus_stream.set_enqueue_rt_handler(stream_buffer_all);
    on_report_options = grbl.on_report_options;
    grbl.on_report_options = on_report_my_options;
    on_execute_realtime = grbl.on_execute_realtime;
    grbl.on_execute_realtime = check_register_and_execute_gcode;
}

@datdadev
Copy link
Author

I’m not sure if the state machine I’ve planned out is correct.

image

@terjeio
Copy link
Contributor

terjeio commented Aug 15, 2024

I would get rid of the while loops in poll_for_received_data() - since these blocks the foreground process, poll_for_received_data() is aready part of the outer loop in the core. Read data from the modbus stream until a complete message is received and process it - return a modbus error to the client if gcode is already running, confirm if not

I’m not sure if the state machine I’ve planned out is correct.

If it works then you are good.

flush_input_buffer(void)

There is a stream function available for this, call modbus_stream.reset_read_buffer().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants