commit 3f4d63ea58373d2f6ffa194d5178ce52a7e5b994
parent 6f5b5eb66a3dfc977d7df95b443b2e2b13ae4021
Author: Christian Ermann <christianermann@gmail.com>
Date:   Tue, 22 Oct 2024 22:33:27 -0700
Add gcode interpreter
Diffstat:
| A | .gitignore | | | 1 | + | 
| A | LICENSE | | | 19 | +++++++++++++++++++ | 
| A | Makefile | | | 11 | +++++++++++ | 
| A | include/command.h | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ | 
| A | include/machine.h | | | 91 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | include/timing.h | | | 22 | ++++++++++++++++++++++ | 
| A | include/token.h | | | 56 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | include/tui.h | | | 29 | +++++++++++++++++++++++++++++ | 
| A | src/command.c | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | src/machine.c | | | 297 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | src/main.c | | | 223 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | src/timing.c | | | 62 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | src/token.c | | | 309 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | src/tui.c | | | 62 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
14 files changed, 1292 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+*.gcode
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2024 Christian Ermann
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,11 @@
+all:
+	gcc src/* -Iinclude -lm -o gcode-interpreter
+
+run: all
+	./gcode-interpreter
+
+.PHONY: clean
+clean:
+	rm gcode-interpreter
+	rm error.log
+	rm out
diff --git a/include/command.h b/include/command.h
@@ -0,0 +1,43 @@
+#ifndef COMMAND_H
+#define COMMAND_H
+
+#include "token.h"
+
+#include <stdbool.h>
+
+typedef enum {
+    CODE_G = TOKEN_G_CODE,
+    CODE_M = TOKEN_M_CODE,
+} CodeType;
+
+typedef struct {
+    bool is_set;
+    CodeType type;
+    unsigned int value;
+} Code;
+
+typedef struct {
+    bool is_set;
+    float value;
+} Param;
+
+typedef struct {
+    Code code;
+    Param param_s;
+    Param param_x;
+    Param param_y;
+    Param param_z;
+    Param param_e;
+    Param param_f;
+} Command;
+
+typedef enum {
+    COMMAND_STATUS__UNRECOGNIZED_TOKEN,
+    COMMAND_STATUS__NOT_READY,
+    COMMAND_STATUS__READY,
+} CommandStatus;
+
+void command_init(Command* command);
+CommandStatus command_process_token(Command* command, const Token* token);
+
+#endif
diff --git a/include/machine.h b/include/machine.h
@@ -0,0 +1,91 @@
+#ifndef MACHINE_H
+#define MACHINE_H
+
+#include "command.h"
+#include "timing.h"
+
+typedef enum {
+    UNITS_MM,
+    UNITS_INCHES,
+} Units;
+
+static const char* UNITS_STRINGS[] = {
+    "mm",
+    "in",
+};
+
+typedef enum {
+    POSITIONING_MODE_ABSOLUTE,
+    POSITIONING_MODE_RELATIVE,
+} PositioningMode;
+
+static const char* POSITIONING_MODE_STRINGS[] = {
+    "absolute",
+    "relative",
+};
+
+typedef enum {
+    EXECUTE_STATUS__UNRECOGNIZED_COMMAND,
+    EXECUTE_STATUS__UNRECOGNIZED_GCODE,
+    EXECUTE_STATUS__UNRECOGNIZED_MCODE,
+    EXECUTE_STATUS__IDLE,
+    EXECUTE_STATUS__IN_PROGRESS,
+    EXECUTE_STATUS__FINISHED,
+} ExecutionStatus;
+
+static const char* MACHINE_ERROR_STRINGS[] = {
+    "command unrecognized.",
+    "gcode unrecognized.",
+    "mcode unrecognized.",
+};
+
+typedef struct {
+    float x;
+    float y;
+    float z;
+} ControlData;
+
+typedef struct {
+    ExecutionStatus status;
+    bool has_data;
+    ControlData data;
+} ExecutionResult;
+
+typedef enum {
+    MOVE_NONE,
+    MOVE_LINEAR,
+} MoveType;
+
+typedef struct {
+    float start;
+    float distance;
+} AxisStatus;
+
+typedef struct {
+    MoveType type;
+    Time start;
+    Time end;
+    Time duration;
+    AxisStatus x;
+    AxisStatus y;
+    AxisStatus z;
+} Move;
+
+typedef struct {
+    float x;
+    float y;
+    float z;
+    float feed_rate;
+    Units units;
+    PositioningMode positioning_mode;
+    Move current_move;
+} Machine;
+
+void machine_init(Machine* machine);
+ExecutionResult machine_execute_command(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+);
+
+#endif
diff --git a/include/timing.h b/include/timing.h
@@ -0,0 +1,22 @@
+#ifndef TIMING_H
+#define TIMING_H
+
+#include <stdbool.h>
+
+typedef struct {
+    long time_s;
+    long time_ns;
+} Time;
+
+Time time_make(long time_s, long time_ns);
+Time time_zero();
+Time time_get();
+
+Time time_add(const Time* t0, const Time* t1);
+Time time_sub(const Time* t0, const Time* t1);
+float time_div(const Time* t0, const Time* t1);
+
+int time_cmp(const Time* t0, const Time* t1);
+bool time_eql_zero(const Time* t);
+
+#endif
diff --git a/include/token.h b/include/token.h
@@ -0,0 +1,56 @@
+#ifndef TOKEN_H
+#define TOKEN_H
+
+#include <stdio.h>
+
+typedef struct {
+    FILE* file;
+    unsigned int line_idx;
+    unsigned int char_idx;
+} Cursor;
+
+void cursor_init(Cursor* cursor, FILE* file);
+char cursor_getc(Cursor* cursor);
+void cursor_ungetc(Cursor* cursor, char c);
+
+static const char* TOKEN_ERROR_STRINGS[] = {
+    "field value exceeded max length.",
+    "invalid character. unable to parse integer.",
+    "no digits found. unable to parse integer.",
+    "invalid character. unable to parse float.",
+    "no digits found. unable to parse float.",
+    "field type does not support value parsing.",
+    "field type unrecognized.",
+};
+
+typedef enum {
+    TOKEN_COMMENT = ';',
+    TOKEN_G_CODE = 'G',
+    TOKEN_M_CODE = 'M',
+    TOKEN_S_PARAM = 'S',
+    TOKEN_X_PARAM = 'X',
+    TOKEN_Y_PARAM = 'Y',
+    TOKEN_Z_PARAM = 'Z',
+    TOKEN_F_PARAM = 'F',
+    TOKEN_E_PARAM = 'E',
+    TOKEN_EOF = EOF,
+    TOKEN_ERROR = '!',
+} TokenType;
+
+typedef union {
+    float param_value;
+    unsigned int code_value;
+    unsigned int error_value;
+} TokenValue;
+
+typedef struct {
+    TokenType type;
+    TokenValue value;
+    unsigned int line_idx;
+    unsigned int char_idx;
+} Token;
+
+Token parse_token(Cursor* cursor);
+void recover_from_error(Cursor* cursor, const Token* token);
+
+#endif
diff --git a/include/tui.h b/include/tui.h
@@ -0,0 +1,29 @@
+#ifndef TUI_H
+#define TUI_H
+
+#include <unistd.h>
+
+#define type(str) write(1, str, sizeof(str))
+
+#define STR_(x) #x
+#define STR(x) STR_(x)
+
+#define esc "\x1b"
+#define csi esc "["
+
+#define switch_to csi "?1049"
+#define main_buffer "h"
+#define alternate_buffer "l"
+
+#define clear_buffer csi "2J"
+#define clear_line csi "2K"
+#define clear_to_end_of_line csi "0K"
+
+#define hide_cursor csi "?25l"
+#define show_cursor csi "?25h"
+#define set_cursor(n, m) csi STR(n) ";" STR(m) "H"
+
+void initialize_terminal();
+void type_line(const char* msg, unsigned int line);
+
+#endif
diff --git a/src/command.c b/src/command.c
@@ -0,0 +1,67 @@
+#include "command.h"
+
+void command_init(Command* command) {
+    command->code.is_set = false;
+    command->param_s.is_set = false;
+    command->param_x.is_set = false;
+    command->param_y.is_set = false;
+    command->param_z.is_set = false;
+    command->param_e.is_set = false;
+    command->param_f.is_set = false;
+}
+
+CommandStatus command_process_token(Command* command, const Token* token) {
+    switch (token->type) {
+        case TOKEN_M_CODE: {
+            if (command->code.is_set) {
+                return COMMAND_STATUS__READY;
+            }
+            command->code.is_set = true;
+            command->code.type = CODE_M;
+            command->code.value = token->value.code_value;
+            break;
+        }
+        case TOKEN_G_CODE: {
+            if (command->code.is_set) {
+                return COMMAND_STATUS__READY;
+            }
+            command->code.is_set = true;
+            command->code.type = CODE_G;
+            command->code.value = token->value.code_value;
+            break;
+        }
+        case TOKEN_S_PARAM: {
+            command->param_s.is_set = true;
+            command->param_s.value = token->value.param_value;
+            break;
+        }
+        case TOKEN_X_PARAM: {
+            command->param_x.is_set = true;
+            command->param_x.value = token->value.param_value;
+            break;
+        }
+        case TOKEN_Y_PARAM: {
+            command->param_y.is_set = true;
+            command->param_y.value = token->value.param_value;
+            break;
+        }
+        case TOKEN_Z_PARAM: {
+            command->param_z.is_set = true;
+            command->param_z.value = token->value.param_value;
+            break;
+        }
+        case TOKEN_E_PARAM: {
+            command->param_e.is_set = true;
+            command->param_e.value = token->value.param_value;
+            break;
+        }
+        case TOKEN_F_PARAM: {
+            command->param_f.is_set = true;
+            command->param_f.value = token->value.param_value;
+            break;
+        }
+        default:
+            return COMMAND_STATUS__UNRECOGNIZED_TOKEN;
+    }
+    return COMMAND_STATUS__NOT_READY;
+}
diff --git a/src/machine.c b/src/machine.c
@@ -0,0 +1,297 @@
+#include "machine.h"
+
+#include <math.h>
+
+#define DEFAULT_FEED_RATE 3000 // 3000 mm/m = 50 mm/s
+#define DEFAULT_UNITS UNITS_MM
+#define DEFAULT_POSITIONING_MODE POSITIONING_MODE_ABSOLUTE
+
+ExecutionResult machine_execute_m_code(Machine* machine, const Command* command);
+ExecutionResult machine_execute_g_code(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+);
+
+ExecutionResult machine_execute_g1(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+);
+ExecutionResult machine_execute_g21(Machine* machine, const Command* command);
+ExecutionResult machine_execute_g90(Machine* machine, const Command* command);
+ExecutionResult machine_execute_g91(Machine* machine, const Command* command);
+ExecutionResult machine_execute_g92(Machine* machine, const Command* command);
+
+void machine_init(Machine* machine) {
+    machine->x = 0;
+    machine->y = 0;
+    machine->z = 0;
+    machine->feed_rate = DEFAULT_FEED_RATE;
+    machine->units = DEFAULT_UNITS;
+    machine->positioning_mode = DEFAULT_POSITIONING_MODE;
+    machine->current_move.type = MOVE_NONE;
+}
+
+ExecutionResult machine_execute_command(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+) {
+    switch (command->code.type) {
+        case CODE_M:
+            return machine_execute_m_code(machine, command);
+        case CODE_G:
+            return machine_execute_g_code(machine, command, now);
+        default: {
+            ExecutionResult result = {
+                .status = EXECUTE_STATUS__UNRECOGNIZED_COMMAND,
+                .has_data = false,
+            };
+            return result;
+        }
+    }
+}
+
+ExecutionResult machine_execute_m_code(
+    Machine* machine,
+    const Command* command
+) {
+    switch (command->code.value) {
+        default: {
+            ExecutionResult result = {
+                .status = EXECUTE_STATUS__UNRECOGNIZED_MCODE,
+                .has_data = false,
+            };
+            return result;
+        }
+    }
+}
+
+ExecutionResult machine_execute_g_code(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+) {
+    switch (command->code.value) {
+        case 0:
+        case 1:
+            return machine_execute_g1(machine, command, now);
+        case 21:
+            return machine_execute_g21(machine, command);
+        case 90:
+            return machine_execute_g90(machine, command);
+        case 91:
+            return machine_execute_g91(machine, command);
+        case 92:
+            return machine_execute_g92(machine, command);
+        default: {
+            ExecutionResult result = {
+                .status = EXECUTE_STATUS__UNRECOGNIZED_GCODE,
+                .has_data = false,
+            };
+            return result;
+        }
+    }
+}
+
+float x_distance(Machine* machine, const Command* command) {
+    if (!command->param_x.is_set) {
+        return 0;
+    }
+    switch (machine->positioning_mode) {
+        case POSITIONING_MODE_ABSOLUTE:
+            return command->param_x.value - machine->x;
+        case POSITIONING_MODE_RELATIVE:
+            return command->param_x.value;
+    }
+}
+
+float y_distance(Machine* machine, const Command* command) {
+    if (!command->param_y.is_set) {
+        return 0;
+    }
+    switch (machine->positioning_mode) {
+        case POSITIONING_MODE_ABSOLUTE:
+            return command->param_y.value - machine->y;
+        case POSITIONING_MODE_RELATIVE:
+            return command->param_y.value;
+    }
+}
+
+float z_distance(Machine* machine, const Command* command) {
+    if (!command->param_z.is_set) {
+        return 0;
+    }
+    switch (machine->positioning_mode) {
+        case POSITIONING_MODE_ABSOLUTE:
+            return command->param_z.value - machine->z;
+        case POSITIONING_MODE_RELATIVE:
+            return command->param_z.value;
+        default:
+            return 0;
+    }
+}
+
+Time g1_execute_duration(Machine* machine, const Command* command) {
+    float dx = x_distance(machine, command);
+    float dy = y_distance(machine, command);
+    float dz = z_distance(machine, command);
+    float d = sqrtf((dx * dx) + (dy * dy) + (dz * dz));
+    float t = 60.0f * d / machine->feed_rate;
+    return time_make(0, (long)round(t * 1e9));
+}
+
+ExecutionResult machine_execute_g1_start(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+) {
+    if (command->param_f.is_set) {
+        machine->feed_rate = command->param_f.value;
+    }
+    machine->current_move.type = MOVE_LINEAR;
+    machine->current_move.start = *now;
+    machine->current_move.duration = g1_execute_duration(machine, command);
+    machine->current_move.end = time_add(now, &machine->current_move.duration);
+    machine->current_move.x.start = machine->x;
+    machine->current_move.y.start = machine->y;
+    machine->current_move.z.start = machine->z;
+    machine->current_move.x.distance = x_distance(machine, command);
+    machine->current_move.y.distance = y_distance(machine, command);
+    machine->current_move.z.distance = z_distance(machine, command);
+    if (time_eql_zero(&machine->current_move.duration)) {
+        machine->current_move.type = MOVE_NONE;
+        ExecutionResult result = {
+            .status = EXECUTE_STATUS__FINISHED,
+            .has_data = false,
+        };
+        return result;
+    }
+    ExecutionResult result = {
+        .status = EXECUTE_STATUS__IN_PROGRESS,
+        .has_data = false,
+    };
+    return result;
+}
+
+ExecutionResult machine_execute_g1_end(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+) {
+    machine->x = machine->current_move.x.start + machine->current_move.x.distance;
+    machine->y = machine->current_move.y.start + machine->current_move.y.distance;
+    machine->z = machine->current_move.z.start + machine->current_move.z.distance;
+    machine->current_move.type = MOVE_NONE;
+    ControlData data = {
+        .x = machine->x,
+        .y = machine->y,
+        .z = machine->z,
+    };
+    ExecutionResult result = {
+        .status = EXECUTE_STATUS__FINISHED,
+        .has_data = true,
+        .data = data,
+    };
+    return result;
+}
+
+ExecutionResult machine_execute_g1_tick(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+) {
+    Time elapsed = time_sub(now, &machine->current_move.start);
+    float percent_complete = time_div(&elapsed, &machine->current_move.duration);
+    float dx = machine->current_move.x.distance * percent_complete;
+    float dy = machine->current_move.y.distance * percent_complete;
+    float dz = machine->current_move.z.distance * percent_complete;
+    machine->x = machine->current_move.x.start + dx;
+    machine->y = machine->current_move.y.start + dy;
+    machine->z = machine->current_move.z.start + dz;
+    ControlData data = {
+        .x = machine->x,
+        .y = machine->y,
+        .z = machine->z,
+    };
+    ExecutionResult result = {
+        .status = EXECUTE_STATUS__IN_PROGRESS,
+        .has_data = true,
+        .data = data,
+    };
+    return result;
+}
+
+ExecutionResult machine_execute_g1(
+    Machine* machine,
+    const Command* command,
+    const Time* now
+) {
+    if (machine->current_move.type == MOVE_NONE) {
+        return machine_execute_g1_start(machine, command, now);
+    }
+    if (time_cmp(now, &machine->current_move.end) >= 0) {
+        return machine_execute_g1_end(machine, command, now);
+    }
+    return machine_execute_g1_tick(machine, command, now);
+}
+
+ExecutionResult machine_execute_g21(Machine* machine, const Command* command) {
+    machine->units = UNITS_MM;
+    ExecutionResult result = {
+        .status = EXECUTE_STATUS__FINISHED,
+        .has_data = false,
+    };
+    return result;
+}
+
+ExecutionResult machine_execute_g90(Machine* machine, const Command* command) {
+    machine->positioning_mode = POSITIONING_MODE_ABSOLUTE;
+    ExecutionResult result = {
+        .status = EXECUTE_STATUS__FINISHED,
+        .has_data = false,
+    };
+    return result;
+}
+
+ExecutionResult machine_execute_g91(Machine* machine, const Command* command) {
+    machine->positioning_mode = POSITIONING_MODE_RELATIVE;
+    ExecutionResult result = {
+        .status = EXECUTE_STATUS__FINISHED,
+        .has_data = false,
+    };
+    return result;
+}
+
+ExecutionResult machine_execute_g92(Machine* machine, const Command* command) {
+    if (
+        !command->param_x.is_set &&
+        !command->param_y.is_set &&
+        !command->param_z.is_set
+    ) {
+        machine->x = 0.0f;
+        machine->y = 0.0f;
+        machine->z = 0.0f;
+        ExecutionResult result = {
+            .status = EXECUTE_STATUS__FINISHED,
+            .has_data = false,
+        };
+        return result;
+    }
+
+    if (command->param_x.is_set) {
+        machine->x = command->param_x.value;
+    }
+    if (command->param_y.is_set) {
+        machine->y = command->param_y.value;
+    }
+    if (command->param_z.is_set) {
+        machine->z = command->param_z.value;
+    }
+    ExecutionResult result = {
+        .status = EXECUTE_STATUS__FINISHED,
+        .has_data = false,
+    };
+    return result;
+}
diff --git a/src/main.c b/src/main.c
@@ -0,0 +1,223 @@
+#include "token.h"
+#include "command.h"
+#include "machine.h"
+#include "timing.h"
+#include "tui.h"
+
+#include <assert.h>
+#include <string.h>
+#include <errno.h>
+
+void print_state(
+    const Machine* machine,
+    const ExecutionResult* execution_result
+) {
+    type(set_cursor(1, 2));
+    printf("gcode interpreter");
+    type(clear_to_end_of_line);
+
+    type(set_cursor(3, 2));
+    printf("machine state:");
+    type(clear_to_end_of_line);
+
+    type(set_cursor(4, 6));
+    printf(
+        "xyz: [ %4.3f %4.3f %4.3f ] (%s/sec)",
+        machine->x,
+        machine->y,
+        machine->z,
+        UNITS_STRINGS[machine->units]
+    );
+    type(clear_to_end_of_line);
+
+    type(set_cursor(5, 6));
+    printf(
+        "feed rate: [ %4.3f ] (%s/min)",
+        machine->feed_rate,
+        UNITS_STRINGS[machine->units]
+    );
+    type(clear_to_end_of_line);
+
+    type(set_cursor(6, 6));
+    printf(
+        "positioning_mode: %s",
+        POSITIONING_MODE_STRINGS[machine->positioning_mode]
+    );
+    type(clear_to_end_of_line);
+}
+
+void print_token_error(const Token* token, unsigned int* error_count) {
+    type(set_cursor(8, 2));
+    printf(
+        "%c [L%d C%d] -- %s\n",
+        token->type,
+        token->line_idx,
+        token->char_idx,
+        TOKEN_ERROR_STRINGS[token->value.error_value]
+    );
+    type(clear_to_end_of_line);
+
+    type(set_cursor(9, 2));
+    printf("total error count: %d", *error_count);
+    type(clear_to_end_of_line);
+
+    type(set_cursor(10, 2));
+    fprintf(
+        stderr,
+        "%c [L%d C%d] -- %s\n",
+        token->type,
+        token->line_idx,
+        token->char_idx,
+        TOKEN_ERROR_STRINGS[token->value.error_value]
+    );
+}
+
+void print_execution_error(
+    const Command* command,
+    const ExecutionResult* result,
+    unsigned int* error_count
+) {
+    type(set_cursor(8, 2));
+    printf(
+        "! [%c%-3d] -- %s",
+        command->code.type,
+        command->code.value,
+        MACHINE_ERROR_STRINGS[result->status]
+    );
+    type(clear_to_end_of_line);
+
+    type(set_cursor(9, 2));
+    printf("total error count: %d", *error_count);
+    type(clear_to_end_of_line);
+
+    type(set_cursor(10, 2));
+    fprintf(
+        stderr,
+        "! [%c%-3d] -- %s\n",
+        command->code.type,
+        command->code.value,
+        MACHINE_ERROR_STRINGS[result->status]
+    );
+}
+
+void save_output(
+    FILE* file,
+    const ExecutionResult* result,
+    const Time* now
+) {
+    if (!result->has_data) {
+        return;
+    }
+    fprintf(
+        file,
+        "%10ld (s) %10ld (ns) - [ %4.3f %4.3f %4.3f ]\n",
+        now->time_s,
+        now->time_ns,
+        result->data.x,
+        result->data.y,
+        result->data.z
+    );
+}
+
+int main(int argc, char** argv) {
+    freopen("error.log", "w", stderr);
+    initialize_terminal();
+
+    const char* filename = "CE3E3V2_3DBenchy.gcode";
+
+    FILE* file = fopen(filename, "r");
+    if (file == NULL) {
+        fprintf(stderr, "! [%s]: %s\n", filename, strerror(errno));
+        return -1;
+    }
+
+    const char* output_filename = "out";
+    FILE* output_file = fopen(output_filename, "w");
+    if (output_file == NULL) {
+        fprintf(stderr, "! [%s]: %s\n", filename, strerror(errno));
+        return -1;
+    }
+
+    Cursor cursor;
+    cursor_init(&cursor, file);
+
+    Command command;
+    command_init(&command);
+
+    Machine machine;
+    machine_init(&machine);
+
+    Token token;
+    CommandStatus command_status = COMMAND_STATUS__NOT_READY;
+    ExecutionResult execution_result = {
+        .status = EXECUTE_STATUS__IDLE,
+        .has_data = false,
+    };
+
+    unsigned int error_count = 0;
+
+    print_state(&machine, &execution_result);
+
+    Time now;
+    for (;;) {
+        now = time_get();
+
+        switch (execution_result.status) {
+            case EXECUTE_STATUS__IDLE: {
+                token = parse_token(&cursor);
+                if (token.type == TOKEN_ERROR) {
+                    error_count += 1;
+                    print_token_error(&token, &error_count);
+                    recover_from_error(&cursor, &token);
+                }
+                if (token.type == TOKEN_EOF) {
+                    return 0;
+                }
+
+                command_status = command_process_token(&command, &token);
+                if (command_status == COMMAND_STATUS__READY) {
+                    execution_result = machine_execute_command(
+                        &machine,
+                        &command,
+                        &now
+                    );
+                    save_output(output_file, &execution_result, &now);
+                    print_state(&machine, &execution_result);
+                }
+                break;
+            }
+            case EXECUTE_STATUS__IN_PROGRESS: {
+                execution_result = machine_execute_command(
+                    &machine,
+                    &command,
+                    &now
+                );
+                save_output(output_file, &execution_result, &now);
+                print_state(&machine, &execution_result);
+                break;
+            }
+            case EXECUTE_STATUS__UNRECOGNIZED_COMMAND:
+            case EXECUTE_STATUS__UNRECOGNIZED_MCODE:
+            case EXECUTE_STATUS__UNRECOGNIZED_GCODE:
+                error_count += 1;
+                print_execution_error(
+                    &command,
+                    &execution_result,
+                    &error_count
+                );
+            case EXECUTE_STATUS__FINISHED: {
+                command_init(&command);
+                command_status = command_process_token(&command, &token);
+                assert(command_status == COMMAND_STATUS__NOT_READY);
+                execution_result.status = EXECUTE_STATUS__IDLE;
+                execution_result.has_data = false;
+                break;
+            }
+            default:
+                return 0;
+        }
+
+    }
+    return 0;
+}
+
diff --git a/src/timing.c b/src/timing.c
@@ -0,0 +1,62 @@
+#include "timing.h"
+
+#include <time.h>
+
+Time time_make(long time_s, long time_ns) {
+    const long billion = 1000000000;
+    long overflow = (time_ns >= 0 ? time_ns : time_ns - (billion - 1)) / billion;
+    Time t = {
+        .time_s = time_s + overflow,
+        .time_ns = time_ns - overflow * billion,
+    };
+    return t;
+}
+
+Time time_zero() {
+    return time_make(0, 0);
+};
+
+Time time_get() {
+    struct timespec ts;
+    clock_gettime(CLOCK_MONOTONIC, &ts);
+    return time_make(ts.tv_sec, ts.tv_nsec);
+};
+
+Time time_add(const Time* t0, const Time* t1) {
+    long time_s = t0->time_s + t1->time_s;
+    long time_ns = t0->time_ns + t1->time_ns;
+    return time_make(time_s, time_ns);
+}
+
+Time time_sub(const Time* t0, const Time* t1) {
+    long time_s = t0->time_s - t1->time_s;
+    long time_ns = t0->time_ns - t1->time_ns;
+    return time_make(time_s, time_ns);
+}
+
+float time_div(const Time* t0, const Time* t1) {
+    long denominator = t1->time_s * 1e9 + t1->time_ns;
+    float q_a = ((float)t0->time_s / denominator) * 1e9;
+    float q_b = (float)t0->time_ns / denominator;
+    return q_a + q_b;
+}
+
+int time_cmp(const Time* t0, const Time* t1) {
+    if (t0->time_s > t1->time_s) {
+        return 1;
+    }
+    if (t0->time_s < t1->time_s) {
+        return -1;
+    }
+    if (t0->time_ns > t1->time_ns) {
+        return 1;
+    }
+    if (t0->time_ns < t1->time_ns) {
+        return -1;
+    }
+    return 0;
+}
+
+bool time_eql_zero(const Time* t) {
+    return t->time_s == 0 && t->time_ns == 0;
+}
diff --git a/src/token.c b/src/token.c
@@ -0,0 +1,309 @@
+#include "token.h"
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+#define MAX_FIELD_LEN 31
+
+void cursor_init(Cursor* cursor, FILE* file) {
+    cursor->file = file;
+    cursor->line_idx = 1;
+    cursor->char_idx = 1;
+}
+
+char cursor_getc(Cursor* cursor) {
+    char c = fgetc(cursor->file);
+    switch (c) {
+        case EOF:
+            break;
+        case '\n':
+            cursor->line_idx += 1;
+        case '\r':
+            cursor->char_idx = 0;
+            break;
+        default:
+            cursor->char_idx += 1;
+    }
+    return c;
+}
+
+void cursor_ungetc(Cursor* cursor, char c) {
+    ungetc(c, cursor->file);
+    switch (c) {
+        case EOF:
+            break;
+        default:
+            cursor->char_idx -= 1;
+    }
+}
+
+typedef enum {
+    TOKEN_STATUS__FIELD_VALUE_TOO_LONG,
+    TOKEN_STATUS__INVALID_CHAR_INT,
+    TOKEN_STATUS__NO_DIGITS_INT,
+    TOKEN_STATUS__INVALID_CHAR_FLOAT,
+    TOKEN_STATUS__NO_DIGITS_FLOAT,
+    TOKEN_STATUS__VALUE_PARSING_NOT_SUPPORTED,
+    TOKEN_STATUS__UNRECOGNIZED,
+    TOKEN_STATUS__OKAY,
+} TokenStatus;
+
+void skip_whitespace(Cursor* cursor) {
+    for (;;) {
+        char c = cursor_getc(cursor);
+        switch (c) {
+            case ' ':
+            case '\t':
+            case '\n':
+            case '\r':
+                continue;
+            default:
+                cursor_ungetc(cursor, c);
+                return;
+        }
+    }
+}
+
+void skip_until_whitespace(Cursor* cursor) {
+    for (;;) {
+        char c = cursor_getc(cursor);
+        switch (c) {
+            case EOF:
+            case ' ':
+            case '\t':
+            case '\n':
+            case '\r':
+                return;
+            default:
+                continue;
+        }
+    }
+}
+
+void skip_until_newline(Cursor* cursor) {
+    for (;;) {
+        char c = cursor_getc(cursor);
+        switch (c) {
+            case EOF:
+            case '\n':
+                return;
+            default:
+                continue;
+        }
+    }
+}
+
+typedef struct {
+    int length;
+    TokenStatus status;
+} ReadIntoBufferResult;
+
+ReadIntoBufferResult read_into_buffer(
+    Cursor* cursor,
+    char* buffer,
+    int max_len
+)
+{
+    ReadIntoBufferResult result;
+    int i;
+    char c;
+    for (i = 0; i < max_len; i += 1) {
+        c = cursor_getc(cursor);
+        switch (c) {
+            case EOF:
+            case ' ':
+            case '\t':
+            case '\n':
+            case '\r':
+                buffer[i] = 0;
+                result.length = i;
+                result.status = TOKEN_STATUS__OKAY;
+                return result;
+            default:
+                buffer[i] = c;
+                continue;
+        }
+    }
+    cursor_ungetc(cursor, c);
+    buffer[max_len - 1] = 0;
+    result.length = i;
+    result.status = TOKEN_STATUS__FIELD_VALUE_TOO_LONG;
+    return result;
+}
+
+typedef struct {
+    int value;
+    TokenStatus status;
+} ParseIntResult;
+
+ParseIntResult parse_int(char* str) {
+    ParseIntResult result;
+    char* end;
+    result.value = strtol(str, &end, 10);
+    if (end == str) {
+        result.status = TOKEN_STATUS__NO_DIGITS_INT;
+    }
+    else if (*end != '\0') {
+        result.status = TOKEN_STATUS__INVALID_CHAR_INT;
+    }
+    else {
+        result.status = TOKEN_STATUS__OKAY;
+    }
+    return result;
+}
+
+typedef struct {
+    float value;
+    TokenStatus status;
+} ParseFloatResult;
+
+ParseFloatResult parse_float(char* str) {
+    ParseFloatResult result;
+    char* end;
+    result.value = strtof(str, &end);
+    if (end == str) {
+        result.status = TOKEN_STATUS__NO_DIGITS_FLOAT;
+    }
+    else if (*end != '\0') {
+        result.status = TOKEN_STATUS__INVALID_CHAR_FLOAT;
+    }
+    else {
+        result.status = TOKEN_STATUS__OKAY;
+    }
+    return result;
+}
+
+Token parse_field(Cursor* cursor, TokenType type) {
+    Token t;
+    t.line_idx = cursor->line_idx;
+    t.char_idx = cursor->char_idx - 1;
+
+    char buffer[MAX_FIELD_LEN + 1];
+    ReadIntoBufferResult result = read_into_buffer(
+        cursor,
+        buffer,
+        MAX_FIELD_LEN + 1
+    );
+    if (result.status != TOKEN_STATUS__OKAY) {
+        t.type = TOKEN_ERROR;
+        t.value.error_value = result.status;
+        return t;
+    }
+
+    TokenValue value;
+    switch (type) {
+        case TOKEN_G_CODE:
+        case TOKEN_M_CODE: {
+            ParseIntResult result = parse_int(buffer);
+            if (result.status != TOKEN_STATUS__OKAY) {
+                t.type = TOKEN_ERROR;
+                t.value.error_value = result.status;
+            }
+            else {
+                t.type = type;
+                t.value.code_value = result.value;
+            }
+            break;
+        }
+        case TOKEN_S_PARAM:
+        case TOKEN_X_PARAM:
+        case TOKEN_Y_PARAM:
+        case TOKEN_Z_PARAM:
+        case TOKEN_F_PARAM:
+        case TOKEN_E_PARAM: {
+            ParseFloatResult result = parse_float(buffer);
+            if (result.status != TOKEN_STATUS__OKAY) {
+                t.type = TOKEN_ERROR;
+                t.value.error_value = result.status;
+            }
+            else {
+                t.type = type;
+                t.value.param_value = result.value;
+            }
+            break;
+        }
+        default:
+            t.type = TOKEN_ERROR;
+            t.value.error_value = TOKEN_STATUS__VALUE_PARSING_NOT_SUPPORTED;
+            break;
+    }
+    return t;
+}
+
+Token parse_comment(Cursor* cursor) {
+    Token t = {
+        .type = TOKEN_COMMENT,
+        .line_idx = cursor->line_idx,
+        .char_idx = cursor->char_idx - 1,
+    };
+    skip_until_newline(cursor);
+    return t;
+}
+
+Token parse_token(Cursor* cursor) {
+    skip_whitespace(cursor);
+
+    char c = cursor_getc(cursor);
+    switch (c) {
+        case ';':
+            return parse_comment(cursor);
+        case 'g':
+        case 'G':
+            return parse_field(cursor, TOKEN_G_CODE);
+        case 'm':
+        case 'M':
+            return parse_field(cursor, TOKEN_M_CODE);
+        case 's':
+        case 'S':
+            return parse_field(cursor, TOKEN_S_PARAM);
+        case 'x':
+        case 'X':
+            return parse_field(cursor, TOKEN_X_PARAM);
+        case 'y':
+        case 'Y':
+            return parse_field(cursor, TOKEN_Y_PARAM);
+        case 'z':
+        case 'Z':
+            return parse_field(cursor, TOKEN_Z_PARAM);
+        case 'f':
+        case 'F':
+            return parse_field(cursor, TOKEN_F_PARAM);
+        case 'e':
+        case 'E':
+            return parse_field(cursor, TOKEN_E_PARAM);
+        case EOF: {
+            Token t = {
+                .type = TOKEN_EOF,
+                .line_idx = cursor->line_idx,
+                .char_idx = cursor->char_idx,
+            };
+            return t;
+        }
+        default: {
+            cursor_ungetc(cursor, c);
+            Token t = {
+                .type = TOKEN_ERROR,
+                .line_idx = cursor->line_idx,
+                .char_idx = cursor->char_idx,
+                .value.error_value = TOKEN_STATUS__UNRECOGNIZED,
+            };
+            return t;
+        }
+    }
+}
+
+void recover_from_error(Cursor* cursor, const Token* token) {
+    switch (token->value.error_value) {
+        case TOKEN_STATUS__FIELD_VALUE_TOO_LONG:
+        case TOKEN_STATUS__UNRECOGNIZED:
+            skip_until_whitespace(cursor);
+            break;
+        default:
+            // most errors don't require any action to get back to a valid
+            // parsing state.
+            break;
+    }
+}
+
diff --git a/src/tui.c b/src/tui.c
@@ -0,0 +1,62 @@
+#include "tui.h"
+
+#include <stdlib.h>
+#include <signal.h>
+#include <termios.h>
+#include <unistd.h>
+#include <stdio.h>
+
+struct termios initial;
+
+void restore_terminal() {
+    type(
+        switch_to alternate_buffer
+        clear_buffer
+        switch_to main_buffer
+        show_cursor
+    );
+    // restore original termios params
+    tcsetattr(1, TCSANOW, &initial);
+}
+
+void restore_terminal_exit(int i) {
+    exit(1);
+}
+
+void initialize_terminal() {
+    // 1. save initial termios params
+    // 2. disable echo and canonical mode
+    struct termios t;
+    tcgetattr(1, &t);
+    initial = t;
+    t.c_lflag &= (~ECHO & ~ICANON);
+    tcsetattr(1, TCSANOW, &t);
+
+    // register cleanup function
+    atexit(restore_terminal);
+    signal(SIGTERM, restore_terminal_exit);
+    signal(SIGINT, restore_terminal_exit);
+
+    // disable output buffering
+    setvbuf(stdout, NULL, _IONBF, 0);
+
+    type(
+        switch_to alternate_buffer
+        clear_buffer
+        hide_cursor
+    );
+}
+
+void type_line(const char* msg, unsigned int line) {
+    const unsigned int buf_len = 64;
+    char buf[buf_len];
+    unsigned int str_len = snprintf(
+        buf,
+        buf_len - 1,
+        set_cursor("%d", "1"),
+        line
+    );
+    buf[str_len] = '\0';
+    type(buf);
+    //printf(msg);
+}