feat: sync latest run execution updates

This commit is contained in:
2026-06-22 21:47:16 -04:00
parent 0b1aad39e1
commit 8d3177cb73
92 changed files with 22837 additions and 246 deletions

View File

@@ -143,3 +143,6 @@ When extending this workspace:
2. Keep extraction and patching reproducible.
3. Keep adapters narrow and explicit.
4. Keep LinuxCNC-derived logic traceable to its upstream file origin.
5. After every GPT/Codex execution completes, append the full execution
process log to
`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`.

View File

@@ -7,6 +7,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <map>
#include <sstream>
#include <string>
@@ -14,6 +15,17 @@
namespace {
struct MotionSegment {
int line = 0;
std::string type;
std::string motion_class;
double start_seconds = 0.0;
double duration_seconds = 0.0;
double velocity_mm_per_min = 0.0;
std::map<std::string, double> start_axes;
std::map<std::string, double> end_axes;
};
struct TaskRuntime {
bool initialized = false;
std::string state = "ESTOP";
@@ -22,9 +34,16 @@ struct TaskRuntime {
std::string exec_state = "DONE";
std::string open_program;
int opened_line_count = 0;
int opened_source_line_count = 0;
int executable_line_count = 0;
int next_program_line = 0;
long long task_cycle = 0;
long long servo_cycle = 0;
bool motion_plan_loaded = false;
int active_segment_index = 0;
double run_elapsed_seconds = 0.0;
std::string motion_plan_program_path;
std::vector<MotionSegment> motion_plan;
std::map<std::string, std::string> staged_files;
std::vector<std::string> program_lines;
std::vector<std::string> events;
@@ -141,6 +160,111 @@ int json_int_after(const char *json, const char *key, int fallback)
return static_cast<int>(json_number_after(json, key, fallback));
}
std::size_t matching_brace(const std::string &text, std::size_t open)
{
int depth = 0;
bool in_string = false;
bool escaped = false;
for (std::size_t i = open; i < text.size(); ++i) {
const char ch = text[i];
if (in_string) {
if (escaped) {
escaped = false;
} else if (ch == '\\') {
escaped = true;
} else if (ch == '"') {
in_string = false;
}
continue;
}
if (ch == '"') {
in_string = true;
} else if (ch == '{') {
depth += 1;
} else if (ch == '}') {
depth -= 1;
if (depth == 0) {
return i;
}
}
}
return std::string::npos;
}
std::string json_object_after(const std::string &json, const char *key)
{
const std::size_t key_pos = json.find(key);
if (key_pos == std::string::npos) {
return "";
}
const std::size_t open = json.find('{', key_pos);
if (open == std::string::npos) {
return "";
}
const std::size_t close = matching_brace(json, open);
if (close == std::string::npos) {
return "";
}
return json.substr(open, close - open + 1);
}
std::map<std::string, double> json_axes_object_after(const std::string &json, const char *key)
{
std::map<std::string, double> axes;
const std::string object = json_object_after(json, key);
for (const char *axis : {"x", "y", "z", "a", "b", "c", "u", "v", "w"}) {
axes[axis] = json_number_after(object.c_str(), (std::string("\"") + axis + "\"").c_str(), 0.0);
}
return axes;
}
std::vector<std::string> json_segment_objects(const std::string &json)
{
std::vector<std::string> segments;
std::size_t at = json.find("\"segments\"");
if (at == std::string::npos) {
return segments;
}
at = json.find('[', at);
if (at == std::string::npos) {
return segments;
}
while (at < json.size()) {
const std::size_t open = json.find('{', at);
const std::size_t close_array = json.find(']', at);
if (open == std::string::npos || (close_array != std::string::npos && close_array < open)) {
break;
}
const std::size_t close = matching_brace(json, open);
if (close == std::string::npos) {
break;
}
segments.push_back(json.substr(open, close - open + 1));
at = close + 1;
}
return segments;
}
double clamp_double(double value, double low, double high)
{
return std::min(std::max(value, low), high);
}
std::string axis_json(const std::map<std::string, double> &start_axes,
const std::map<std::string, double> &end_axes,
double progress)
{
std::ostringstream out;
for (const char *axis : {"x", "y", "z", "a", "b", "c"}) {
const auto start_it = start_axes.find(axis);
const auto end_it = end_axes.find(axis);
const double start = start_it == start_axes.end() ? 0.0 : start_it->second;
const double end = end_it == end_axes.end() ? start : end_it->second;
out << ",\"" << axis << "\":" << (start + (end - start) * progress);
}
return out.str();
}
std::string trim_copy(const std::string &value)
{
std::size_t begin = 0;
@@ -198,12 +322,104 @@ void enqueue_linear_move_from_line(TaskRuntime &state, const std::string &line)
line.find('B') == std::string::npos && line.find('C') == std::string::npos) {
command << ",\"x\":" << fallback;
}
command << ",\"velocity\":60}";
const std::size_t feed_pos = line.find('F');
double velocity_units_per_second = 60.0;
if (feed_pos != std::string::npos) {
char *end = nullptr;
const double feed_units_per_minute = std::strtod(line.c_str() + feed_pos + 1, &end);
if (end != line.c_str() + feed_pos + 1 && feed_units_per_minute > 0.0) {
velocity_units_per_second = feed_units_per_minute / 60.0;
}
}
command << ",\"velocity\":" << velocity_units_per_second << "}";
forward_motion_command(command.str());
state.events.push_back("task_queue_motion_line:" + std::to_string(line_number));
state.next_program_line += 1;
}
bool load_motion_plan(TaskRuntime &state, const char *plan_json)
{
if (!plan_json) {
return false;
}
const std::string json(plan_json);
std::vector<MotionSegment> segments;
for (const std::string &item : json_segment_objects(json)) {
MotionSegment segment;
segment.line = json_int_after(item.c_str(), "\"line\"", 0);
segment.type = json_string_after(item.c_str(), "\"type\"", "");
segment.motion_class = json_string_after(item.c_str(), "\"motionClass\"", "");
segment.start_seconds = json_number_after(item.c_str(), "\"startSeconds\"", 0.0);
segment.duration_seconds = std::max(json_number_after(item.c_str(), "\"durationSeconds\"", 0.0), 0.0);
segment.velocity_mm_per_min = std::max(json_number_after(item.c_str(), "\"velocityMmPerMin\"", 0.0), 0.0);
segment.start_axes = json_axes_object_after(item, "\"startAxes\"");
segment.end_axes = json_axes_object_after(item, "\"endAxes\"");
if (segment.line <= 0) {
segment.line = static_cast<int>(segments.size()) + 1;
}
segments.push_back(segment);
}
if (segments.empty()) {
return false;
}
state.motion_plan = std::move(segments);
state.motion_plan_loaded = true;
state.active_segment_index = 0;
state.run_elapsed_seconds = 0.0;
state.motion_plan_program_path = json_string_after(plan_json, "\"programPath\"", state.open_program);
if (!state.motion_plan.empty()) {
state.opened_source_line_count = std::max(
state.opened_source_line_count,
state.motion_plan.back().line);
}
state.events.push_back("task_motion_plan_loaded:" + std::to_string(state.motion_plan.size()));
return true;
}
void forward_timed_motion_sample(TaskRuntime &state)
{
if (!state.motion_plan_loaded || state.motion_plan.empty()) {
return;
}
while (state.active_segment_index < static_cast<int>(state.motion_plan.size()) - 1) {
const MotionSegment &segment = state.motion_plan[state.active_segment_index];
const double end_seconds = segment.start_seconds + segment.duration_seconds;
if (state.run_elapsed_seconds < end_seconds) {
break;
}
state.active_segment_index += 1;
}
if (state.active_segment_index >= static_cast<int>(state.motion_plan.size())) {
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.next_program_line = std::max(state.opened_line_count, state.opened_source_line_count);
return;
}
const MotionSegment &segment = state.motion_plan[state.active_segment_index];
const double duration = std::max(segment.duration_seconds, 0.000001);
const double progress = clamp_double((state.run_elapsed_seconds - segment.start_seconds) / duration, 0.0, 1.0);
const double velocity_units_per_second = segment.velocity_mm_per_min / 60.0;
std::ostringstream command;
command << "{\"type\":\"EMC_TRAJ_LINEAR_MOVE\",\"source\":\"feed_timed_motion_plan\"";
command << ",\"line\":" << segment.line;
command << axis_json(segment.start_axes, segment.end_axes, progress);
command << ",\"velocity\":" << velocity_units_per_second;
command << ",\"currentVel\":" << velocity_units_per_second;
command << ",\"requestedVel\":" << velocity_units_per_second;
command << ",\"segmentProgress\":" << progress << "}";
forward_motion_command(command.str());
const int max_line = std::max(state.opened_line_count, state.opened_source_line_count);
state.next_program_line = std::min(std::max(segment.line, 1), std::max(max_line, 1));
if (state.active_segment_index == static_cast<int>(state.motion_plan.size()) - 1 && progress >= 1.0) {
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.next_program_line = std::max(state.opened_line_count, state.opened_source_line_count);
state.events.push_back("task_motion_plan_complete");
}
}
void enqueue_mdi(TaskRuntime &state, const char *json)
{
const std::string mdi = json_string_after(json, "\"mdi\"");
@@ -265,6 +481,8 @@ std::string status_json()
out << ",\"cycle\":" << state.task_cycle;
out << ",\"openProgram\":\"" << json_escape(state.open_program) << "\"";
out << ",\"openedLineCount\":" << state.opened_line_count;
out << ",\"openedSourceLineCount\":" << state.opened_source_line_count;
out << ",\"executableLineCount\":" << state.executable_line_count;
out << ",\"nextProgramLine\":" << state.next_program_line << "}";
out << ",\"servoCycle\":" << state.servo_cycle;
out << ",\"motionStatus\":" << read_motion_status_json();
@@ -341,14 +559,30 @@ int lctask_open_program(const char *path)
}
state.open_program = path;
state.program_lines = split_program_lines(it->second);
state.opened_line_count = static_cast<int>(state.program_lines.size());
state.executable_line_count = static_cast<int>(state.program_lines.size());
state.opened_source_line_count = static_cast<int>(std::count(it->second.begin(), it->second.end(), '\n'));
if (!it->second.empty() && it->second.back() != '\n') {
state.opened_source_line_count += 1;
}
state.opened_line_count = state.opened_source_line_count;
state.next_program_line = 0;
state.active_segment_index = 0;
state.run_elapsed_seconds = 0.0;
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.events.push_back(std::string("task_open_program:") + path);
return 0;
}
int lctask_load_program_motion_plan_json(const char *plan_json)
{
auto &state = task_runtime();
if (!state.initialized || !plan_json) {
return -1;
}
return load_motion_plan(state, plan_json) ? 0 : -1;
}
int lctask_send_command_json(const char *command_json)
{
auto &state = task_runtime();
@@ -373,9 +607,11 @@ int lctask_send_command_json(const char *command_json)
if (state.next_program_line < 0) {
state.next_program_line = 0;
}
if (state.next_program_line >= state.opened_line_count) {
if (!state.motion_plan_loaded && state.next_program_line >= state.executable_line_count) {
state.next_program_line = 0;
}
state.active_segment_index = 0;
state.run_elapsed_seconds = 0.0;
state.interp_state = "READING";
state.exec_state = "WAITING_FOR_MOTION";
state.events.push_back("task_plan_run");
@@ -396,6 +632,7 @@ int lctask_send_command_json(const char *command_json)
if (contains_token(command_json, "EMC_TASK_ABORT")) {
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.run_elapsed_seconds = 0.0;
state.events.push_back("task_abort");
return forward_motion_command("{\"type\":\"EMC_TRAJ_ABORT\"}");
}
@@ -413,6 +650,17 @@ int lctask_send_command_json(const char *command_json)
state.events.push_back("task_jog_incr");
return forward_motion_command(command_json);
}
if (contains_token(command_json, "EMC_JOINT_HOME")) {
state.mode = "MANUAL";
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.next_program_line = 0;
state.active_segment_index = 0;
state.run_elapsed_seconds = 0.0;
state.events.push_back("task_joint_home");
forward_motion_command("{\"type\":\"EMC_TRAJ_LINEAR_MOVE\",\"line\":1,\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"b\":0,\"c\":0,\"velocity\":0}");
return 0;
}
return -1;
}
@@ -431,9 +679,18 @@ int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles
}
for (int i = 0; i < task_cycles; ++i) {
state.task_cycle += 1;
if (state.interp_state == "READING" && state.next_program_line < state.opened_line_count) {
enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]);
if (state.next_program_line >= state.opened_line_count) {
const bool has_program_work = state.motion_plan_loaded
? state.active_segment_index < static_cast<int>(state.motion_plan.size())
: state.next_program_line < state.executable_line_count;
if (state.interp_state == "READING" && has_program_work) {
if (state.motion_plan_loaded) {
const double delta_seconds = task_period_ns > 0 ? static_cast<double>(task_period_ns) / 1000000000.0 : 0.01;
state.run_elapsed_seconds += delta_seconds;
forward_timed_motion_sample(state);
} else {
enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]);
}
if (!state.motion_plan_loaded && state.next_program_line >= state.executable_line_count) {
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.events.push_back("task_plan_complete");

View File

@@ -7,6 +7,7 @@ extern "C" {
int lctask_init_session(const char *session_json);
int lctask_stage_file(const char *path, const char *text);
int lctask_open_program(const char *path);
int lctask_load_program_motion_plan_json(const char *plan_json);
int lctask_send_command_json(const char *command_json);
int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles);
int lctask_read_status_json(char *out, int out_len);

View File

@@ -97,6 +97,10 @@ export async function createLinuxCncTaskHalSdk(moduleOptions = {}) {
);
},
loadProgramMotionPlan(plan) {
return callWithJson(mod, "lctask_load_program_motion_plan_json", plan);
},
sendCommand(command) {
return callWithJson(mod, "lctask_send_command_json", command);
},

