-
Notifications
You must be signed in to change notification settings - Fork 87
SD Card implementation, a technical walktrough
The SD Card implementation makes good use of the fact that the HAL is function pointer based. This allows code to be "hooked into" grblHAL without the core codebase needing any changes or beeing aware that changes are beeing made to its runtime environment.
In order to stream GCode from a SDCard a number of features needs to be implemented:
- Somehow the SD Card needs to me mounted
- It must be possible to list the files
- It must be possible to select a file and start a job reading from the selected file
- It must be possible to cancel, pause and restart the job
- Proper error handling needs to be implemented
- If manual tool hange is to be supported it must be possible to suspend a running job
All this is achieved by just temporarily changing function pointers...
1 - 3 is implemented by adding a set of new system commands ($
-commands). This is done by setting the ´hal.driver_sys_command_execute` function pointer to point to our system command parser. When the base parser comes across a system command it does not understand it will pass it too our function, we can then decide if it is a command we want to act upon. The pointer is set when the driver initializes the SD Card subsystem.
Our system command parser function:
static status_code_t sdcard_parse (uint_fast16_t state, char *line, char *lcline)
{
status_code_t retval = Status_Unhandled;
if(line[1] == 'F') switch(line[2]) {
case '\0':
retval = sdcard_ls(line); // (re)use line buffer for reporting filenames
break;
case 'M':
retval = sdcard_mount() ? Status_OK : Status_SDMountError;
break;
case '=':
if (state != STATE_IDLE)
retval = Status_SystemGClock;
else {
if(file_open(&line[3])) {
gc_state.last_error = Status_OK; // Start with no errors
hal.report.status_message(Status_OK); // and confirm command to originator
memcpy(&active_stream, &hal.stream, sizeof(io_stream_t)); // Save current stream pointers
hal.stream.type = StreamSetting_SDCard; // then redirect to read from SD card instead
hal.stream.read = sdcard_read; // ...
hal.stream.enqueue_realtime_command = drop_input_stream; // Drop input from current stream except realtime commands
#if M6_ENABLE
hal.stream.suspend_read = sdcard_suspend; // ...
#else
hal.stream.suspend_read = NULL; // ...
#endif
hal.driver_rt_report = sdcard_report; // Add percent complete to real time report
hal.report.status_message = trap_status_report; // Redirect status message and feedback message
hal.report.feedback_message = trap_feedback_message; // reports here
retval = Status_OK;
} else
retval = Status_SDReadError;
}
break;
default:
retval = Status_InvalidStatement;
break;
}
return retval;
}
Our fuction will be called with two versions of the system command, one uppercased with spaces removed (line
) and one with no changes applied (lcline
). Since filenames may contain spaces and the filing system selected may be case sensitive we choose to use the lcline
version when handling filenames.
$FM
- mount the SD Card
$F
- list files and directories recursively
$F=<filename
- open a file and start reading from it
The function pointer magic happens when a $F=<filename
command is issued. If the file is valid the current function pointers for stream handling is saved away - we need them later to restore normal operation when the job is complete. The input stream is then "redirected" to our function sdcard_read
by changing the pointer hal.stream.read
to point to our function. The main protocol loop calls the function pointed to by hal.stream.read
when reading data from the input stream, it has no idea about where the data is coming from (and it does not need to know either).
Every character read from a input stream passes through the function pointed to by hal.stream.enqueue_realtime_command
, this strips out real-time commands and submits them to be handled by the core state-machine. When reading data from the SD Card we still want the real-time input from the original stream to be handled normally (allowing reset, feed-hold etc.) but any other input should be discarded.
This is achieved by redirecting hal.stream.enqueue_realtime_command
to our function drop_input_stream
, this calls the original we saved away and returns true to the original caller - indicating all characters should be thrown away (after real-time commands has been picked off):
// Read a character from the SD Card file
static int16_t sdcard_read (void)
{
int16_t c = -1;
if(file.eol == 1)
file.line++;
if(file.handle) {
if(sys.state == STATE_IDLE || (sys.state & (STATE_CYCLE|STATE_HOLD)))
c = file_read();
if(c == -1) { // EOF or error reading or grbl problem
file_close();
if(file.eol == 0) // Return newline if line was incorrectly terminated
c = '\n';
}
} else if(sys.state == STATE_IDLE)
sdcard_end_job();
return c;
}
// Drop input from current stream except realtime commands
ISR_CODE bool drop_input_stream (char c)
{
active_stream.enqueue_realtime_command(c);
return true;
}
All status codes from handling input in the main protocol loop is passed over the hal.report.status_message
function pointer before beeing sent to the output stream. By redirecting this to our own function trap_status_report
we can then check if any error occured when the last line read from the SD Card was processed:
void trap_status_report (status_code_t status_code)
{
if(status_code != Status_OK) {
char buf[50];
sprintf(buf, "error:%d in SD file at line %d\r\n", (uint8_t)status_code, file.line);
hal.stream.write(buf);
sdcard_end_job();
}
}
If an error occurs we simply report the error and terminate the job.
To indicate to the user how the job is progressing we want to add a message to the standard real time report.
This is done by setting the hal.driver_rt_report
to point to a function that formats such a message.
This function will be called every time a real-time report is generated by the core:
static void sdcard_report (stream_write_ptr stream_write)
{
stream_write("|SD:");
stream_write(ftoa((float)file.pos / (float)file.size * 100.0f, 1));
}
Normally streaming from a file is complete when the file ends, however the NIST specification states that a M30 command should terminate the job and rewind the file. To comply with NIST we can redirect feedback messages to a function and check for the program end message and terminate the job when this is encountered:
void trap_feedback_message (message_code_t message_code)
{
report_feedback_message(message_code);
if(message_code == Message_ProgramEnd)
sdcard_end_job();
}
We call the original report function so that the GCode sender is also made aware of any feedback.
grblHAL has support for manual tool change (driver dependent), in order for this to work when streaming a job from the SD Card we must pause the streaming and allow normal input to take place until tool change is complete.
Manual tool change support activated in the core by pointing hal.stream.suspend_read
to a handler function, if NULL then M6 commands will result in an error beeing generated. Our handler looks like this:
static bool sdcard_suspend (bool suspend)
{
if(suspend) {
hal.stream.reset_read_buffer();
hal.stream.read = active_stream.read; // Restore normal stream input for tool change (jog etc)
hal.stream.enqueue_realtime_command = active_stream.enqueue_realtime_command;
hal.report.status_message = report_status_message; // as well as normal status messages reporting
} else {
hal.stream.read = sdcard_read; // Resume reading from SD card
hal.stream.enqueue_realtime_command = drop_input_stream;
hal.report.status_message = trap_status_report; // and redirect status messages back to us
}
return true;
}
Basically this restores the redirected function pointers until the tool change is complete, and then sets them back to continue processing input from the SD Card file.
When the job is complete the file is closed and all redirected function pointers are set back to their original values:
static void sdcard_end_job (void)
{
file_close();
memcpy(&hal.stream, &active_stream, sizeof(io_stream_t)); // Restore stream pointers
hal.stream.reset_read_buffer(); // and flush input buffer
hal.driver_rt_report = NULL;
hal.report.status_message = report_status_message;
hal.report.feedback_message = report_feedback_message;
}
Reset during streaming is handled via code in the driver that is called on a reset. A reset message is issued if the SD Card was streaming while a job was active, the file is closed and the function pointers changed are restored to their original values:
void sdcard_reset (void)
{
if(hal.stream.type == StreamSetting_SDCard) {
char buf[70];
sprintf(buf, "[MSG:Reset during streaming of SD file at line: %d]\r\n", file.line);
hal.stream.write(buf);
sdcard_end_job();
}
}
Function pointers provides a clean and flexible way of extending functionality, this without a single change to the core.
No maze of #define
and corresponding #if or #ifdef #else #endif
statements possibly resulting in both overhead and unreadable/hard to maintain spaghetti code.
Full source for the SD Card plugin here.