View File

@@ -52,5 +52,116 @@ const events = sdk.readEvents().events;
assert.equal(events.includes("task_plan_run"), true);
assert.equal(events.includes("task_mdi_switchkins:M429"), true);
sdk.resetSession();
sdk.initSession({
iniPath: "xyzac-trt.ini",
iniText: "[TRAJ]\nCOORDINATES = X Y Z A C\n",
});
assert.equal(sdk.stageFile("programs/feed-plan.ngc", "G1 X120 F60\nG1 X121 F600\n"), 0);
assert.equal(sdk.openProgram("programs/feed-plan.ngc"), 0);
assert.equal(sdk.loadProgramMotionPlan({
programPath: "programs/feed-plan.ngc",
segments: [
{
line: 1,
type: "STRAIGHT_FEED",
motionClass: "feed",
feedMode: "units-per-minute",
startSeconds: 0,
durationSeconds: 120,
velocityMmPerMin: 60,
startAxes: { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0 },
endAxes: { x: 120, y: 0, z: 0, a: 0, b: 0, c: 0 },
},
{
line: 2,
type: "STRAIGHT_FEED",
motionClass: "feed",
feedMode: "units-per-minute",
startSeconds: 120,
durationSeconds: 0.1,
velocityMmPerMin: 600,
startAxes: { x: 120, y: 0, z: 0, a: 0, b: 0, c: 0 },
endAxes: { x: 121, y: 0, z: 0, a: 0, b: 0, c: 0 },
},
],
}), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" }), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 }), 0);
assert.equal(sdk.runCycles({ taskCycles: 1, taskPeriodNs: 1000000000, servoPeriodNs: 1000000 }), 0);
status = sdk.readStatus();
assert.equal(status.motionStatus.motion.programLine, 1);
assert.equal(status.motionStatus.axis.x > 0 && status.motionStatus.axis.x < 2, true);
assert.equal(status.motionStatus.motion.currentVel, 1);
assert.equal(status.motionStatus.motion.currentVel !== 60, true);
assert.equal(sdk.runCycles({ taskCycles: 119, taskPeriodNs: 1000000000, servoPeriodNs: 1000000 }), 0);
status = sdk.readStatus();
assert.equal(status.motionStatus.motion.programLine, 2);
assert.equal(status.motionStatus.motion.currentVel, 10);
assert.equal(status.motionStatus.axis.x >= 120, true);
sdk.resetSession();
sdk.initSession({
iniPath: "xyzac-trt.ini",
iniText: "[TRAJ]\nCOORDINATES = X Y Z A C\n",
});
assert.equal(sdk.stageFile("programs/comment-lines.ngc", [
"(comment before first move)",
"",
"G1 X1 F60",
"; inline comment-only line",
"G1 X2 F60",
"M2",
].join("\n")), 0);
assert.equal(sdk.openProgram("programs/comment-lines.ngc"), 0);
assert.equal(sdk.loadProgramMotionPlan({
programPath: "programs/comment-lines.ngc",
segments: [
{
line: 3,
type: "STRAIGHT_FEED",
motionClass: "feed",
feedMode: "units-per-minute",
startSeconds: 0,
durationSeconds: 1,
velocityMmPerMin: 60,
startAxes: { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0 },
endAxes: { x: 1, y: 0, z: 0, a: 0, b: 0, c: 0 },
},
{
line: 5,
type: "STRAIGHT_FEED",
motionClass: "feed",
feedMode: "units-per-minute",
startSeconds: 1,
durationSeconds: 1,
velocityMmPerMin: 60,
startAxes: { x: 1, y: 0, z: 0, a: 0, b: 0, c: 0 },
endAxes: { x: 2, y: 0, z: 0, a: 0, b: 0, c: 0 },
},
],
}), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" }), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 }), 0);
assert.equal(sdk.runCycles({ taskCycles: 1, taskPeriodNs: 500000000, servoPeriodNs: 1000000 }), 0);
status = sdk.readStatus();
assert.equal(status.task.openedLineCount, 6);
assert.equal(status.task.openedSourceLineCount, 6);
assert.equal(status.task.executableLineCount, 3);
assert.equal(status.motionStatus.motion.programLine, 3);
assert.equal(status.halSnapshot.pins["motion.program-line"].value, 3);
assert.equal(sdk.runCycles({ taskCycles: 1, taskPeriodNs: 600000000, servoPeriodNs: 1000000 }), 0);
status = sdk.readStatus();
assert.equal(status.motionStatus.motion.programLine, 5);
assert.equal(status.halSnapshot.pins["motion.program-line"].value, 5);
assert.equal(status.task.nextProgramLine, 5);
assert.equal(sdk.runCycles({ taskCycles: 1, taskPeriodNs: 1000000000, servoPeriodNs: 1000000 }), 0);
status = sdk.readStatus();
assert.equal(status.task.nextProgramLine, 6);
console.log("linuxcnc_task_hal_sdk=ok");
console.log("task_hal_sdk_status_snapshot=ok");
console.log("task_hal_feed_timed_motion_plan=ok");
console.log("task_hal_comment_source_line_numbers=ok");

View File

@@ -52,7 +52,7 @@ link_wasm_module \
-s ENVIRONMENT=web,node \
-s ALLOW_MEMORY_GROWTH=1 \
-s NO_EXIT_RUNTIME=1 \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_hal_init","_hal_ready","_hal_exit","_hal_malloc","_hal_pin_bit_new","_hal_pin_float_new","_hal_pin_s32_new","_hal_pin_u32_new","_hal_pin_s64_new","_hal_pin_u64_new","_hal_pin_bit_newf","_hal_pin_float_newf","_hal_pin_s32_newf","_hal_pin_u32_newf","_hal_pin_s64_newf","_hal_pin_u64_newf","_hal_param_bit_new","_hal_param_float_new","_hal_param_s32_new","_hal_param_u32_new","_hal_param_s64_new","_hal_param_u64_new","_hal_param_bit_newf","_hal_param_float_newf","_hal_param_s32_newf","_hal_param_u32_newf","_hal_param_s64_newf","_hal_param_u64_newf","_hal_get_pin_value_by_name","_hal_get_signal_value_by_name","_hal_get_param_value_by_name","_hal_link","_hal_unlink","_hal_set_p","_hal_get_p","_hal_create_thread","_hal_add_funct_to_thread","_hal_del_funct_from_thread","_hal_start_threads","_hal_stop_threads","_lchal_init_runtime","_lchal_load_hal_file","_lchal_set_pin_float","_lchal_set_pin_s32","_lchal_set_pin_bit","_lchal_get_pin_json","_lchal_get_snapshot_json","_lchal_step_threads","_lchal_reset_runtime","_lcmot_init_from_ini","_lcmot_write_command_json","_lcmot_step_servo","_lcmot_read_status_json","_lcmot_read_hal_snapshot_json","_lcmot_reset","_lctask_init_session","_lctask_stage_file","_lctask_open_program","_lctask_send_command_json","_lctask_run_cycles","_lctask_read_status_json","_lctask_read_events_json","_lctask_reset_session"]' \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_hal_init","_hal_ready","_hal_exit","_hal_malloc","_hal_pin_bit_new","_hal_pin_float_new","_hal_pin_s32_new","_hal_pin_u32_new","_hal_pin_s64_new","_hal_pin_u64_new","_hal_pin_bit_newf","_hal_pin_float_newf","_hal_pin_s32_newf","_hal_pin_u32_newf","_hal_pin_s64_newf","_hal_pin_u64_newf","_hal_param_bit_new","_hal_param_float_new","_hal_param_s32_new","_hal_param_u32_new","_hal_param_s64_new","_hal_param_u64_new","_hal_param_bit_newf","_hal_param_float_newf","_hal_param_s32_newf","_hal_param_u32_newf","_hal_param_s64_newf","_hal_param_u64_newf","_hal_get_pin_value_by_name","_hal_get_signal_value_by_name","_hal_get_param_value_by_name","_hal_link","_hal_unlink","_hal_set_p","_hal_get_p","_hal_create_thread","_hal_add_funct_to_thread","_hal_del_funct_from_thread","_hal_start_threads","_hal_stop_threads","_lchal_init_runtime","_lchal_load_hal_file","_lchal_set_pin_float","_lchal_set_pin_s32","_lchal_set_pin_bit","_lchal_get_pin_json","_lchal_get_snapshot_json","_lchal_step_threads","_lchal_reset_runtime","_lcmot_init_from_ini","_lcmot_write_command_json","_lcmot_step_servo","_lcmot_read_status_json","_lcmot_read_hal_snapshot_json","_lcmot_reset","_lctask_init_session","_lctask_stage_file","_lctask_open_program","_lctask_load_program_motion_plan_json","_lctask_send_command_json","_lctask_run_cycles","_lctask_read_status_json","_lctask_read_events_json","_lctask_reset_session"]' \
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString","stringToUTF8","lengthBytesUTF8","getValue","setValue"]'
echo "linuxcnc_task_hal_wasm_build=ok"