接入 LinuxCNC TP 运行反馈
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
#include <stdarg.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -91,6 +92,76 @@ static int pose_near(const EmcPose *actual, const EmcPose *expected)
|
|||||||
fabs(actual->tran.z - expected->tran.z) < 1e-9;
|
fabs(actual->tran.z - expected->tran.z) < 1e-9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *find_json_key(const char *cursor, const char *key)
|
||||||
|
{
|
||||||
|
if (!cursor || !key) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char pattern[64];
|
||||||
|
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
||||||
|
const char *match = strstr(cursor, pattern);
|
||||||
|
if (!match) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
match = strchr(match + strlen(pattern), ':');
|
||||||
|
return match ? match + 1 : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double read_json_number_in_object(const char *object_start, const char *object_end, const char *key, double fallback)
|
||||||
|
{
|
||||||
|
const char *value = find_json_key(object_start, key);
|
||||||
|
if (!value || value >= object_end) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
char *end = NULL;
|
||||||
|
const double number = strtod(value, &end);
|
||||||
|
if (end == value || end > object_end || !isfinite(number)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int read_json_string_in_object(
|
||||||
|
const char *object_start,
|
||||||
|
const char *object_end,
|
||||||
|
const char *key,
|
||||||
|
char *out,
|
||||||
|
size_t out_size)
|
||||||
|
{
|
||||||
|
const char *value = find_json_key(object_start, key);
|
||||||
|
if (!value || value >= object_end || out_size == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
while (value < object_end && (*value == ' ' || *value == '\t' || *value == '\n' || *value == '\r')) {
|
||||||
|
++value;
|
||||||
|
}
|
||||||
|
if (value >= object_end || *value != '"') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
++value;
|
||||||
|
size_t index = 0;
|
||||||
|
while (value < object_end && *value && *value != '"' && index + 1 < out_size) {
|
||||||
|
out[index++] = *value++;
|
||||||
|
}
|
||||||
|
out[index] = '\0';
|
||||||
|
return index > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void appendf(char *out, size_t size, size_t *offset, const char *format, ...)
|
||||||
|
{
|
||||||
|
if (*offset >= size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
const int written = vsnprintf(out + *offset, size - *offset, format, args);
|
||||||
|
va_end(args);
|
||||||
|
if (written > 0) {
|
||||||
|
*offset += (size_t)written;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void append(char *out, size_t size, size_t *offset, const char *text)
|
static void append(char *out, size_t size, size_t *offset, const char *text)
|
||||||
{
|
{
|
||||||
if (*offset >= size) {
|
if (*offset >= size) {
|
||||||
@@ -102,6 +173,594 @@ static void append(char *out, size_t size, size_t *offset, const char *text)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int index;
|
||||||
|
int line;
|
||||||
|
char type[32];
|
||||||
|
EmcPose end;
|
||||||
|
EmcPose start;
|
||||||
|
double feed_rate;
|
||||||
|
int plane;
|
||||||
|
double center_first;
|
||||||
|
double center_second;
|
||||||
|
int rotation;
|
||||||
|
double axis_end_point;
|
||||||
|
int zero_length;
|
||||||
|
int accepted;
|
||||||
|
int emitted;
|
||||||
|
} LctpMotionEvent;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
double cycle_time;
|
||||||
|
double max_velocity;
|
||||||
|
double max_acceleration;
|
||||||
|
double max_jerk;
|
||||||
|
double rapid_scale;
|
||||||
|
double feed_scale;
|
||||||
|
double tolerance;
|
||||||
|
int queue_size;
|
||||||
|
int max_cycles;
|
||||||
|
int sample_stride;
|
||||||
|
} LctpTimingOptions;
|
||||||
|
|
||||||
|
static LctpTimingOptions read_timing_options(const char *json)
|
||||||
|
{
|
||||||
|
LctpTimingOptions options;
|
||||||
|
options.cycle_time = read_json_number_in_object(json, json + strlen(json), "cycleTime", 0.001);
|
||||||
|
options.max_velocity = read_json_number_in_object(json, json + strlen(json), "maxVelocity", 35.0);
|
||||||
|
options.max_acceleration = read_json_number_in_object(json, json + strlen(json), "maxAcceleration", 500.0);
|
||||||
|
options.max_jerk = read_json_number_in_object(json, json + strlen(json), "maxJerk", 1000.0);
|
||||||
|
options.rapid_scale = read_json_number_in_object(json, json + strlen(json), "rapidScale", 1.0);
|
||||||
|
options.feed_scale = read_json_number_in_object(json, json + strlen(json), "feedScale", 1.0);
|
||||||
|
options.tolerance = read_json_number_in_object(json, json + strlen(json), "tolerance", 0.0);
|
||||||
|
options.queue_size = (int)read_json_number_in_object(json, json + strlen(json), "queueSize", TP_DEFAULT_QUEUE_SIZE);
|
||||||
|
options.max_cycles = (int)read_json_number_in_object(json, json + strlen(json), "maxCycles", 1000000);
|
||||||
|
options.sample_stride = (int)read_json_number_in_object(json, json + strlen(json), "sampleStride", 10.0);
|
||||||
|
|
||||||
|
if (!isfinite(options.cycle_time) || options.cycle_time <= 0.0) options.cycle_time = 0.001;
|
||||||
|
if (!isfinite(options.max_velocity) || options.max_velocity <= 0.0) options.max_velocity = 35.0;
|
||||||
|
if (!isfinite(options.max_acceleration) || options.max_acceleration <= 0.0) options.max_acceleration = 500.0;
|
||||||
|
if (!isfinite(options.max_jerk) || options.max_jerk <= 0.0) options.max_jerk = 1000.0;
|
||||||
|
if (!isfinite(options.rapid_scale) || options.rapid_scale <= 0.0) options.rapid_scale = 1.0;
|
||||||
|
if (!isfinite(options.feed_scale) || options.feed_scale <= 0.0) options.feed_scale = 1.0;
|
||||||
|
if (!isfinite(options.tolerance) || options.tolerance < 0.0) options.tolerance = 0.0;
|
||||||
|
if (options.queue_size <= 0) options.queue_size = TP_DEFAULT_QUEUE_SIZE;
|
||||||
|
if (options.max_cycles <= 0) options.max_cycles = 1000000;
|
||||||
|
if (options.sample_stride <= 0) options.sample_stride = 10;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_motion_events(const char *json, LctpMotionEvent *events, int max_events)
|
||||||
|
{
|
||||||
|
const char *motion = find_json_key(json, "motion");
|
||||||
|
if (!motion) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const char *cursor = strchr(motion, '[');
|
||||||
|
if (!cursor) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
EmcPose previous;
|
||||||
|
memset(&previous, 0, sizeof(previous));
|
||||||
|
while (count < max_events) {
|
||||||
|
const char *object_start = strchr(cursor, '{');
|
||||||
|
if (!object_start) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const char *object_end = strchr(object_start, '}');
|
||||||
|
if (!object_end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LctpMotionEvent *event = &events[count];
|
||||||
|
memset(event, 0, sizeof(*event));
|
||||||
|
event->index = count;
|
||||||
|
event->line = (int)read_json_number_in_object(object_start, object_end, "line", -1.0);
|
||||||
|
event->feed_rate = read_json_number_in_object(object_start, object_end, "feedRate", 0.0);
|
||||||
|
event->plane = (int)read_json_number_in_object(object_start, object_end, "plane", 170.0);
|
||||||
|
event->center_first = read_json_number_in_object(object_start, object_end, "centerFirst", 0.0);
|
||||||
|
event->center_second = read_json_number_in_object(object_start, object_end, "centerSecond", 0.0);
|
||||||
|
event->rotation = (int)read_json_number_in_object(object_start, object_end, "rotation", 0.0);
|
||||||
|
event->axis_end_point = read_json_number_in_object(object_start, object_end, "axisEndPoint", 0.0);
|
||||||
|
event->start = previous;
|
||||||
|
read_json_string_in_object(object_start, object_end, "type", event->type, sizeof(event->type));
|
||||||
|
event->end.tran.x = read_json_number_in_object(object_start, object_end, "x", 0.0);
|
||||||
|
event->end.tran.y = read_json_number_in_object(object_start, object_end, "y", 0.0);
|
||||||
|
event->end.tran.z = read_json_number_in_object(object_start, object_end, "z", 0.0);
|
||||||
|
event->end.a = read_json_number_in_object(object_start, object_end, "a", 0.0);
|
||||||
|
event->end.b = read_json_number_in_object(object_start, object_end, "b", 0.0);
|
||||||
|
event->end.c = read_json_number_in_object(object_start, object_end, "c", 0.0);
|
||||||
|
event->end.u = read_json_number_in_object(object_start, object_end, "u", 0.0);
|
||||||
|
event->end.v = read_json_number_in_object(object_start, object_end, "v", 0.0);
|
||||||
|
event->end.w = read_json_number_in_object(object_start, object_end, "w", 0.0);
|
||||||
|
event->zero_length =
|
||||||
|
fabs(event->end.tran.x - event->start.tran.x) < TP_POS_EPSILON &&
|
||||||
|
fabs(event->end.tran.y - event->start.tran.y) < TP_POS_EPSILON &&
|
||||||
|
fabs(event->end.tran.z - event->start.tran.z) < TP_POS_EPSILON &&
|
||||||
|
fabs(event->end.a - event->start.a) < TP_POS_EPSILON &&
|
||||||
|
fabs(event->end.b - event->start.b) < TP_POS_EPSILON &&
|
||||||
|
fabs(event->end.c - event->start.c) < TP_POS_EPSILON &&
|
||||||
|
fabs(event->end.u - event->start.u) < TP_POS_EPSILON &&
|
||||||
|
fabs(event->end.v - event->start.v) < TP_POS_EPSILON &&
|
||||||
|
fabs(event->end.w - event->start.w) < TP_POS_EPSILON;
|
||||||
|
previous = event->end;
|
||||||
|
count += 1;
|
||||||
|
cursor = object_end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct state_tag_t tag_for_event(const LctpMotionEvent *event)
|
||||||
|
{
|
||||||
|
struct state_tag_t tag;
|
||||||
|
memset(&tag, 0, sizeof(tag));
|
||||||
|
tag.fields[GM_FIELD_LINE_NUMBER] = event->index + 1;
|
||||||
|
tag.fields_float[GM_FIELD_FLOAT_LINE_NUMBER] = (float)event->line;
|
||||||
|
tag.fields_float[GM_FIELD_FLOAT_FEED] = (float)event->feed_rate;
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double event_velocity(const LctpMotionEvent *event, const LctpTimingOptions *options)
|
||||||
|
{
|
||||||
|
if (strcmp(event->type, "STRAIGHT_TRAVERSE") == 0) {
|
||||||
|
return options->max_velocity * options->rapid_scale;
|
||||||
|
}
|
||||||
|
const double feed_units_per_sec = event->feed_rate > 0.0 ? event->feed_rate / 60.0 : options->max_velocity;
|
||||||
|
const double scaled = feed_units_per_sec * options->feed_scale;
|
||||||
|
return fmin(fmax(scaled, 0.000001), options->max_velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PmCartesian arc_center_for_event(const LctpMotionEvent *event)
|
||||||
|
{
|
||||||
|
PmCartesian center;
|
||||||
|
memset(¢er, 0, sizeof(center));
|
||||||
|
if (event->plane == 180) {
|
||||||
|
center.x = event->center_first;
|
||||||
|
center.z = event->center_second;
|
||||||
|
center.y = event->start.tran.y;
|
||||||
|
} else if (event->plane == 190) {
|
||||||
|
center.y = event->center_first;
|
||||||
|
center.z = event->center_second;
|
||||||
|
center.x = event->start.tran.x;
|
||||||
|
} else {
|
||||||
|
center.x = event->center_first;
|
||||||
|
center.y = event->center_second;
|
||||||
|
center.z = event->start.tran.z;
|
||||||
|
}
|
||||||
|
return center;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PmCartesian arc_normal_for_event(const LctpMotionEvent *event)
|
||||||
|
{
|
||||||
|
PmCartesian normal;
|
||||||
|
memset(&normal, 0, sizeof(normal));
|
||||||
|
if (event->plane == 180) {
|
||||||
|
normal.y = 1.0;
|
||||||
|
} else if (event->plane == 190) {
|
||||||
|
normal.x = 1.0;
|
||||||
|
} else {
|
||||||
|
normal.z = 1.0;
|
||||||
|
}
|
||||||
|
return normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_motion_event_to_tp(
|
||||||
|
TP_STRUCT *tp,
|
||||||
|
LctpMotionEvent *event,
|
||||||
|
const LctpTimingOptions *options)
|
||||||
|
{
|
||||||
|
const double vel = event_velocity(event, options);
|
||||||
|
struct state_tag_t tag = tag_for_event(event);
|
||||||
|
if (strcmp(event->type, "ARC_FEED") == 0) {
|
||||||
|
const PmCartesian center = arc_center_for_event(event);
|
||||||
|
const PmCartesian normal = arc_normal_for_event(event);
|
||||||
|
return tpAddCircle(
|
||||||
|
tp,
|
||||||
|
event->end,
|
||||||
|
center,
|
||||||
|
normal,
|
||||||
|
event->rotation,
|
||||||
|
EMC_MOTION_TYPE_ARC,
|
||||||
|
vel,
|
||||||
|
options->max_velocity,
|
||||||
|
options->max_acceleration,
|
||||||
|
options->max_jerk,
|
||||||
|
status.enables_new,
|
||||||
|
0,
|
||||||
|
tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int motion_type = strcmp(event->type, "STRAIGHT_TRAVERSE") == 0
|
||||||
|
? EMC_MOTION_TYPE_TRAVERSE
|
||||||
|
: EMC_MOTION_TYPE_FEED;
|
||||||
|
return tpAddLine(
|
||||||
|
tp,
|
||||||
|
event->end,
|
||||||
|
motion_type,
|
||||||
|
vel,
|
||||||
|
options->max_velocity,
|
||||||
|
options->max_acceleration,
|
||||||
|
options->max_jerk,
|
||||||
|
status.enables_new,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void append_zero_length_segment(
|
||||||
|
char *out,
|
||||||
|
size_t out_size,
|
||||||
|
size_t *offset,
|
||||||
|
LctpMotionEvent *event,
|
||||||
|
int *emitted_segments,
|
||||||
|
int cycles,
|
||||||
|
const LctpTimingOptions *options)
|
||||||
|
{
|
||||||
|
appendf(out, out_size, offset,
|
||||||
|
"%s{\"index\":%d,\"line\":%d,\"type\":\"%s\","
|
||||||
|
"\"zeroLength\":true,\"tpCycleRc\":0,\"tpGetPos\":0,\"cycles\":0,"
|
||||||
|
"\"durationSeconds\":0,\"elapsedSeconds\":%.12g,"
|
||||||
|
"\"queueDepth\":0,\"activeDepth\":0,\"velocity\":0,"
|
||||||
|
"\"x\":%.12g,\"y\":%.12g,\"z\":%.12g,\"a\":%.12g,\"b\":%.12g,\"c\":%.12g}",
|
||||||
|
*emitted_segments == 0 ? "" : ",",
|
||||||
|
event->index,
|
||||||
|
event->line,
|
||||||
|
event->type,
|
||||||
|
cycles * options->cycle_time,
|
||||||
|
event->end.tran.x,
|
||||||
|
event->end.tran.y,
|
||||||
|
event->end.tran.z,
|
||||||
|
event->end.a,
|
||||||
|
event->end.b,
|
||||||
|
event->end.c);
|
||||||
|
event->emitted = 1;
|
||||||
|
*emitted_segments += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int enqueue_next_nonzero_event(
|
||||||
|
TP_STRUCT *tp,
|
||||||
|
LctpMotionEvent *events,
|
||||||
|
int event_count,
|
||||||
|
int *next_event,
|
||||||
|
const LctpTimingOptions *options,
|
||||||
|
int *accepted_events,
|
||||||
|
int *add_failures,
|
||||||
|
char *out,
|
||||||
|
size_t out_size,
|
||||||
|
size_t *offset,
|
||||||
|
int *emitted_segments,
|
||||||
|
int cycles)
|
||||||
|
{
|
||||||
|
while (*next_event < event_count) {
|
||||||
|
LctpMotionEvent *event = &events[*next_event];
|
||||||
|
*next_event += 1;
|
||||||
|
if (event->zero_length) {
|
||||||
|
append_zero_length_segment(out, out_size, offset, event, emitted_segments, cycles, options);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int add_rc = add_motion_event_to_tp(tp, event, options);
|
||||||
|
if (add_rc == TP_ERR_OK) {
|
||||||
|
event->accepted = 1;
|
||||||
|
*accepted_events += 1;
|
||||||
|
} else if (add_rc == TP_ERR_ZERO_LENGTH) {
|
||||||
|
event->zero_length = 1;
|
||||||
|
append_zero_length_segment(out, out_size, offset, event, emitted_segments, cycles, options);
|
||||||
|
} else {
|
||||||
|
*add_failures += 1;
|
||||||
|
}
|
||||||
|
return add_rc;
|
||||||
|
}
|
||||||
|
return TP_ERR_NO_ACTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void append_runtime_sample(
|
||||||
|
char *samples,
|
||||||
|
size_t samples_size,
|
||||||
|
size_t *samples_offset,
|
||||||
|
int *sample_count,
|
||||||
|
int cycles,
|
||||||
|
int motion_index,
|
||||||
|
const LctpMotionEvent *event,
|
||||||
|
const LctpTimingOptions *options,
|
||||||
|
const TP_STRUCT *tp)
|
||||||
|
{
|
||||||
|
EmcPose pos;
|
||||||
|
memset(&pos, 0, sizeof(pos));
|
||||||
|
tpGetPos(tp, &pos);
|
||||||
|
appendf(samples, samples_size, samples_offset,
|
||||||
|
"%s{\"sampleIndex\":%d,\"cycle\":%d,\"timeSeconds\":%.12g,"
|
||||||
|
"\"motionIndex\":%d,\"line\":%d,\"type\":\"%s\","
|
||||||
|
"\"x\":%.12g,\"y\":%.12g,\"z\":%.12g,\"a\":%.12g,\"b\":%.12g,\"c\":%.12g,"
|
||||||
|
"\"u\":%.12g,\"v\":%.12g,\"w\":%.12g,"
|
||||||
|
"\"currentVelocity\":%.12g,\"requestedVelocity\":%.12g,"
|
||||||
|
"\"distanceToGo\":%.12g,"
|
||||||
|
"\"dtgX\":%.12g,\"dtgY\":%.12g,\"dtgZ\":%.12g,"
|
||||||
|
"\"queueDepth\":%d,\"activeDepth\":%d}",
|
||||||
|
*sample_count == 0 ? "" : ",",
|
||||||
|
*sample_count,
|
||||||
|
cycles,
|
||||||
|
cycles * options->cycle_time,
|
||||||
|
motion_index,
|
||||||
|
event ? event->line : -1,
|
||||||
|
event ? event->type : "-",
|
||||||
|
pos.tran.x,
|
||||||
|
pos.tran.y,
|
||||||
|
pos.tran.z,
|
||||||
|
pos.a,
|
||||||
|
pos.b,
|
||||||
|
pos.c,
|
||||||
|
pos.u,
|
||||||
|
pos.v,
|
||||||
|
pos.w,
|
||||||
|
status.current_vel,
|
||||||
|
status.requested_vel,
|
||||||
|
status.distance_to_go,
|
||||||
|
status.dtg.tran.x,
|
||||||
|
status.dtg.tran.y,
|
||||||
|
status.dtg.tran.z,
|
||||||
|
tpQueueDepth((TP_STRUCT *)tp),
|
||||||
|
tpActiveDepth((TP_STRUCT *)tp));
|
||||||
|
*sample_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
char *lctp_run_canonical_motion_timing(const char *json)
|
||||||
|
{
|
||||||
|
const size_t out_size = 4 * 1024 * 1024;
|
||||||
|
char *out = (char *)malloc(out_size);
|
||||||
|
if (!out) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
out[0] = '\0';
|
||||||
|
size_t offset = 0;
|
||||||
|
char *samples = (char *)malloc(out_size);
|
||||||
|
if (!samples) {
|
||||||
|
free(out);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
samples[0] = '\0';
|
||||||
|
size_t samples_offset = 0;
|
||||||
|
int sample_count = 0;
|
||||||
|
|
||||||
|
if (!json) {
|
||||||
|
append(out, out_size, &offset, "{\"ok\":false,\"error\":\"missing canonical motion JSON\"}\n");
|
||||||
|
free(samples);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum { MAX_EVENTS = 4096 };
|
||||||
|
LctpMotionEvent *events = (LctpMotionEvent *)calloc(MAX_EVENTS, sizeof(LctpMotionEvent));
|
||||||
|
if (!events) {
|
||||||
|
append(out, out_size, &offset, "{\"ok\":false,\"error\":\"failed to allocate motion events\"}\n");
|
||||||
|
free(samples);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LctpTimingOptions options = read_timing_options(json);
|
||||||
|
const int event_count = parse_motion_events(json, events, MAX_EVENTS);
|
||||||
|
if (event_count <= 0) {
|
||||||
|
free(events);
|
||||||
|
append(out, out_size, &offset, "{\"ok\":false,\"error\":\"no canonical motion events\"}\n");
|
||||||
|
free(samples);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
init_motion_state();
|
||||||
|
status.net_feed_scale = options.feed_scale;
|
||||||
|
status.feed_scale = options.feed_scale;
|
||||||
|
status.rapid_scale = options.rapid_scale;
|
||||||
|
|
||||||
|
TP_STRUCT tp;
|
||||||
|
memset(&tp, 0, sizeof(tp));
|
||||||
|
|
||||||
|
EmcPose start;
|
||||||
|
memset(&start, 0, sizeof(start));
|
||||||
|
const int create_rc = tpCreate(&tp, options.queue_size, 100);
|
||||||
|
const int set_cycle_rc = tpSetCycleTime(&tp, options.cycle_time);
|
||||||
|
const int set_pos_rc = tpSetPos(&tp, &start);
|
||||||
|
const int set_vmax_rc = tpSetVmax(&tp, options.max_velocity, options.max_velocity);
|
||||||
|
const int set_vlimit_rc = tpSetVlimit(&tp, options.max_velocity);
|
||||||
|
const int set_amax_rc = tpSetAmax(&tp, options.max_acceleration);
|
||||||
|
const int set_term_rc = tpSetTermCond(
|
||||||
|
&tp,
|
||||||
|
options.tolerance > 0.0 ? TC_TERM_COND_PARABOLIC : TC_TERM_COND_STOP,
|
||||||
|
options.tolerance);
|
||||||
|
|
||||||
|
int add_failures = 0;
|
||||||
|
int next_event = 0;
|
||||||
|
int accepted_events = 0;
|
||||||
|
appendf(out, out_size, &offset,
|
||||||
|
"{\"apiName\":\"linuxcnc_tp_queue_timing\","
|
||||||
|
"\"semanticBoundary\":\"linuxcnc_tp_queue_runtime_timing_from_canonical_motion\","
|
||||||
|
"\"ok\":true,"
|
||||||
|
"\"tpCreate\":%d,"
|
||||||
|
"\"tpSetCycleTime\":%d,"
|
||||||
|
"\"tpSetPos\":%d,"
|
||||||
|
"\"tpSetVmax\":%d,"
|
||||||
|
"\"tpSetVlimit\":%d,"
|
||||||
|
"\"tpSetAmax\":%d,"
|
||||||
|
"\"tpSetTermCond\":%d,"
|
||||||
|
"\"cycleTime\":%.12g,"
|
||||||
|
"\"motionCount\":%d,"
|
||||||
|
"\"segments\":[",
|
||||||
|
create_rc,
|
||||||
|
set_cycle_rc,
|
||||||
|
set_pos_rc,
|
||||||
|
set_vmax_rc,
|
||||||
|
set_vlimit_rc,
|
||||||
|
set_amax_rc,
|
||||||
|
set_term_rc,
|
||||||
|
options.cycle_time,
|
||||||
|
event_count);
|
||||||
|
|
||||||
|
int cycles = 0;
|
||||||
|
int cycle_rc = 0;
|
||||||
|
int current_segment = -1;
|
||||||
|
int emitted_segments = 0;
|
||||||
|
int segment_start_cycle = 0;
|
||||||
|
int segment_last_cycle = 0;
|
||||||
|
while (next_event < event_count && tpQueueDepth(&tp) < options.queue_size) {
|
||||||
|
enqueue_next_nonzero_event(
|
||||||
|
&tp,
|
||||||
|
events,
|
||||||
|
event_count,
|
||||||
|
&next_event,
|
||||||
|
&options,
|
||||||
|
&accepted_events,
|
||||||
|
&add_failures,
|
||||||
|
out,
|
||||||
|
out_size,
|
||||||
|
&offset,
|
||||||
|
&emitted_segments,
|
||||||
|
cycles);
|
||||||
|
}
|
||||||
|
while (cycles < options.max_cycles && (!tpIsDone(&tp) || next_event < event_count)) {
|
||||||
|
while (next_event < event_count && tpQueueDepth(&tp) < options.queue_size) {
|
||||||
|
enqueue_next_nonzero_event(
|
||||||
|
&tp,
|
||||||
|
events,
|
||||||
|
event_count,
|
||||||
|
&next_event,
|
||||||
|
&options,
|
||||||
|
&accepted_events,
|
||||||
|
&add_failures,
|
||||||
|
out,
|
||||||
|
out_size,
|
||||||
|
&offset,
|
||||||
|
&emitted_segments,
|
||||||
|
cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
cycle_rc = tpRunCycle(&tp, (long)llround(options.cycle_time * 1000000000.0));
|
||||||
|
cycles += 1;
|
||||||
|
const struct state_tag_t exec_tag = tpGetExecTag(&tp);
|
||||||
|
const int exec_segment = exec_tag.fields[GM_FIELD_LINE_NUMBER] - 1;
|
||||||
|
if (exec_segment >= 0 && exec_segment < event_count) {
|
||||||
|
if (
|
||||||
|
sample_count == 0 ||
|
||||||
|
cycles % options.sample_stride == 0 ||
|
||||||
|
exec_segment != current_segment
|
||||||
|
) {
|
||||||
|
append_runtime_sample(
|
||||||
|
samples,
|
||||||
|
out_size,
|
||||||
|
&samples_offset,
|
||||||
|
&sample_count,
|
||||||
|
cycles,
|
||||||
|
exec_segment,
|
||||||
|
&events[exec_segment],
|
||||||
|
&options,
|
||||||
|
&tp);
|
||||||
|
}
|
||||||
|
if (current_segment >= 0 && exec_segment != current_segment) {
|
||||||
|
LctpMotionEvent *completed = &events[current_segment];
|
||||||
|
EmcPose pos;
|
||||||
|
memset(&pos, 0, sizeof(pos));
|
||||||
|
const int get_pos_rc = tpGetPos(&tp, &pos);
|
||||||
|
const double vel = event_velocity(completed, &options);
|
||||||
|
appendf(out, out_size, &offset,
|
||||||
|
"%s{\"index\":%d,\"line\":%d,\"type\":\"%s\","
|
||||||
|
"\"tpCycleRc\":%d,\"tpGetPos\":%d,\"cycles\":%d,"
|
||||||
|
"\"durationSeconds\":%.12g,\"elapsedSeconds\":%.12g,"
|
||||||
|
"\"queueDepth\":%d,\"activeDepth\":%d,\"velocity\":%.12g,"
|
||||||
|
"\"x\":%.12g,\"y\":%.12g,\"z\":%.12g,\"a\":%.12g,\"b\":%.12g,\"c\":%.12g}",
|
||||||
|
emitted_segments == 0 ? "" : ",",
|
||||||
|
current_segment,
|
||||||
|
completed->line,
|
||||||
|
completed->type,
|
||||||
|
cycle_rc,
|
||||||
|
get_pos_rc,
|
||||||
|
segment_last_cycle - segment_start_cycle + 1,
|
||||||
|
(segment_last_cycle - segment_start_cycle + 1) * options.cycle_time,
|
||||||
|
segment_last_cycle * options.cycle_time,
|
||||||
|
tpQueueDepth(&tp),
|
||||||
|
tpActiveDepth(&tp),
|
||||||
|
vel,
|
||||||
|
pos.tran.x,
|
||||||
|
pos.tran.y,
|
||||||
|
pos.tran.z,
|
||||||
|
pos.a,
|
||||||
|
pos.b,
|
||||||
|
pos.c);
|
||||||
|
emitted_segments += 1;
|
||||||
|
segment_start_cycle = cycles;
|
||||||
|
}
|
||||||
|
if (exec_segment != current_segment) {
|
||||||
|
current_segment = exec_segment;
|
||||||
|
segment_start_cycle = cycles;
|
||||||
|
}
|
||||||
|
segment_last_cycle = cycles;
|
||||||
|
}
|
||||||
|
if (cycle_rc < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current_segment >= 0) {
|
||||||
|
append_runtime_sample(
|
||||||
|
samples,
|
||||||
|
out_size,
|
||||||
|
&samples_offset,
|
||||||
|
&sample_count,
|
||||||
|
cycles,
|
||||||
|
current_segment,
|
||||||
|
&events[current_segment],
|
||||||
|
&options,
|
||||||
|
&tp);
|
||||||
|
LctpMotionEvent *completed = &events[current_segment];
|
||||||
|
EmcPose pos;
|
||||||
|
memset(&pos, 0, sizeof(pos));
|
||||||
|
const int get_pos_rc = tpGetPos(&tp, &pos);
|
||||||
|
const double vel = event_velocity(completed, &options);
|
||||||
|
appendf(out, out_size, &offset,
|
||||||
|
"%s{\"index\":%d,\"line\":%d,\"type\":\"%s\","
|
||||||
|
"\"tpCycleRc\":%d,\"tpGetPos\":%d,\"cycles\":%d,"
|
||||||
|
"\"durationSeconds\":%.12g,\"elapsedSeconds\":%.12g,"
|
||||||
|
"\"queueDepth\":%d,\"activeDepth\":%d,\"velocity\":%.12g,"
|
||||||
|
"\"x\":%.12g,\"y\":%.12g,\"z\":%.12g,\"a\":%.12g,\"b\":%.12g,\"c\":%.12g}",
|
||||||
|
emitted_segments == 0 ? "" : ",",
|
||||||
|
current_segment,
|
||||||
|
completed->line,
|
||||||
|
completed->type,
|
||||||
|
cycle_rc,
|
||||||
|
get_pos_rc,
|
||||||
|
segment_last_cycle - segment_start_cycle + 1,
|
||||||
|
(segment_last_cycle - segment_start_cycle + 1) * options.cycle_time,
|
||||||
|
segment_last_cycle * options.cycle_time,
|
||||||
|
tpQueueDepth(&tp),
|
||||||
|
tpActiveDepth(&tp),
|
||||||
|
vel,
|
||||||
|
pos.tran.x,
|
||||||
|
pos.tran.y,
|
||||||
|
pos.tran.z,
|
||||||
|
pos.a,
|
||||||
|
pos.b,
|
||||||
|
pos.c);
|
||||||
|
emitted_segments += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendf(out, out_size, &offset,
|
||||||
|
"],\"samples\":[%s],\"sampleCount\":%d,"
|
||||||
|
"\"acceptedMotionCount\":%d,\"emittedMotionCount\":%d,\"totalCycles\":%d,"
|
||||||
|
"\"totalSeconds\":%.12g,\"addFailures\":%d,\"tpDone\":%d,\"finalQueueDepth\":%d}\n",
|
||||||
|
samples,
|
||||||
|
sample_count,
|
||||||
|
accepted_events,
|
||||||
|
emitted_segments,
|
||||||
|
cycles,
|
||||||
|
cycles * options.cycle_time,
|
||||||
|
add_failures,
|
||||||
|
tpIsDone(&tp),
|
||||||
|
tpQueueDepth(&tp));
|
||||||
|
|
||||||
|
free(events);
|
||||||
|
free(samples);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
static void append_linear_probe(char *out, size_t size, size_t *offset)
|
static void append_linear_probe(char *out, size_t size, size_t *offset)
|
||||||
{
|
{
|
||||||
init_motion_state();
|
init_motion_state();
|
||||||
|
|||||||
228
wasm-port/runtime/sdk/src/linuxcnc-tp.js
Normal file
228
wasm-port/runtime/sdk/src/linuxcnc-tp.js
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import createLinuxCncTpModule from "../../../build/wasm/tp/linuxcnc_tp.js";
|
||||||
|
|
||||||
|
const SEMANTIC_BOUNDARY = "linuxcnc_tp_queue_runtime_timing_from_canonical_motion";
|
||||||
|
|
||||||
|
function allocCString(mod, value) {
|
||||||
|
const bytes = mod.lengthBytesUTF8(value) + 1;
|
||||||
|
const ptr = mod._malloc(bytes);
|
||||||
|
mod.stringToUTF8(value, ptr, bytes);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function callTiming(mod, payload) {
|
||||||
|
const inputPtr = allocCString(mod, JSON.stringify(payload));
|
||||||
|
let resultPtr = 0;
|
||||||
|
try {
|
||||||
|
resultPtr = mod._lctp_run_canonical_motion_timing(inputPtr);
|
||||||
|
if (!resultPtr) {
|
||||||
|
throw new Error("lctp_run_canonical_motion_timing returned null");
|
||||||
|
}
|
||||||
|
const text = mod.UTF8ToString(resultPtr);
|
||||||
|
const result = JSON.parse(text);
|
||||||
|
if (result.ok !== true) {
|
||||||
|
throw new Error(result.error || "LinuxCNC TP queue timing failed");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
if (resultPtr) mod._lctp_free_string(resultPtr);
|
||||||
|
mod._free(inputPtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLinuxCncTpSdk(moduleOptions = {}) {
|
||||||
|
const mod = await createLinuxCncTpModule(moduleOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiName: "linuxcnc-tp-wasm-sdk",
|
||||||
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
|
module: mod,
|
||||||
|
|
||||||
|
readiness() {
|
||||||
|
return {
|
||||||
|
apiName: "linuxcnc-tp-wasm-sdk-readiness",
|
||||||
|
loaded: true,
|
||||||
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
|
runCanonicalMotionTimingReady: typeof mod._lctp_run_canonical_motion_timing === "function",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
runCanonicalMotionTiming({ motion = [], options = {} } = {}) {
|
||||||
|
const result = callTiming(mod, {
|
||||||
|
...options,
|
||||||
|
motion: motion.map((event, index) => ({
|
||||||
|
index,
|
||||||
|
line: Number.isFinite(Number(event.line)) ? Number(event.line) : -1,
|
||||||
|
type: event.type,
|
||||||
|
feedRate: Number.isFinite(Number(event.feedRate)) ? Number(event.feedRate) : 0,
|
||||||
|
x: numberOrZero(event.axes?.x),
|
||||||
|
y: numberOrZero(event.axes?.y),
|
||||||
|
z: numberOrZero(event.axes?.z),
|
||||||
|
a: numberOrZero(event.axes?.a),
|
||||||
|
b: numberOrZero(event.axes?.b),
|
||||||
|
c: numberOrZero(event.axes?.c),
|
||||||
|
u: numberOrZero(event.axes?.u),
|
||||||
|
v: numberOrZero(event.axes?.v),
|
||||||
|
w: numberOrZero(event.axes?.w),
|
||||||
|
plane: numberOrZero(event.axes?.arc?.plane),
|
||||||
|
centerFirst: numberOrZero(event.axes?.arc?.centerFirst),
|
||||||
|
centerSecond: numberOrZero(event.axes?.arc?.centerSecond),
|
||||||
|
rotation: numberOrZero(event.axes?.arc?.rotation),
|
||||||
|
axisEndPoint: numberOrZero(event.axes?.arc?.axisEndPoint),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
return normalizeTimingResult(result, motion);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTimingResult(result, motion) {
|
||||||
|
const rawSegmentsByIndex = new Map((result.segments || []).map((segment) => [segment.index, segment]));
|
||||||
|
let elapsedSeconds = 0;
|
||||||
|
const segments = motion.map((event, index) => {
|
||||||
|
const raw = rawSegmentsByIndex.get(index);
|
||||||
|
const durationSeconds = Math.max(Number(raw?.durationSeconds) || 0, 0);
|
||||||
|
elapsedSeconds += durationSeconds;
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
line: event.line ?? null,
|
||||||
|
type: event.type,
|
||||||
|
motionClass: event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed",
|
||||||
|
durationSeconds,
|
||||||
|
elapsedSeconds,
|
||||||
|
currentVelocity: Number(raw?.velocity) || 0,
|
||||||
|
velocityMmPerMin: (Number(raw?.velocity) || 0) * 60,
|
||||||
|
queueDepth: Number(raw?.queueDepth) || 0,
|
||||||
|
activeDepth: Number(raw?.activeDepth) || 0,
|
||||||
|
cycles: Number(raw?.cycles) || 0,
|
||||||
|
axes: event.axes || {},
|
||||||
|
runtimeAxes: axesFromRuntimeRecord(raw, event.axes || {}),
|
||||||
|
tp: raw || null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const rawSamples = Array.isArray(result.samples) && result.samples.length > 0
|
||||||
|
? result.samples
|
||||||
|
: synthesizeSamplesFromSegments(result.segments || []);
|
||||||
|
const samples = normalizeSamples(rawSamples);
|
||||||
|
|
||||||
|
const feedSeconds = segments
|
||||||
|
.filter((segment) => segment.motionClass === "feed")
|
||||||
|
.reduce((total, segment) => total + segment.durationSeconds, 0);
|
||||||
|
const rapidSeconds = segments
|
||||||
|
.filter((segment) => segment.motionClass === "rapid")
|
||||||
|
.reduce((total, segment) => total + segment.durationSeconds, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-tp-program-timing",
|
||||||
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
|
sourceBasis: "LinuxCNC interpreter canonical motion events queued through LinuxCNC src/emc/tp WASM",
|
||||||
|
plannerRuntimeReady: result.ok === true
|
||||||
|
&& result.addFailures === 0
|
||||||
|
&& result.tpDone === 1
|
||||||
|
&& segments.length === motion.length,
|
||||||
|
sampleCount: Number(result.sampleCount) || samples.length,
|
||||||
|
samples,
|
||||||
|
totalSeconds: elapsedSeconds,
|
||||||
|
totalMinutes: elapsedSeconds / 60,
|
||||||
|
feedSeconds,
|
||||||
|
rapidSeconds,
|
||||||
|
motionCount: motion.length,
|
||||||
|
emittedMotionCount: result.emittedMotionCount || 0,
|
||||||
|
acceptedMotionCount: result.acceptedMotionCount || 0,
|
||||||
|
totalCycles: result.totalCycles || 0,
|
||||||
|
cycleTime: result.cycleTime || 0.001,
|
||||||
|
addFailures: result.addFailures || 0,
|
||||||
|
tpDone: result.tpDone === 1,
|
||||||
|
finalQueueDepth: result.finalQueueDepth || 0,
|
||||||
|
segments,
|
||||||
|
raw: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSamples(samples) {
|
||||||
|
return samples.map((sample, index) => ({
|
||||||
|
sampleIndex: Number(sample.sampleIndex ?? index),
|
||||||
|
cycle: Number(sample.cycle) || 0,
|
||||||
|
timeSeconds: Number(sample.timeSeconds) || 0,
|
||||||
|
motionIndex: Number(sample.motionIndex) || 0,
|
||||||
|
line: Number.isFinite(Number(sample.line)) ? Number(sample.line) : null,
|
||||||
|
type: sample.type || "-",
|
||||||
|
axes: {
|
||||||
|
x: numberOrZero(sample.x),
|
||||||
|
y: numberOrZero(sample.y),
|
||||||
|
z: numberOrZero(sample.z),
|
||||||
|
a: numberOrZero(sample.a),
|
||||||
|
b: numberOrZero(sample.b),
|
||||||
|
c: numberOrZero(sample.c),
|
||||||
|
u: numberOrZero(sample.u),
|
||||||
|
v: numberOrZero(sample.v),
|
||||||
|
w: numberOrZero(sample.w),
|
||||||
|
},
|
||||||
|
currentVelocity: numberOrZero(sample.currentVelocity),
|
||||||
|
currentVelocityMmPerMin: numberOrZero(sample.currentVelocity) * 60,
|
||||||
|
requestedVelocity: numberOrZero(sample.requestedVelocity),
|
||||||
|
requestedVelocityMmPerMin: numberOrZero(sample.requestedVelocity) * 60,
|
||||||
|
distanceToGo: numberOrZero(sample.distanceToGo),
|
||||||
|
dtg: {
|
||||||
|
x: numberOrZero(sample.dtgX),
|
||||||
|
y: numberOrZero(sample.dtgY),
|
||||||
|
z: numberOrZero(sample.dtgZ),
|
||||||
|
},
|
||||||
|
queueDepth: Number(sample.queueDepth) || 0,
|
||||||
|
activeDepth: Number(sample.activeDepth) || 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function synthesizeSamplesFromSegments(segments) {
|
||||||
|
return segments.map((segment, index) => ({
|
||||||
|
sampleIndex: index,
|
||||||
|
cycle: Number(segment.cycles) || 0,
|
||||||
|
timeSeconds: Number(segment.elapsedSeconds) || 0,
|
||||||
|
motionIndex: Number(segment.index) || index,
|
||||||
|
line: segment.line,
|
||||||
|
type: segment.type,
|
||||||
|
x: segment.x,
|
||||||
|
y: segment.y,
|
||||||
|
z: segment.z,
|
||||||
|
a: segment.a,
|
||||||
|
b: segment.b,
|
||||||
|
c: segment.c,
|
||||||
|
u: segment.u,
|
||||||
|
v: segment.v,
|
||||||
|
w: segment.w,
|
||||||
|
currentVelocity: segment.velocity,
|
||||||
|
requestedVelocity: segment.velocity,
|
||||||
|
distanceToGo: segment.distanceToGo ?? 0,
|
||||||
|
dtgX: segment.dtgX ?? 0,
|
||||||
|
dtgY: segment.dtgY ?? 0,
|
||||||
|
dtgZ: segment.dtgZ ?? 0,
|
||||||
|
queueDepth: segment.queueDepth,
|
||||||
|
activeDepth: segment.activeDepth,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function axesFromRuntimeRecord(record, fallback = {}) {
|
||||||
|
return {
|
||||||
|
x: numberOrFallback(record?.x, fallback.x, 0),
|
||||||
|
y: numberOrFallback(record?.y, fallback.y, 0),
|
||||||
|
z: numberOrFallback(record?.z, fallback.z, 0),
|
||||||
|
a: numberOrFallback(record?.a, fallback.a, 0),
|
||||||
|
b: numberOrFallback(record?.b, fallback.b, 0),
|
||||||
|
c: numberOrFallback(record?.c, fallback.c, 0),
|
||||||
|
u: numberOrFallback(record?.u, fallback.u, 0),
|
||||||
|
v: numberOrFallback(record?.v, fallback.v, 0),
|
||||||
|
w: numberOrFallback(record?.w, fallback.w, 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberOrFallback(...values) {
|
||||||
|
for (const value of values) {
|
||||||
|
const number = Number(value);
|
||||||
|
if (Number.isFinite(number)) return number;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberOrZero(value) {
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? number : 0;
|
||||||
|
}
|
||||||
@@ -80,4 +80,84 @@ try {
|
|||||||
tp._lctp_free_string(resultPtr);
|
tp._lctp_free_string(resultPtr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function allocCString(mod, value) {
|
||||||
|
const bytes = mod.lengthBytesUTF8(value) + 1;
|
||||||
|
const ptr = mod._malloc(bytes);
|
||||||
|
mod.stringToUTF8(value, ptr, bytes);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timingPayload = JSON.stringify({
|
||||||
|
cycleTime: 0.001,
|
||||||
|
maxVelocity: 10,
|
||||||
|
maxAcceleration: 50,
|
||||||
|
maxJerk: 100,
|
||||||
|
queueSize: 8,
|
||||||
|
motion: [
|
||||||
|
{ line: 2, type: "STRAIGHT_TRAVERSE", x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, feedRate: 0 },
|
||||||
|
{ line: 3, type: "STRAIGHT_FEED", x: 1, y: 0, z: 0, a: 0, b: 0, c: 0, feedRate: 60 },
|
||||||
|
{ line: 4, type: "STRAIGHT_FEED", x: 1, y: 1, z: 0, a: 0, b: 0, c: 0, feedRate: 60 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const timingInputPtr = allocCString(tp, timingPayload);
|
||||||
|
const timingResultPtr = tp._lctp_run_canonical_motion_timing(timingInputPtr);
|
||||||
|
assert.notEqual(timingResultPtr, 0);
|
||||||
|
try {
|
||||||
|
const timing = JSON.parse(tp.UTF8ToString(timingResultPtr));
|
||||||
|
assert.equal(timing.ok, true);
|
||||||
|
assert.equal(timing.semanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
|
||||||
|
assert.equal(timing.motionCount, 3);
|
||||||
|
assert.equal(timing.addFailures, 0);
|
||||||
|
assert.equal(timing.emittedMotionCount, 3);
|
||||||
|
assert.equal(timing.segments[0].zeroLength, true);
|
||||||
|
assert.equal(timing.segments[0].durationSeconds, 0);
|
||||||
|
assert.equal(timing.totalSeconds > 0, true);
|
||||||
|
assert.equal(timing.tpDone, 1);
|
||||||
|
} finally {
|
||||||
|
tp._lctp_free_string(timingResultPtr);
|
||||||
|
tp._free(timingInputPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcPayload = JSON.stringify({
|
||||||
|
cycleTime: 0.001,
|
||||||
|
maxVelocity: 10,
|
||||||
|
maxAcceleration: 50,
|
||||||
|
maxJerk: 100,
|
||||||
|
queueSize: 8,
|
||||||
|
motion: [
|
||||||
|
{ line: 2, type: "STRAIGHT_TRAVERSE", x: 1, y: 0, z: 0, a: 0, b: 0, c: 0, feedRate: 0 },
|
||||||
|
{
|
||||||
|
line: 3,
|
||||||
|
type: "ARC_FEED",
|
||||||
|
x: 0,
|
||||||
|
y: 1,
|
||||||
|
z: 0,
|
||||||
|
a: 0,
|
||||||
|
b: 0,
|
||||||
|
c: 0,
|
||||||
|
feedRate: 60,
|
||||||
|
plane: 170,
|
||||||
|
centerFirst: 0,
|
||||||
|
centerSecond: 0,
|
||||||
|
rotation: -1,
|
||||||
|
axisEndPoint: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const arcInputPtr = allocCString(tp, arcPayload);
|
||||||
|
const arcResultPtr = tp._lctp_run_canonical_motion_timing(arcInputPtr);
|
||||||
|
assert.notEqual(arcResultPtr, 0);
|
||||||
|
try {
|
||||||
|
const timing = JSON.parse(tp.UTF8ToString(arcResultPtr));
|
||||||
|
assert.equal(timing.ok, true);
|
||||||
|
assert.equal(timing.addFailures, 0);
|
||||||
|
assert.equal(timing.emittedMotionCount, 2);
|
||||||
|
assert.equal(timing.segments[1].type, "ARC_FEED");
|
||||||
|
assert.equal(timing.segments[1].durationSeconds > 1.4, true);
|
||||||
|
assert.equal(timing.tpDone, 1);
|
||||||
|
} finally {
|
||||||
|
tp._lctp_free_string(arcResultPtr);
|
||||||
|
tp._free(arcInputPtr);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("tp_wasm_node_smoke=ok");
|
console.log("tp_wasm_node_smoke=ok");
|
||||||
|
|||||||
@@ -104,5 +104,5 @@ link_wasm_module \
|
|||||||
-s ENVIRONMENT=web,node \
|
-s ENVIRONMENT=web,node \
|
||||||
-s ALLOW_MEMORY_GROWTH=1 \
|
-s ALLOW_MEMORY_GROWTH=1 \
|
||||||
-s NO_EXIT_RUNTIME=1 \
|
-s NO_EXIT_RUNTIME=1 \
|
||||||
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lctp_run_probe","_lctp_free_string"]' \
|
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lctp_run_probe","_lctp_run_canonical_motion_timing","_lctp_free_string"]' \
|
||||||
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString"]'
|
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build-static.mjs",
|
"build": "node scripts/build-static.mjs",
|
||||||
"dev": "python3 -m http.server 4173",
|
"dev": "python3 -m http.server 4173",
|
||||||
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh",
|
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
|
||||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import { cp, mkdir, rm, readFile } from "node:fs/promises";
|
import { cp, mkdir, readdir, rm, readFile } from "node:fs/promises";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const appRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
const appRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||||
|
const repoRoot = dirname(dirname(appRoot));
|
||||||
const distDir = join(appRoot, "dist");
|
const distDir = join(appRoot, "dist");
|
||||||
|
|
||||||
await rm(distDir, { recursive: true, force: true });
|
await rm(distDir, { recursive: true, force: true });
|
||||||
await mkdir(distDir, { recursive: true });
|
await mkdir(distDir, { recursive: true });
|
||||||
await cp(join(appRoot, "index.html"), join(distDir, "index.html"));
|
await cp(join(appRoot, "index.html"), join(distDir, "index.html"));
|
||||||
await cp(join(appRoot, "src"), join(distDir, "src"), { recursive: true });
|
await cp(join(appRoot, "src"), join(distDir, "src"), { recursive: true });
|
||||||
|
await copyLinuxCncManifest();
|
||||||
|
await copyLinuxCncConfigAssets();
|
||||||
|
await copyKinematicsRuntimeAssets();
|
||||||
|
await copyInterpreterRuntimeAssets();
|
||||||
|
await copyTpRuntimeAssets();
|
||||||
|
|
||||||
const packageJson = JSON.parse(await readFile(join(appRoot, "package.json"), "utf8"));
|
const packageJson = JSON.parse(await readFile(join(appRoot, "package.json"), "utf8"));
|
||||||
const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"];
|
const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"];
|
||||||
@@ -22,4 +28,59 @@ if (forbidden.length > 0) {
|
|||||||
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
|
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyInterpreterRuntimeAssets() {
|
||||||
|
const coreSrcDir = join(repoRoot, "wasm-port/build/wasm/core");
|
||||||
|
const coreDistDir = join(distDir, "wasm-port/build/wasm/core");
|
||||||
|
await mkdir(coreDistDir, { recursive: true });
|
||||||
|
for (const entry of ["linuxcnc_interp.js", "linuxcnc_interp.wasm"]) {
|
||||||
|
await cp(join(coreSrcDir, entry), join(coreDistDir, entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyTpRuntimeAssets() {
|
||||||
|
const tpSrcDir = join(repoRoot, "wasm-port/build/wasm/tp");
|
||||||
|
const tpDistDir = join(distDir, "wasm-port/build/wasm/tp");
|
||||||
|
await mkdir(tpDistDir, { recursive: true });
|
||||||
|
for (const entry of ["linuxcnc_tp.js", "linuxcnc_tp.wasm"]) {
|
||||||
|
await cp(join(tpSrcDir, entry), join(tpDistDir, entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("gmoccapy_static_build=ok");
|
console.log("gmoccapy_static_build=ok");
|
||||||
|
|
||||||
|
async function copyLinuxCncConfigAssets() {
|
||||||
|
const configSrcDir = join(repoRoot, "wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting");
|
||||||
|
const configDistDir = join(distDir, "configs/sim/axis/vismach/5axis/table-rotary-tilting");
|
||||||
|
await mkdir(configDistDir, { recursive: true });
|
||||||
|
await cp(configSrcDir, configDistDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLinuxCncManifest() {
|
||||||
|
const manifestDistDir = join(distDir, "wasm-port/tools");
|
||||||
|
await mkdir(manifestDistDir, { recursive: true });
|
||||||
|
await cp(
|
||||||
|
join(repoRoot, "wasm-port/tools/source-manifest.txt"),
|
||||||
|
join(manifestDistDir, "source-manifest.txt"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyKinematicsRuntimeAssets() {
|
||||||
|
const sdkSrcDir = join(repoRoot, "wasm-port/runtime/sdk/src");
|
||||||
|
const sdkDistDir = join(distDir, "wasm-port/runtime/sdk/src");
|
||||||
|
await mkdir(sdkDistDir, { recursive: true });
|
||||||
|
await cp(
|
||||||
|
join(sdkSrcDir, "linuxcnc-kinematics.js"),
|
||||||
|
join(sdkDistDir, "linuxcnc-kinematics.js"),
|
||||||
|
);
|
||||||
|
for (const entry of ["linuxcnc-interp.js", "linuxcnc-hal.js", "linuxcnc-tp.js"]) {
|
||||||
|
await cp(join(sdkSrcDir, entry), join(sdkDistDir, entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
const kinematicsSrcDir = join(repoRoot, "wasm-port/build/wasm/kinematics");
|
||||||
|
const kinematicsDistDir = join(distDir, "wasm-port/build/wasm/kinematics");
|
||||||
|
await mkdir(kinematicsDistDir, { recursive: true });
|
||||||
|
for (const entry of await readdir(kinematicsSrcDir)) {
|
||||||
|
if (!/^linuxcnc_.*_kinematics\.(js|wasm)$/.test(entry)) continue;
|
||||||
|
await cp(join(kinematicsSrcDir, entry), join(kinematicsDistDir, entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { createSimulationStore } from "./state/store.js";
|
import { createSimulationStore } from "./state/store.js";
|
||||||
import { mountGmoccapyShell } from "./ui/gmoccapy-shell.js";
|
import { mountGmoccapyShell } from "./ui/gmoccapy-shell.js";
|
||||||
|
import { createLinuxCncInterpreterRuntime } from "./runtime/linuxcnc-interpreter-runtime.js";
|
||||||
|
import { createLinuxCncInterpreterWorkerRuntime } from "./runtime/linuxcnc-interpreter-worker-client.js";
|
||||||
|
import { createLinuxCncKinematicsRuntime } from "./runtime/linuxcnc-kinematics-runtime.js";
|
||||||
|
import { createLinuxCncKinematicsWorkerRuntime } from "./runtime/linuxcnc-kinematics-worker-client.js";
|
||||||
|
import { loadLinuxCncIniConfig } from "./runtime/linuxcnc-ini-runtime.js";
|
||||||
|
|
||||||
const app = document.querySelector("#app");
|
const app = document.querySelector("#app");
|
||||||
|
|
||||||
@@ -9,11 +14,134 @@ if (!app) {
|
|||||||
|
|
||||||
const store = createSimulationStore();
|
const store = createSimulationStore();
|
||||||
const shell = mountGmoccapyShell(app, store);
|
const shell = mountGmoccapyShell(app, store);
|
||||||
|
const iniConfigReady = attachProfileIniConfig(store, store.getState().profile);
|
||||||
|
const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile);
|
||||||
|
const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store);
|
||||||
|
let attachedKinematicsProfile = store.getState().machineProfile;
|
||||||
|
let attachedIniProfile = store.getState().machineProfile;
|
||||||
|
store.subscribe((state) => {
|
||||||
|
if (state.machineProfile !== attachedIniProfile) {
|
||||||
|
attachedIniProfile = state.machineProfile;
|
||||||
|
attachProfileIniConfig(store, state.profile).catch(() => {});
|
||||||
|
}
|
||||||
|
if (state.machineProfile !== attachedKinematicsProfile) {
|
||||||
|
attachedKinematicsProfile = state.machineProfile;
|
||||||
|
attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile).catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.webRtcp5AxisSimulation = {
|
window.webRtcp5AxisSimulation = {
|
||||||
getState: store.getState,
|
getState: store.getState,
|
||||||
dispatch: store.dispatch,
|
dispatch: store.dispatch,
|
||||||
|
refreshKinematicsFrame: store.refreshKinematicsFrame,
|
||||||
|
saveSession: store.saveSession,
|
||||||
|
restoreSession: store.restoreSession,
|
||||||
|
stageMachineFiles: store.stageMachineFiles,
|
||||||
|
runFullBoundaryAudit: store.runFullBoundaryAudit,
|
||||||
getRegions: shell.getRegions,
|
getRegions: shell.getRegions,
|
||||||
|
iniConfigReady,
|
||||||
|
kinematicsRuntimeReady,
|
||||||
|
interpreterRuntimeReady,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch({ type: "BOOT_READY" });
|
store.dispatch({ type: "BOOT_READY" });
|
||||||
|
|
||||||
|
async function attachProfileIniConfig(store, profile) {
|
||||||
|
try {
|
||||||
|
const iniConfig = await loadLinuxCncIniConfig(profile);
|
||||||
|
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||||
|
return store.getState().iniConfigReadiness;
|
||||||
|
} catch (error) {
|
||||||
|
store.dispatch({ type: "INI_CONFIG_FAILED", path: profile.iniPath, error: error.message });
|
||||||
|
return store.getState().iniConfigReadiness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attachDefaultKinematicsRuntime(store, moduleId = "xyzac-trt") {
|
||||||
|
const sdkModuleUrls = [
|
||||||
|
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
||||||
|
new URL("../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
||||||
|
];
|
||||||
|
const errors = [];
|
||||||
|
for (const sdkModuleUrl of sdkModuleUrls) {
|
||||||
|
if (typeof Worker === "function") {
|
||||||
|
try {
|
||||||
|
const runtime = await createLinuxCncKinematicsWorkerRuntime({ moduleId, sdkModuleUrl });
|
||||||
|
store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime });
|
||||||
|
await store.refreshKinematicsFrame({ operatorMessage: `LinuxCNC kinematics ${runtime.moduleId} worker ready` });
|
||||||
|
return runtime.readiness();
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${sdkModuleUrl} worker: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const runtime = await createLinuxCncKinematicsRuntime({ moduleId, sdkModuleUrl });
|
||||||
|
store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime });
|
||||||
|
return runtime.readiness();
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${sdkModuleUrl}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const runtime = await createLinuxCncKinematicsRuntime({ moduleId });
|
||||||
|
store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime });
|
||||||
|
return runtime.readiness();
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`default runtime: ${error.message}`);
|
||||||
|
store.dispatch({
|
||||||
|
type: "SET_FRAME_SOURCE",
|
||||||
|
sourceMode: "fixture-ui-only",
|
||||||
|
operatorMessage: `LinuxCNC kinematics runtime unavailable: ${errors.join(" | ")}`,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-kinematics-runtime-readiness",
|
||||||
|
moduleId,
|
||||||
|
loaded: false,
|
||||||
|
sourceMode: "fixture-ui-only",
|
||||||
|
error: errors.join(" | "),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attachDefaultInterpreterRuntime(store) {
|
||||||
|
const sdkModuleUrls = [
|
||||||
|
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-interp.js", import.meta.url).href,
|
||||||
|
new URL("../wasm-port/runtime/sdk/src/linuxcnc-interp.js", import.meta.url).href,
|
||||||
|
];
|
||||||
|
const errors = [];
|
||||||
|
for (const sdkModuleUrl of sdkModuleUrls) {
|
||||||
|
if (typeof Worker === "function") {
|
||||||
|
try {
|
||||||
|
const runtime = await createLinuxCncInterpreterWorkerRuntime({ sdkModuleUrl });
|
||||||
|
store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime });
|
||||||
|
return runtime.readiness();
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${sdkModuleUrl} worker: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const runtime = await createLinuxCncInterpreterRuntime({ sdkModuleUrl });
|
||||||
|
store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime });
|
||||||
|
return runtime.readiness();
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${sdkModuleUrl}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const runtime = await createLinuxCncInterpreterRuntime();
|
||||||
|
store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime });
|
||||||
|
return runtime.readiness();
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`default runtime: ${error.message}`);
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-interpreter-runtime-readiness",
|
||||||
|
loaded: false,
|
||||||
|
sourceMode: "fixture-line-playback",
|
||||||
|
error: errors.join(" | "),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
12
web-rtcp-5axis-sim-plan/app/src/profiles/index.js
Normal file
12
web-rtcp-5axis-sim-plan/app/src/profiles/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { xyzacTrtProfile } from "./xyzac-trt.js";
|
||||||
|
import { xyzbcTrtProfile } from "./xyzbc-trt.js";
|
||||||
|
|
||||||
|
export const fiveAxisProfiles = [xyzacTrtProfile, xyzbcTrtProfile];
|
||||||
|
|
||||||
|
export function getFiveAxisProfile(profileId = "xyzac-trt") {
|
||||||
|
const profile = fiveAxisProfiles.find((entry) => entry.id === profileId);
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error(`unknown five-axis profile: ${profileId}`);
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
@@ -71,6 +71,62 @@ export const linuxCncSourceReferenceMap = [
|
|||||||
boundary: "linuxcnc_program_reference",
|
boundary: "linuxcnc_program_reference",
|
||||||
usage: "representative XYZAC switchkins demonstration program",
|
usage: "representative XYZAC switchkins demonstration program",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "xyzbc-trt-ini",
|
||||||
|
profileId: "xyzbc-trt",
|
||||||
|
kind: "ini",
|
||||||
|
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
|
||||||
|
boundary: "linuxcnc_config_reference",
|
||||||
|
usage: "machine profile, KINS, TRAJ coordinates, HAL and remap declarations",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "xyzbc-trt-pyvcp",
|
||||||
|
profileId: "xyzbc-trt",
|
||||||
|
kind: "pyvcp_xml",
|
||||||
|
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml",
|
||||||
|
boundary: "ui_hal_binding_reference",
|
||||||
|
usage: "SWITCHKINS labels and operator buttons",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "xyzbc-trt-table",
|
||||||
|
profileId: "xyzbc-trt",
|
||||||
|
kind: "tool_table",
|
||||||
|
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl",
|
||||||
|
boundary: "linuxcnc_config_reference",
|
||||||
|
usage: "tool table source for future LinuxCNC-backed runtime session",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "xyzbc-trt-kins",
|
||||||
|
profileId: "xyzbc-trt",
|
||||||
|
kind: "kinematics_source",
|
||||||
|
path: "src/emc/kinematics/xyzbc-trt-kins.c",
|
||||||
|
boundary: "linuxcnc_source_required",
|
||||||
|
usage: "final source-derived XYZBC kinematics implementation source",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trtfuncs-xyzbc",
|
||||||
|
profileId: "xyzbc-trt",
|
||||||
|
kind: "kinematics_source",
|
||||||
|
path: "src/emc/kinematics/trtfuncs.c",
|
||||||
|
boundary: "linuxcnc_source_required",
|
||||||
|
usage: "shared table-rotary-tilting kinematics functions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "switchkins-source-xyzbc",
|
||||||
|
profileId: "xyzbc-trt",
|
||||||
|
kind: "kinematics_source",
|
||||||
|
path: "src/emc/kinematics/switchkins.c",
|
||||||
|
boundary: "linuxcnc_source_required",
|
||||||
|
usage: "switchable kinematics behavior backing M428/M429/M430",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "xyzbc-switchkins-demo",
|
||||||
|
profileId: "xyzbc-trt",
|
||||||
|
kind: "gcode_demo",
|
||||||
|
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
|
||||||
|
boundary: "linuxcnc_program_reference",
|
||||||
|
usage: "representative XYZBC switchkins demonstration program",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getSourceReferencesForProfile(profileId) {
|
export function getSourceReferencesForProfile(profileId) {
|
||||||
|
|||||||
136
web-rtcp-5axis-sim-plan/app/src/profiles/xyzbc-trt.js
Normal file
136
web-rtcp-5axis-sim-plan/app/src/profiles/xyzbc-trt.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { xyzacTrtProfile } from "./xyzac-trt.js";
|
||||||
|
import { getSourceReferencesForProfile } from "./source-reference-map.js";
|
||||||
|
import { xyzacTrtPyvcpPanelSchema } from "../panel-schema/xyzac-trt-pyvcp.js";
|
||||||
|
|
||||||
|
const sourceReferenceObjects = getSourceReferencesForProfile("xyzbc-trt");
|
||||||
|
|
||||||
|
export const xyzbcTrtProfile = {
|
||||||
|
...xyzacTrtProfile,
|
||||||
|
id: "xyzbc-trt",
|
||||||
|
title: "XYZBC table rotary tilting",
|
||||||
|
iniPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
|
||||||
|
pyvcpXmlPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml",
|
||||||
|
generatedHalPath: null,
|
||||||
|
toolTablePath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl",
|
||||||
|
machineName: "sim-xyzbc-trt-kins (switchkins)",
|
||||||
|
display: {
|
||||||
|
...xyzacTrtProfile.display,
|
||||||
|
geometry: "XYZB",
|
||||||
|
openFile: "./demos/xyzbc_switchkins.ngc",
|
||||||
|
pyvcp: "./xyzbc-trt.xml",
|
||||||
|
},
|
||||||
|
rs274ngc: {
|
||||||
|
...xyzacTrtProfile.rs274ngc,
|
||||||
|
parameterFile: "xyzbc.var",
|
||||||
|
},
|
||||||
|
coordinates: ["X", "Y", "Z", "B", "C"],
|
||||||
|
joints: ["joint.0", "joint.1", "joint.2", "joint.3", "joint.4"],
|
||||||
|
kinematics: "xyzbc-trt-kins",
|
||||||
|
kinematicsModuleId: "xyzbc-trt",
|
||||||
|
kinematicsParameters: {
|
||||||
|
...xyzacTrtProfile.kinematicsParameters,
|
||||||
|
switchkinsTypes: [
|
||||||
|
{ value: 0, label: "identity", mdiCommand: "M429", webKinsType: "identity" },
|
||||||
|
{ value: 1, label: "XYZBC TCP", mdiCommand: "M428", webKinsType: "tcp-xyzbc" },
|
||||||
|
{ value: 2, label: "USERK", mdiCommand: "M430", webKinsType: "userk" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
hal: {
|
||||||
|
...xyzacTrtProfile.hal,
|
||||||
|
halcmd: {
|
||||||
|
...xyzacTrtProfile.hal.halcmd,
|
||||||
|
loadusr: ["xyzbc-trt-gui"],
|
||||||
|
feedbackNets: [
|
||||||
|
{ signal: "table-x", source: "joint.0.pos-fb", target: "xyzbc-trt-gui.table-x" },
|
||||||
|
{ signal: "saddle-y", source: "joint.1.pos-fb", target: "xyzbc-trt-gui.saddle-y" },
|
||||||
|
{ signal: "spindle-z", source: "joint.2.pos-fb", target: "xyzbc-trt-gui.spindle-z" },
|
||||||
|
{ signal: "tilt-b", source: "joint.3.pos-fb", target: "xyzbc-trt-gui.tilt-b" },
|
||||||
|
{ signal: "rotate-c", source: "joint.4.pos-fb", target: "xyzbc-trt-gui.rotate-c" },
|
||||||
|
],
|
||||||
|
offsetNets: [
|
||||||
|
{ signal: "tool-offset", source: "motion.tooloffset.z", target: "xyzbc-trt-kins.tool-offset" },
|
||||||
|
{ signal: "tool-offset", source: "xyzbc-trt-kins.tool-offset", target: "xyzbc-trt-gui.tool-offset" },
|
||||||
|
{ signal: "x-offset", source: "xyzbc-trt-kins.x-offset", target: "xyzbc-trt-gui.x-offset" },
|
||||||
|
{ signal: "z-offset", source: "xyzbc-trt-kins.z-offset", target: "xyzbc-trt-gui.z-offset" },
|
||||||
|
],
|
||||||
|
initialSets: [
|
||||||
|
{ pin: "x-offset", value: -20 },
|
||||||
|
{ pin: "z-offset", value: -15 },
|
||||||
|
{ pin: "xyzbc-trt-kins.x-rot-point", value: 0 },
|
||||||
|
{ pin: "xyzbc-trt-kins.y-rot-point", value: 0 },
|
||||||
|
{ pin: "xyzbc-trt-kins.z-rot-point", value: 0 },
|
||||||
|
{ pin: "xyzbc-trt-kins.conventional-directions", value: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
traj: {
|
||||||
|
...xyzacTrtProfile.traj,
|
||||||
|
coordinates: "XYZBC",
|
||||||
|
},
|
||||||
|
axisLimits: {
|
||||||
|
X: xyzacTrtProfile.axisLimits.X,
|
||||||
|
Y: xyzacTrtProfile.axisLimits.Y,
|
||||||
|
Z: xyzacTrtProfile.axisLimits.Z,
|
||||||
|
B: { min: -36000, max: 36000, maxVelocity: 30, maxAcceleration: 300 },
|
||||||
|
C: { min: -36000, max: 36000, maxVelocity: 30, maxAcceleration: 300 },
|
||||||
|
},
|
||||||
|
jointConfig: [
|
||||||
|
xyzacTrtProfile.jointConfig[0],
|
||||||
|
xyzacTrtProfile.jointConfig[1],
|
||||||
|
xyzacTrtProfile.jointConfig[2],
|
||||||
|
{ id: 3, axis: "B", type: "ANGULAR", home: 0, min: -100, max: 50, maxVelocity: 30, maxAcceleration: 300 },
|
||||||
|
{ id: 4, axis: "C", type: "ANGULAR", home: 0, min: -36000, max: 36000, maxVelocity: 30, maxAcceleration: 300 },
|
||||||
|
],
|
||||||
|
halPins: [
|
||||||
|
"motion.switchkins-type",
|
||||||
|
"motion.analog-out-03",
|
||||||
|
"motion.tooloffset.z",
|
||||||
|
"xyzbc-trt-kins.tool-offset",
|
||||||
|
"xyzbc-trt-kins.x-offset",
|
||||||
|
"xyzbc-trt-kins.z-offset",
|
||||||
|
"xyzbc-trt-kins.x-rot-point",
|
||||||
|
"xyzbc-trt-kins.y-rot-point",
|
||||||
|
"xyzbc-trt-kins.z-rot-point",
|
||||||
|
"xyzbc-trt-kins.conventional-directions",
|
||||||
|
"halui.mdi-command-00",
|
||||||
|
"halui.mdi-command-01",
|
||||||
|
"halui.mdi-command-02",
|
||||||
|
],
|
||||||
|
offsets: {
|
||||||
|
x: -20,
|
||||||
|
z: -15,
|
||||||
|
xRotPoint: 0,
|
||||||
|
yRotPoint: 0,
|
||||||
|
zRotPoint: 0,
|
||||||
|
conventionalDirections: 0,
|
||||||
|
},
|
||||||
|
sourceReferences: sourceReferenceObjects.map((reference) => reference.path),
|
||||||
|
sourceReferenceObjects,
|
||||||
|
panelSchema: {
|
||||||
|
...xyzacTrtPyvcpPanelSchema,
|
||||||
|
id: "xyzbc-trt-switchkins-pyvcp",
|
||||||
|
profileId: "xyzbc-trt",
|
||||||
|
sourceXmlPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml",
|
||||||
|
groups: xyzacTrtPyvcpPanelSchema.groups.map((group) => ({
|
||||||
|
...group,
|
||||||
|
controls: group.controls.map((control) => (
|
||||||
|
control.id === "kinstype-legends"
|
||||||
|
? {
|
||||||
|
...control,
|
||||||
|
legends: ["0:IDENTITY", "1: XYZBC ", "2: USERK "],
|
||||||
|
}
|
||||||
|
: control.id === "type1-button"
|
||||||
|
? {
|
||||||
|
...control,
|
||||||
|
text: "TCP:XYZBC",
|
||||||
|
webAction: { type: "SET_KINS_TYPE", kinsType: "tcp-xyzbc" },
|
||||||
|
}
|
||||||
|
: control
|
||||||
|
)),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
samplePrograms: [
|
||||||
|
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
|
||||||
|
"linuxcnc/nc_files/3D_Chips.ngc",
|
||||||
|
],
|
||||||
|
};
|
||||||
166
web-rtcp-5axis-sim-plan/app/src/runtime/execution-timing.js
Normal file
166
web-rtcp-5axis-sim-plan/app/src/runtime/execution-timing.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
const LINEAR_AXES = ["x", "y", "z", "u", "v", "w"];
|
||||||
|
const ANGULAR_AXES = ["a", "b", "c"];
|
||||||
|
const ALL_AXES = [...LINEAR_AXES, ...ANGULAR_AXES];
|
||||||
|
|
||||||
|
export function buildProgramExecutionTiming({
|
||||||
|
motion = [],
|
||||||
|
profile = null,
|
||||||
|
feedOverride = 100,
|
||||||
|
rapidOverride = 100,
|
||||||
|
defaultFeedRate = 100,
|
||||||
|
} = {}) {
|
||||||
|
const limits = buildVelocityLimits(profile);
|
||||||
|
const segments = [];
|
||||||
|
let previousAxes = null;
|
||||||
|
let elapsedSeconds = 0;
|
||||||
|
let feedRate = Number(defaultFeedRate) > 0 ? Number(defaultFeedRate) : 100;
|
||||||
|
|
||||||
|
for (let index = 0; index < motion.length; index += 1) {
|
||||||
|
const event = motion[index];
|
||||||
|
const axes = normalizeAxes(event.axes, previousAxes);
|
||||||
|
if (Number.isFinite(event.feedRate) && event.feedRate > 0) {
|
||||||
|
feedRate = event.feedRate;
|
||||||
|
}
|
||||||
|
const segment = buildTimingSegment({
|
||||||
|
event,
|
||||||
|
index,
|
||||||
|
axes,
|
||||||
|
previousAxes: previousAxes || axes,
|
||||||
|
limits,
|
||||||
|
feedRate,
|
||||||
|
feedOverride,
|
||||||
|
rapidOverride,
|
||||||
|
elapsedSeconds,
|
||||||
|
});
|
||||||
|
elapsedSeconds += segment.durationSeconds;
|
||||||
|
segments.push({
|
||||||
|
...segment,
|
||||||
|
elapsedSeconds,
|
||||||
|
});
|
||||||
|
previousAxes = axes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedSeconds = segments
|
||||||
|
.filter((segment) => segment.motionClass === "feed")
|
||||||
|
.reduce((total, segment) => total + segment.durationSeconds, 0);
|
||||||
|
const rapidSeconds = segments
|
||||||
|
.filter((segment) => segment.motionClass === "rapid")
|
||||||
|
.reduce((total, segment) => total + segment.durationSeconds, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-program-execution-timing",
|
||||||
|
semanticBoundary: "linuxcnc_canonical_motion_timing_estimate_not_planner_queue",
|
||||||
|
sourceBasis: "LinuxCNC canonical motion events plus INI/profile velocity limits and feed overrides",
|
||||||
|
totalSeconds: elapsedSeconds,
|
||||||
|
totalMinutes: elapsedSeconds / 60,
|
||||||
|
feedSeconds,
|
||||||
|
rapidSeconds,
|
||||||
|
motionCount: segments.length,
|
||||||
|
segments,
|
||||||
|
limits,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timingAtMotionIndex(timing, motionIndex = 0) {
|
||||||
|
const segment = timing?.segments?.[motionIndex] || null;
|
||||||
|
return {
|
||||||
|
elapsedSeconds: segment?.elapsedSeconds || 0,
|
||||||
|
remainingSeconds: Math.max((timing?.totalSeconds || 0) - (segment?.elapsedSeconds || 0), 0),
|
||||||
|
currentVelocity: segment?.velocityMmPerMin || 0,
|
||||||
|
segmentDurationSeconds: segment?.durationSeconds || 0,
|
||||||
|
segmentDistanceMm: segment?.linearDistanceMm || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTimingSegment({
|
||||||
|
event,
|
||||||
|
index,
|
||||||
|
axes,
|
||||||
|
previousAxes,
|
||||||
|
limits,
|
||||||
|
feedRate,
|
||||||
|
feedOverride,
|
||||||
|
rapidOverride,
|
||||||
|
elapsedSeconds,
|
||||||
|
}) {
|
||||||
|
const deltas = Object.fromEntries(ALL_AXES.map((axis) => [axis, axes[axis] - previousAxes[axis]]));
|
||||||
|
const linearDistanceMm = vectorLength(LINEAR_AXES.map((axis) => deltas[axis]));
|
||||||
|
const angularDistanceDeg = vectorLength(ANGULAR_AXES.map((axis) => deltas[axis]));
|
||||||
|
const motionClass = event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed";
|
||||||
|
const requestedLinearVelocity = motionClass === "rapid"
|
||||||
|
? limits.maxLinearVelocityMmPerMin * percent(rapidOverride)
|
||||||
|
: Math.max(feedRate, 0) * percent(feedOverride);
|
||||||
|
const cappedLinearVelocity = Math.min(
|
||||||
|
requestedLinearVelocity || limits.defaultLinearVelocityMmPerMin,
|
||||||
|
limits.maxLinearVelocityMmPerMin,
|
||||||
|
);
|
||||||
|
const linearSeconds = linearDistanceMm > 0
|
||||||
|
? linearDistanceMm / Math.max(cappedLinearVelocity / 60, 0.000001)
|
||||||
|
: 0;
|
||||||
|
const angularVelocityDegPerMin = motionClass === "rapid"
|
||||||
|
? limits.maxAngularVelocityDegPerMin * percent(rapidOverride)
|
||||||
|
: Math.min(Math.max(feedRate, 0) * percent(feedOverride), limits.maxAngularVelocityDegPerMin);
|
||||||
|
const angularSeconds = angularDistanceDeg > 0
|
||||||
|
? angularDistanceDeg / Math.max(angularVelocityDegPerMin / 60, 0.000001)
|
||||||
|
: 0;
|
||||||
|
const durationSeconds = Math.max(linearSeconds, angularSeconds);
|
||||||
|
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
line: event.line,
|
||||||
|
type: event.type,
|
||||||
|
motionClass,
|
||||||
|
linearDistanceMm,
|
||||||
|
angularDistanceDeg,
|
||||||
|
feedRate,
|
||||||
|
requestedVelocityMmPerMin: requestedLinearVelocity,
|
||||||
|
velocityMmPerMin: cappedLinearVelocity,
|
||||||
|
angularVelocityDegPerMin,
|
||||||
|
durationSeconds,
|
||||||
|
startSeconds: elapsedSeconds,
|
||||||
|
axes,
|
||||||
|
deltas,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVelocityLimits(profile) {
|
||||||
|
const traj = profile?.traj || {};
|
||||||
|
const axisLimits = profile?.axisLimits || {};
|
||||||
|
const maxLinearVelocity = firstFinite(
|
||||||
|
Number(traj.maxLinearVelocity) * 60,
|
||||||
|
...LINEAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * 60),
|
||||||
|
2100,
|
||||||
|
);
|
||||||
|
const defaultLinearVelocity = firstFinite(Number(traj.defaultLinearVelocity) * 60, maxLinearVelocity, 1200);
|
||||||
|
const maxAngularVelocity = firstFinite(
|
||||||
|
...ANGULAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * 60),
|
||||||
|
maxLinearVelocity,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
maxLinearVelocityMmPerMin: maxLinearVelocity,
|
||||||
|
defaultLinearVelocityMmPerMin: defaultLinearVelocity,
|
||||||
|
maxAngularVelocityDegPerMin: maxAngularVelocity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAxes(axes = {}, fallback = null) {
|
||||||
|
return Object.fromEntries(ALL_AXES.map((axis) => [
|
||||||
|
axis,
|
||||||
|
Number.isFinite(Number(axes[axis]))
|
||||||
|
? Number(axes[axis])
|
||||||
|
: Number(fallback?.[axis] || 0),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function vectorLength(values) {
|
||||||
|
return Math.sqrt(values.reduce((total, value) => total + value * value, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function percent(value) {
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? Math.max(number, 0) / 100 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstFinite(...values) {
|
||||||
|
return values.find((value) => Number.isFinite(value) && value > 0) || 1;
|
||||||
|
}
|
||||||
271
web-rtcp-5axis-sim-plan/app/src/runtime/five-axis-session.js
Normal file
271
web-rtcp-5axis-sim-plan/app/src/runtime/five-axis-session.js
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
export const FIVE_AXIS_SESSION_FORMAT = "web-rtcp-5axis-session-snapshot";
|
||||||
|
export const FIVE_AXIS_SESSION_VERSION = 1;
|
||||||
|
export const DEFAULT_SESSION_ID = "gmoccapy-web-session";
|
||||||
|
export const DEFAULT_SESSION_FILENAME = "web-rtcp-5axis-session.json";
|
||||||
|
|
||||||
|
export function createFiveAxisSessionPayload(state) {
|
||||||
|
return validateFiveAxisSessionPayload({
|
||||||
|
apiName: "web-rtcp-5axis-session-payload",
|
||||||
|
payloadVersion: 1,
|
||||||
|
machineProfile: state.machineProfile,
|
||||||
|
sessionName: state.sessionName,
|
||||||
|
sourceMode: state.sourceMode,
|
||||||
|
machine: state.machine,
|
||||||
|
runState: state.runState,
|
||||||
|
activeProgram: state.activeProgram,
|
||||||
|
programSource: state.programSource,
|
||||||
|
programStartLine: state.programStartLine,
|
||||||
|
activeLine: state.activeLine,
|
||||||
|
lineCount: state.lineCount,
|
||||||
|
fileSizeBytes: state.fileSizeBytes,
|
||||||
|
programLines: state.programLines,
|
||||||
|
axisPose: state.axisPose,
|
||||||
|
jointPose: state.jointPose,
|
||||||
|
tcpPose: state.tcpPose,
|
||||||
|
toolAxisVector: state.toolAxisVector,
|
||||||
|
rtcpState: state.rtcpState,
|
||||||
|
kinsType: state.kinsType,
|
||||||
|
feed: state.feed,
|
||||||
|
spindle: state.spindle,
|
||||||
|
coolant: state.coolant,
|
||||||
|
preview: state.preview,
|
||||||
|
toolPreview: state.toolPreview,
|
||||||
|
programExecutionSourceMode: state.programExecutionSourceMode,
|
||||||
|
programExecutionTiming: state.programExecutionTiming,
|
||||||
|
programElapsedSeconds: state.programElapsedSeconds,
|
||||||
|
programRemainingSeconds: state.programRemainingSeconds,
|
||||||
|
programRuntimeFeedback: state.programRuntimeFeedback,
|
||||||
|
programExecution: state.programExecution
|
||||||
|
? {
|
||||||
|
apiName: state.programExecution.apiName,
|
||||||
|
sourceMode: state.programExecution.sourceMode,
|
||||||
|
semanticBoundary: state.programExecution.semanticBoundary,
|
||||||
|
motion: state.programExecution.motion,
|
||||||
|
plannerTiming: state.programExecution.plannerTiming || null,
|
||||||
|
summary: state.programExecution.summary,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
kinematicsRuntimeReadiness: state.kinematicsRuntimeReadiness,
|
||||||
|
interpreterRuntimeReadiness: state.interpreterRuntimeReadiness,
|
||||||
|
linuxCncBoundaryReadiness: state.linuxCncBoundaryReadiness,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFiveAxisSessionSnapshot(sessionId, payload, options = {}) {
|
||||||
|
validateSessionId(sessionId);
|
||||||
|
validateFiveAxisSessionPayload(payload);
|
||||||
|
return {
|
||||||
|
format: FIVE_AXIS_SESSION_FORMAT,
|
||||||
|
version: FIVE_AXIS_SESSION_VERSION,
|
||||||
|
sessionId,
|
||||||
|
createdAt: options.createdAt || new Date().toISOString(),
|
||||||
|
metadata: {
|
||||||
|
source: "web-rtcp-5axis-sim-plan",
|
||||||
|
profile: payload.machineProfile,
|
||||||
|
program: payload.activeProgram,
|
||||||
|
...(options.metadata || {}),
|
||||||
|
},
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateFiveAxisSessionSnapshot(snapshot, sessionId) {
|
||||||
|
assertPlainObject(snapshot, "five-axis session snapshot");
|
||||||
|
if (snapshot.format !== FIVE_AXIS_SESSION_FORMAT) {
|
||||||
|
throw new Error(`Unsupported five-axis session snapshot format: ${snapshot.format}`);
|
||||||
|
}
|
||||||
|
if (snapshot.version !== FIVE_AXIS_SESSION_VERSION) {
|
||||||
|
throw new Error(`Unsupported five-axis session snapshot version: ${snapshot.version}`);
|
||||||
|
}
|
||||||
|
if (snapshot.sessionId !== sessionId) {
|
||||||
|
throw new Error(`Five-axis session snapshot id mismatch: ${snapshot.sessionId}`);
|
||||||
|
}
|
||||||
|
assertPlainObject(snapshot.metadata, "five-axis session snapshot metadata");
|
||||||
|
validateFiveAxisSessionPayload(snapshot.payload);
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveFiveAxisSessionSnapshot(sessionId, payload, options = {}) {
|
||||||
|
const snapshot = createFiveAxisSessionSnapshot(sessionId, payload, options);
|
||||||
|
const path = sessionSnapshotPath(sessionId, options.filename);
|
||||||
|
await saveTextFile(path, `${JSON.stringify(snapshot, null, 2)}\n`, options.storage);
|
||||||
|
return { snapshot, path };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) {
|
||||||
|
const path = sessionSnapshotPath(sessionId, options.filename);
|
||||||
|
const text = await loadTextFile(path, options.storage);
|
||||||
|
let snapshot;
|
||||||
|
try {
|
||||||
|
snapshot = JSON.parse(text);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Invalid five-axis session snapshot JSON: ${error.message}`);
|
||||||
|
}
|
||||||
|
return { snapshot: validateFiveAxisSessionSnapshot(snapshot, sessionId), path };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreFiveAxisSessionState(snapshot) {
|
||||||
|
const payload = validateFiveAxisSessionPayload(snapshot.payload);
|
||||||
|
return {
|
||||||
|
machineProfile: payload.machineProfile,
|
||||||
|
sessionName: payload.sessionName,
|
||||||
|
machine: payload.machine,
|
||||||
|
runState: payload.runState,
|
||||||
|
activeProgram: payload.activeProgram,
|
||||||
|
programSource: payload.programSource,
|
||||||
|
programStartLine: payload.programStartLine,
|
||||||
|
activeLine: payload.activeLine,
|
||||||
|
lineCount: payload.lineCount,
|
||||||
|
fileSizeBytes: payload.fileSizeBytes,
|
||||||
|
programLines: payload.programLines,
|
||||||
|
axisPose: payload.axisPose,
|
||||||
|
rtcpState: payload.rtcpState,
|
||||||
|
kinsType: payload.kinsType,
|
||||||
|
feed: payload.feed,
|
||||||
|
spindle: payload.spindle,
|
||||||
|
coolant: payload.coolant,
|
||||||
|
preview: payload.preview,
|
||||||
|
toolPreview: payload.toolPreview,
|
||||||
|
programExecutionSourceMode: payload.programExecutionSourceMode,
|
||||||
|
programExecutionTiming: payload.programExecutionTiming,
|
||||||
|
programElapsedSeconds: payload.programElapsedSeconds,
|
||||||
|
programRemainingSeconds: payload.programRemainingSeconds,
|
||||||
|
programRuntimeFeedback: payload.programRuntimeFeedback,
|
||||||
|
programExecution: payload.programExecution,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMemorySessionStorage(seed = {}) {
|
||||||
|
const files = new Map(Object.entries(seed));
|
||||||
|
return {
|
||||||
|
files,
|
||||||
|
async getDirectory() {
|
||||||
|
return createDirectoryHandle(files, []);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFiveAxisSessionPayload(payload) {
|
||||||
|
assertPlainObject(payload, "five-axis session payload");
|
||||||
|
if (payload.apiName !== "web-rtcp-5axis-session-payload") {
|
||||||
|
throw new Error(`Unsupported five-axis session payload API: ${payload.apiName}`);
|
||||||
|
}
|
||||||
|
if (payload.payloadVersion !== 1) {
|
||||||
|
throw new Error(`Unsupported five-axis session payload version: ${payload.payloadVersion}`);
|
||||||
|
}
|
||||||
|
if (!["xyzac-trt", "xyzbc-trt"].includes(payload.machineProfile)) {
|
||||||
|
throw new Error(`Unsupported five-axis machine profile: ${payload.machineProfile}`);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(payload.programLines)) {
|
||||||
|
throw new Error("five-axis session programLines must be an array.");
|
||||||
|
}
|
||||||
|
if (payload.programExecution !== null) {
|
||||||
|
assertPlainObject(payload.programExecution, "five-axis session programExecution");
|
||||||
|
if (!Array.isArray(payload.programExecution.motion)) {
|
||||||
|
throw new Error("five-axis session programExecution.motion must be an array.");
|
||||||
|
}
|
||||||
|
assertPlainObject(payload.programExecution.summary, "five-axis session programExecution.summary");
|
||||||
|
}
|
||||||
|
for (const key of ["machine", "axisPose", "feed", "spindle", "coolant", "preview", "toolPreview"]) {
|
||||||
|
assertPlainObject(payload[key], `five-axis session ${key}`);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionSnapshotPath(sessionId, filename = DEFAULT_SESSION_FILENAME) {
|
||||||
|
validateSessionId(sessionId);
|
||||||
|
validateFilename(filename);
|
||||||
|
return `web-rtcp-5axis-sim-plan/sessions/${sessionId}/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTextFile(path, text, storage = globalThis.navigator?.storage) {
|
||||||
|
const root = await getStorageRoot(storage);
|
||||||
|
const dir = await ensureParentDir(root, path);
|
||||||
|
const filename = splitPath(path).at(-1);
|
||||||
|
const fileHandle = await dir.getFileHandle(filename, { create: true });
|
||||||
|
const writable = await fileHandle.createWritable();
|
||||||
|
await writable.write(text);
|
||||||
|
await writable.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTextFile(path, storage = globalThis.navigator?.storage) {
|
||||||
|
const root = await getStorageRoot(storage);
|
||||||
|
const parts = splitPath(path);
|
||||||
|
let current = root;
|
||||||
|
for (const part of parts.slice(0, -1)) {
|
||||||
|
current = await current.getDirectoryHandle(part);
|
||||||
|
}
|
||||||
|
const fileHandle = await current.getFileHandle(parts.at(-1));
|
||||||
|
const file = await fileHandle.getFile();
|
||||||
|
return file.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStorageRoot(storage) {
|
||||||
|
if (!storage?.getDirectory) {
|
||||||
|
throw new Error("OPFS is not available in this browser.");
|
||||||
|
}
|
||||||
|
return storage.getDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureParentDir(root, path) {
|
||||||
|
let current = root;
|
||||||
|
for (const part of splitPath(path).slice(0, -1)) {
|
||||||
|
current = await current.getDirectoryHandle(part, { create: true });
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDirectoryHandle(files, prefix) {
|
||||||
|
return {
|
||||||
|
async getDirectoryHandle(name) {
|
||||||
|
return createDirectoryHandle(files, [...prefix, name]);
|
||||||
|
},
|
||||||
|
async getFileHandle(name) {
|
||||||
|
const path = [...prefix, name].join("/");
|
||||||
|
return {
|
||||||
|
async createWritable() {
|
||||||
|
let content = "";
|
||||||
|
return {
|
||||||
|
async write(text) {
|
||||||
|
content += String(text);
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
files.set(path, content);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async getFile() {
|
||||||
|
if (!files.has(path)) throw new Error(`Missing memory session file: ${path}`);
|
||||||
|
return { async text() { return files.get(path); } };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitPath(path) {
|
||||||
|
const value = String(path || "").replaceAll("\\", "/");
|
||||||
|
const parts = value.split("/").filter(Boolean);
|
||||||
|
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
|
||||||
|
throw new Error(`Invalid session path: ${path}`);
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSessionId(sessionId) {
|
||||||
|
if (!/^[a-zA-Z0-9._-]+$/.test(String(sessionId || ""))) {
|
||||||
|
throw new Error(`Invalid five-axis session id: ${sessionId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFilename(filename) {
|
||||||
|
if (!/^[a-zA-Z0-9._-]+\.json$/.test(String(filename || ""))) {
|
||||||
|
throw new Error(`Invalid five-axis session filename: ${filename}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPlainObject(value, label) {
|
||||||
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
throw new Error(`${label} must be a plain object.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
const MACHINE_FILE_FLAGS = [
|
||||||
|
"fiveaxis_ini_open=1",
|
||||||
|
"fiveaxis_remaps_ready=1",
|
||||||
|
"fiveaxis_file_reached_exit=1",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||||
|
const adapter = state.linuxCncBoundaryAdapter || {};
|
||||||
|
const frame = state.rtcpFrame || {};
|
||||||
|
const programExecution = state.programExecution || null;
|
||||||
|
const machineFileExecution = state.machineFileExecution || null;
|
||||||
|
const machineFileText = String(machineFileExecution?.resultText || "");
|
||||||
|
|
||||||
|
const kinematicsReady = Boolean(
|
||||||
|
adapter.linuxCncKinematicsReady ||
|
||||||
|
frame.readiness?.linuxCncKinematicsReady,
|
||||||
|
);
|
||||||
|
const interpreterReady = Boolean(
|
||||||
|
adapter.linuxCncInterpreterReady ||
|
||||||
|
state.interpreterRuntimeReadiness?.loaded,
|
||||||
|
);
|
||||||
|
const canonicalProgramReady = Boolean(
|
||||||
|
programExecution?.sourceMode === "linuxcnc-interpreter-wasm" &&
|
||||||
|
programExecution?.summary?.motionEventCount > 0,
|
||||||
|
);
|
||||||
|
const machineFileStagingReady = state.machineFileStaging?.status === "staged"
|
||||||
|
&& state.machineFileStaging?.fileCount > 0;
|
||||||
|
const machineFileRemapReady = Boolean(
|
||||||
|
machineFileExecution?.sourceMode === "linuxcnc-machine-file-remap-wasm" &&
|
||||||
|
machineFileExecution?.summary?.machineFileExecutionReady === true &&
|
||||||
|
MACHINE_FILE_FLAGS.every((flag) => machineFileText.includes(flag)),
|
||||||
|
);
|
||||||
|
const plannerRuntimeReady = Boolean(
|
||||||
|
programExecution?.summary?.plannerRuntimeReady === true &&
|
||||||
|
programExecution?.plannerTiming?.plannerRuntimeReady === true,
|
||||||
|
);
|
||||||
|
const halSwitchkinsEvidenceReady = machineFileText.includes("fiveaxis_hal_switchkins: rc=0 found=1");
|
||||||
|
|
||||||
|
const satisfied = [
|
||||||
|
kinematicsReady ? "linuxcnc-kinematics-wasm" : null,
|
||||||
|
interpreterReady ? "linuxcnc-interpreter-wasm" : null,
|
||||||
|
canonicalProgramReady ? "canonical-motion-events" : null,
|
||||||
|
machineFileStagingReady ? "machine-file-staging" : null,
|
||||||
|
machineFileRemapReady ? "fiveaxis-remap-machine-file-run" : null,
|
||||||
|
plannerRuntimeReady ? "linuxcnc-tp-queue-runtime-timing" : null,
|
||||||
|
halSwitchkinsEvidenceReady ? "switchkins-hal-bridge-evidence" : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const missing = [];
|
||||||
|
if (!kinematicsReady) missing.push("linuxcnc kinematics WASM frame");
|
||||||
|
if (!interpreterReady) missing.push("linuxcnc interpreter WASM runtime");
|
||||||
|
if (!canonicalProgramReady) missing.push("linuxcnc canonical motion execution");
|
||||||
|
if (!machineFileStagingReady) missing.push("LinuxCNC machine-file staging");
|
||||||
|
if (!machineFileRemapReady) missing.push("machine-file backed five-axis remap run");
|
||||||
|
if (!plannerRuntimeReady) missing.push("LinuxCNC trajectory planner queue timing runtime");
|
||||||
|
if (!halSwitchkinsEvidenceReady) missing.push("switchkins HAL bridge evidence");
|
||||||
|
|
||||||
|
const blockers = [
|
||||||
|
"native LinuxCNC task/NML process is not ported",
|
||||||
|
"native realtime HAL thread synchronization is not ported",
|
||||||
|
"external user-M process and full tool DB process are not promoted",
|
||||||
|
];
|
||||||
|
if (!plannerRuntimeReady) {
|
||||||
|
blockers.push("LinuxCNC trajectory planner queue is not promoted as browser runtime");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-full-linuxcnc-execution-boundary",
|
||||||
|
profileId: state.machineProfile || adapter.profileId || "unknown",
|
||||||
|
phase: machineFileRemapReady
|
||||||
|
? "partial-linuxcnc-remap-boundary"
|
||||||
|
: canonicalProgramReady
|
||||||
|
? "canonical-interpreter-boundary"
|
||||||
|
: "blocked",
|
||||||
|
semanticBoundary: machineFileRemapReady
|
||||||
|
? "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked"
|
||||||
|
: canonicalProgramReady
|
||||||
|
? "linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked"
|
||||||
|
: "linuxcnc_full_execution_boundary_blocked",
|
||||||
|
sourceMode: machineFileRemapReady
|
||||||
|
? "linuxcnc-machine-file-remap-wasm"
|
||||||
|
: programExecution?.sourceMode || state.programExecutionSourceMode || "fixture-line-playback",
|
||||||
|
readyForUiSimulation: kinematicsReady && interpreterReady && canonicalProgramReady,
|
||||||
|
machineFileBackedRemapReady: machineFileRemapReady,
|
||||||
|
remapRuntimeReady: machineFileRemapReady,
|
||||||
|
halSwitchkinsEvidenceReady,
|
||||||
|
plannerRuntimeReady,
|
||||||
|
nativeTaskReady: false,
|
||||||
|
nativeHalSyncReady: false,
|
||||||
|
fullLinuxCncProgramExecutionReady: false,
|
||||||
|
promotionAllowed: false,
|
||||||
|
satisfied,
|
||||||
|
missing,
|
||||||
|
blockers,
|
||||||
|
evidence: {
|
||||||
|
kinematics: kinematicsReady ? frame.semanticBoundary || adapter.semanticBoundary : null,
|
||||||
|
interpreter: interpreterReady ? state.interpreterRuntimeReadiness?.semanticBoundary || adapter.semanticBoundary : null,
|
||||||
|
canonicalMotionEvents: programExecution?.summary?.motionEventCount || 0,
|
||||||
|
canonicalEventCount: programExecution?.summary?.canonicalEventCount || 0,
|
||||||
|
plannerTiming: plannerRuntimeReady ? programExecution.plannerTiming?.semanticBoundary : null,
|
||||||
|
machineFileFlags: MACHINE_FILE_FLAGS.filter((flag) => machineFileText.includes(flag)),
|
||||||
|
machineFileExecutionReady: machineFileExecution?.summary?.machineFileExecutionReady === true,
|
||||||
|
stagedFileCount: state.machineFileStaging?.fileCount || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { createProfileSourceReferenceSummary } from "../profiles/source-referenc
|
|||||||
|
|
||||||
export function createLinuxCncBoundaryAdapter({
|
export function createLinuxCncBoundaryAdapter({
|
||||||
profile = xyzacTrtProfile,
|
profile = xyzacTrtProfile,
|
||||||
panelSchema = xyzacTrtPyvcpPanelSchema,
|
panelSchema = profile.panelSchema || xyzacTrtPyvcpPanelSchema,
|
||||||
runtime = null,
|
runtime = null,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const sourceSummary = createProfileSourceReferenceSummary(profile.id);
|
const sourceSummary = createProfileSourceReferenceSummary(profile.id);
|
||||||
@@ -14,6 +14,8 @@ export function createLinuxCncBoundaryAdapter({
|
|||||||
const runtimeReady = kinematicsRuntimeReady && interpreterRuntimeReady;
|
const runtimeReady = kinematicsRuntimeReady && interpreterRuntimeReady;
|
||||||
const linuxCncKinematicsReady = kinematicsRuntimeReady
|
const linuxCncKinematicsReady = kinematicsRuntimeReady
|
||||||
&& runtime.kinematicsWasm.sourceMode === "source-derived-kinematics-wasm";
|
&& runtime.kinematicsWasm.sourceMode === "source-derived-kinematics-wasm";
|
||||||
|
const linuxCncInterpreterReady = interpreterRuntimeReady
|
||||||
|
&& runtime.interpreterWasm.sourceMode === "linuxcnc-interpreter-wasm";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiName: "web-rtcp-5axis-linuxcnc-boundary-adapter",
|
apiName: "web-rtcp-5axis-linuxcnc-boundary-adapter",
|
||||||
@@ -24,13 +26,14 @@ export function createLinuxCncBoundaryAdapter({
|
|||||||
runtimeReady,
|
runtimeReady,
|
||||||
kinematicsRuntimeReady,
|
kinematicsRuntimeReady,
|
||||||
interpreterRuntimeReady,
|
interpreterRuntimeReady,
|
||||||
|
linuxCncInterpreterReady,
|
||||||
profileSummary: createProfileSummary(profile),
|
profileSummary: createProfileSummary(profile),
|
||||||
linuxCncKinematicsReady,
|
linuxCncKinematicsReady,
|
||||||
promotionAllowed: linuxCncKinematicsReady,
|
promotionAllowed: linuxCncKinematicsReady,
|
||||||
fullLinuxCncProgramExecutionReady: false,
|
fullLinuxCncProgramExecutionReady: false,
|
||||||
semanticBoundary: linuxCncKinematicsReady
|
semanticBoundary: linuxCncKinematicsReady
|
||||||
? interpreterRuntimeReady
|
? linuxCncInterpreterReady
|
||||||
? "linuxcnc_runtime_supplied_but_interpreter_or_remap_not_promoted"
|
? "linuxcnc_kinematics_and_interpreter_wasm_connected_remap_planner_not_promoted"
|
||||||
: "linuxcnc_kinematics_wasm_runtime_connected"
|
: "linuxcnc_kinematics_wasm_runtime_connected"
|
||||||
: "adapter_entrypoint_only_runtime_not_connected",
|
: "adapter_entrypoint_only_runtime_not_connected",
|
||||||
adapterPoints: {
|
adapterPoints: {
|
||||||
@@ -74,6 +77,7 @@ export function createLinuxCncBoundaryReadiness(adapter = createLinuxCncBoundary
|
|||||||
&& !missing.includes("PyVCP/HAL panel schema"),
|
&& !missing.includes("PyVCP/HAL panel schema"),
|
||||||
missing,
|
missing,
|
||||||
linuxCncKinematicsReady: adapter.linuxCncKinematicsReady,
|
linuxCncKinematicsReady: adapter.linuxCncKinematicsReady,
|
||||||
|
linuxCncInterpreterReady: adapter.linuxCncInterpreterReady,
|
||||||
promotionAllowed: adapter.promotionAllowed,
|
promotionAllowed: adapter.promotionAllowed,
|
||||||
fullLinuxCncProgramExecutionReady: adapter.fullLinuxCncProgramExecutionReady,
|
fullLinuxCncProgramExecutionReady: adapter.fullLinuxCncProgramExecutionReady,
|
||||||
semanticBoundary: adapter.semanticBoundary,
|
semanticBoundary: adapter.semanticBoundary,
|
||||||
|
|||||||
345
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-ini-runtime.js
Normal file
345
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-ini-runtime.js
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
const AXIS_SECTION_RE = /^AXIS_([A-Z])$/;
|
||||||
|
const JOINT_SECTION_RE = /^JOINT_(\d+)$/;
|
||||||
|
|
||||||
|
export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unknown" } = {}) {
|
||||||
|
const sections = parseIniSections(text);
|
||||||
|
const kinsText = getFirstValue(sections, "KINS", "KINEMATICS") || "";
|
||||||
|
const coordinates = getFirstValue(sections, "TRAJ", "COORDINATES") || "";
|
||||||
|
const jointCount = numberOrNull(getFirstValue(sections, "KINS", "JOINTS"));
|
||||||
|
const axisLimits = parseAxisLimits(sections);
|
||||||
|
const jointConfig = parseJointConfig(sections, coordinates);
|
||||||
|
const remaps = parseRemaps(sections);
|
||||||
|
const hal = parseHal(sections);
|
||||||
|
const display = parseDisplay(sections);
|
||||||
|
const halui = {
|
||||||
|
mdiCommands: getValues(sections, "HALUI", "MDI_COMMAND"),
|
||||||
|
};
|
||||||
|
const kinematicsModuleId = inferKinematicsModuleId(kinsText);
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-ini-config",
|
||||||
|
profileId,
|
||||||
|
path,
|
||||||
|
machineName: getFirstValue(sections, "EMC", "MACHINE") || null,
|
||||||
|
kinematics: parseKinematics(kinsText),
|
||||||
|
kinematicsModuleId,
|
||||||
|
kinematicsParameters: {
|
||||||
|
sparm: parseKinematicsParameter(kinsText, "sparm"),
|
||||||
|
joints: jointCount,
|
||||||
|
switchkinsTypes: inferSwitchkinsTypes({ coordinates, halui, kinematicsModuleId }),
|
||||||
|
},
|
||||||
|
traj: {
|
||||||
|
coordinates,
|
||||||
|
linearUnits: getFirstValue(sections, "TRAJ", "LINEAR_UNITS") || null,
|
||||||
|
angularUnits: getFirstValue(sections, "TRAJ", "ANGULAR_UNITS") || null,
|
||||||
|
defaultLinearVelocity: numberOrNull(getFirstValue(sections, "TRAJ", "DEFAULT_LINEAR_VELOCITY")),
|
||||||
|
maxLinearVelocity: numberOrNull(getFirstValue(sections, "TRAJ", "MAX_LINEAR_VELOCITY")),
|
||||||
|
defaultLinearAcceleration: numberOrNull(getFirstValue(sections, "TRAJ", "DEFAULT_LINEAR_ACCELERATION")),
|
||||||
|
maxLinearAcceleration: numberOrNull(getFirstValue(sections, "TRAJ", "MAX_LINEAR_ACCELERATION")),
|
||||||
|
},
|
||||||
|
display,
|
||||||
|
rs274ngc: {
|
||||||
|
subroutinePath: getFirstValue(sections, "RS274NGC", "SUBROUTINE_PATH") || null,
|
||||||
|
halPinVars: boolFromIni(getFirstValue(sections, "RS274NGC", "HAL_PIN_VARS")),
|
||||||
|
parameterFile: getFirstValue(sections, "RS274NGC", "PARAMETER_FILE") || null,
|
||||||
|
remaps,
|
||||||
|
},
|
||||||
|
hal,
|
||||||
|
halui,
|
||||||
|
axisLimits,
|
||||||
|
jointConfig,
|
||||||
|
emcio: {
|
||||||
|
toolTable: getFirstValue(sections, "EMCIO", "TOOL_TABLE") || null,
|
||||||
|
},
|
||||||
|
validation: validateIniConfig({ coordinates, jointCount, axisLimits, jointConfig, kinsText }),
|
||||||
|
semanticBoundary: "linuxcnc_ini_file_browser_parser",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadLinuxCncIniConfig(profile, { baseUrl = import.meta.url, fetchImpl = globalThis.fetch } = {}) {
|
||||||
|
if (!profile?.iniPath) {
|
||||||
|
throw new Error("profile is missing iniPath");
|
||||||
|
}
|
||||||
|
if (typeof fetchImpl !== "function") {
|
||||||
|
throw new Error("fetch is not available for LinuxCNC INI loading");
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateUrls = [
|
||||||
|
new URL(`../../${profile.iniPath}`, baseUrl),
|
||||||
|
new URL(`../../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, baseUrl),
|
||||||
|
];
|
||||||
|
const errors = [];
|
||||||
|
let response = null;
|
||||||
|
for (const url of candidateUrls) {
|
||||||
|
try {
|
||||||
|
response = await fetchImpl(url.href);
|
||||||
|
if (response.ok) break;
|
||||||
|
errors.push(`${url.href}: HTTP ${response.status}`);
|
||||||
|
response = null;
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${url.href}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!response) {
|
||||||
|
throw new Error(`failed to load LinuxCNC INI ${profile.iniPath}: ${errors.join(" | ")}`);
|
||||||
|
}
|
||||||
|
return parseLinuxCncIni(await response.text(), {
|
||||||
|
path: profile.iniPath,
|
||||||
|
profileId: profile.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyIniConfigToProfile(profile, iniConfig) {
|
||||||
|
if (!iniConfig) return profile;
|
||||||
|
return {
|
||||||
|
...profile,
|
||||||
|
machineName: iniConfig.machineName || profile.machineName,
|
||||||
|
kinematics: iniConfig.kinematics.name || profile.kinematics,
|
||||||
|
kinematicsModuleId: iniConfig.kinematicsModuleId || profile.kinematicsModuleId,
|
||||||
|
kinematicsParameters: {
|
||||||
|
...profile.kinematicsParameters,
|
||||||
|
...iniConfig.kinematicsParameters,
|
||||||
|
switchkinsTypes: mergeSwitchkinsTypes(
|
||||||
|
profile.kinematicsParameters?.switchkinsTypes || [],
|
||||||
|
iniConfig.kinematicsParameters.switchkinsTypes,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
...profile.display,
|
||||||
|
...iniConfig.display,
|
||||||
|
},
|
||||||
|
rs274ngc: {
|
||||||
|
...profile.rs274ngc,
|
||||||
|
subroutinePath: iniConfig.rs274ngc.subroutinePath || profile.rs274ngc?.subroutinePath,
|
||||||
|
halPinVars: iniConfig.rs274ngc.halPinVars ?? profile.rs274ngc?.halPinVars,
|
||||||
|
parameterFile: iniConfig.rs274ngc.parameterFile || profile.rs274ngc?.parameterFile,
|
||||||
|
},
|
||||||
|
hal: {
|
||||||
|
...profile.hal,
|
||||||
|
halui: iniConfig.hal.halui || profile.hal?.halui,
|
||||||
|
halFiles: iniConfig.hal.halFiles.length > 0 ? iniConfig.hal.halFiles : profile.hal?.halFiles,
|
||||||
|
postguiHalFiles: iniConfig.hal.postguiHalFiles.length > 0
|
||||||
|
? iniConfig.hal.postguiHalFiles
|
||||||
|
: profile.hal?.postguiHalFiles,
|
||||||
|
halcmd: {
|
||||||
|
...profile.hal?.halcmd,
|
||||||
|
raw: iniConfig.hal.halcmd,
|
||||||
|
initialSets: iniConfig.hal.initialSets.length > 0
|
||||||
|
? iniConfig.hal.initialSets
|
||||||
|
: profile.hal?.halcmd?.initialSets,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
halui: iniConfig.halui.mdiCommands.length > 0 ? iniConfig.halui : profile.halui,
|
||||||
|
traj: {
|
||||||
|
...profile.traj,
|
||||||
|
...dropNullish(iniConfig.traj),
|
||||||
|
},
|
||||||
|
axisLimits: Object.keys(iniConfig.axisLimits).length > 0 ? iniConfig.axisLimits : profile.axisLimits,
|
||||||
|
jointConfig: iniConfig.jointConfig.length > 0 ? iniConfig.jointConfig : profile.jointConfig,
|
||||||
|
linuxCncIniConfig: iniConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIniSections(text) {
|
||||||
|
const sections = new Map();
|
||||||
|
let current = null;
|
||||||
|
for (const rawLine of String(text).split(/\r?\n/)) {
|
||||||
|
const line = stripIniComment(rawLine).trim();
|
||||||
|
if (!line) continue;
|
||||||
|
const sectionMatch = line.match(/^\[([^\]]+)]$/);
|
||||||
|
if (sectionMatch) {
|
||||||
|
current = sectionMatch[1].trim().toUpperCase();
|
||||||
|
if (!sections.has(current)) sections.set(current, new Map());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!current) continue;
|
||||||
|
const equals = line.indexOf("=");
|
||||||
|
if (equals < 0) continue;
|
||||||
|
const key = line.slice(0, equals).trim().toUpperCase();
|
||||||
|
const value = line.slice(equals + 1).trim();
|
||||||
|
const section = sections.get(current);
|
||||||
|
if (!section.has(key)) section.set(key, []);
|
||||||
|
section.get(key).push(value);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripIniComment(line) {
|
||||||
|
let quote = null;
|
||||||
|
for (let index = 0; index < line.length; index += 1) {
|
||||||
|
const char = line[index];
|
||||||
|
if ((char === "\"" || char === "'") && line[index - 1] !== "\\") {
|
||||||
|
quote = quote === char ? null : quote || char;
|
||||||
|
}
|
||||||
|
if (!quote && (char === "#" || char === ";")) {
|
||||||
|
return line.slice(0, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValues(sections, sectionName, key) {
|
||||||
|
return sections.get(sectionName.toUpperCase())?.get(key.toUpperCase()) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFirstValue(sections, sectionName, key) {
|
||||||
|
return getValues(sections, sectionName, key)[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAxisLimits(sections) {
|
||||||
|
const result = {};
|
||||||
|
for (const [sectionName, values] of sections) {
|
||||||
|
const match = sectionName.match(AXIS_SECTION_RE);
|
||||||
|
if (!match) continue;
|
||||||
|
result[match[1]] = {
|
||||||
|
min: numberOrNull(first(values, "MIN_LIMIT")),
|
||||||
|
max: numberOrNull(first(values, "MAX_LIMIT")),
|
||||||
|
maxVelocity: numberOrNull(first(values, "MAX_VELOCITY")),
|
||||||
|
maxAcceleration: numberOrNull(first(values, "MAX_ACCELERATION")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJointConfig(sections, coordinates) {
|
||||||
|
const axisOrder = String(coordinates || "").split("");
|
||||||
|
return [...sections.entries()]
|
||||||
|
.map(([sectionName, values]) => {
|
||||||
|
const match = sectionName.match(JOINT_SECTION_RE);
|
||||||
|
if (!match) return null;
|
||||||
|
const id = Number(match[1]);
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
axis: axisOrder[id] || null,
|
||||||
|
type: first(values, "TYPE") || null,
|
||||||
|
home: numberOrNull(first(values, "HOME")),
|
||||||
|
min: numberOrNull(first(values, "MIN_LIMIT")),
|
||||||
|
max: numberOrNull(first(values, "MAX_LIMIT")),
|
||||||
|
maxVelocity: numberOrNull(first(values, "MAX_VELOCITY")),
|
||||||
|
maxAcceleration: numberOrNull(first(values, "MAX_ACCELERATION")),
|
||||||
|
homeSearchVelocity: numberOrNull(first(values, "HOME_SEARCH_VEL")),
|
||||||
|
homeSequence: numberOrNull(first(values, "HOME_SEQUENCE")),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((left, right) => left.id - right.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRemaps(sections) {
|
||||||
|
return getValues(sections, "RS274NGC", "REMAP").map((value) => {
|
||||||
|
const code = value.match(/\bM\d+\b/i)?.[0]?.toUpperCase() || null;
|
||||||
|
const ngc = value.match(/\bngc=([^\s]+)/i)?.[1] || null;
|
||||||
|
const modalGroup = numberOrNull(value.match(/\bmodalgroup=(\d+)/i)?.[1]);
|
||||||
|
return { raw: value, code, modalGroup, ngc };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHal(sections) {
|
||||||
|
const halcmd = getValues(sections, "HAL", "HALCMD");
|
||||||
|
return {
|
||||||
|
halui: getFirstValue(sections, "HAL", "HALUI") || null,
|
||||||
|
halFiles: getValues(sections, "HAL", "HALFILE"),
|
||||||
|
postguiHalFiles: getValues(sections, "HAL", "POSTGUI_HALFILE"),
|
||||||
|
halcmd,
|
||||||
|
initialSets: halcmd
|
||||||
|
.map((line) => line.match(/^\s*(sets|setp)\s+:?([^\s]+)\s+([-+0-9.eE]+)/i))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((match) => ({ op: match[1], pin: match[2], value: Number(match[3]) })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDisplay(sections) {
|
||||||
|
const jogAxes = getFirstValue(sections, "DISPLAY", "JOG_AXES");
|
||||||
|
return dropNullish({
|
||||||
|
geometry: getFirstValue(sections, "DISPLAY", "GEOMETRY"),
|
||||||
|
display: getFirstValue(sections, "DISPLAY", "DISPLAY"),
|
||||||
|
jogAxes: jogAxes ? jogAxes.split("") : null,
|
||||||
|
pyvcp: getFirstValue(sections, "DISPLAY", "PYVCP"),
|
||||||
|
openFile: getFirstValue(sections, "DISPLAY", "OPEN_FILE"),
|
||||||
|
programPrefix: getFirstValue(sections, "DISPLAY", "PROGRAM_PREFIX"),
|
||||||
|
positionOffset: getFirstValue(sections, "DISPLAY", "POSITION_OFFSET"),
|
||||||
|
positionFeedback: getFirstValue(sections, "DISPLAY", "POSITION_FEEDBACK"),
|
||||||
|
maxFeedOverride: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_FEED_OVERRIDE")),
|
||||||
|
maxLinearVelocity: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_LINEAR_VELOCITY")),
|
||||||
|
maxAngularVelocity: numberOrNull(getFirstValue(sections, "DISPLAY", "MAX_ANGULAR_VELOCITY")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseKinematics(text) {
|
||||||
|
const [name, ...parameters] = String(text || "").trim().split(/\s+/).filter(Boolean);
|
||||||
|
return {
|
||||||
|
name: name || null,
|
||||||
|
raw: text,
|
||||||
|
parameters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseKinematicsParameter(text, key) {
|
||||||
|
return String(text || "").match(new RegExp(`\\b${key}=([^\\s]+)`, "i"))?.[1] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferKinematicsModuleId(kinsText) {
|
||||||
|
const name = parseKinematics(kinsText).name || "";
|
||||||
|
if (name.includes("xyzbc")) return "xyzbc-trt";
|
||||||
|
if (name.includes("xyzac")) return "xyzac-trt";
|
||||||
|
return name.replace(/-kins$/, "") || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferSwitchkinsTypes({ coordinates, halui, kinematicsModuleId }) {
|
||||||
|
const mdiCommands = halui.mdiCommands.length > 0 ? halui.mdiCommands : ["M429", "M428", "M430"];
|
||||||
|
const tcpType = coordinates === "XYZBC" ? "tcp-xyzbc" : "tcp-xyzac";
|
||||||
|
const tcpLabel = `${coordinates || kinematicsModuleId || "TCP"} TCP`;
|
||||||
|
return mdiCommands.map((command, index) => ({
|
||||||
|
value: index === 0 ? 0 : index,
|
||||||
|
label: index === 0 ? "identity" : index === 1 ? tcpLabel : "USERK",
|
||||||
|
mdiCommand: command,
|
||||||
|
webKinsType: index === 0 ? "identity" : index === 1 ? tcpType : "userk",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateIniConfig({ coordinates, jointCount, axisLimits, jointConfig, kinsText }) {
|
||||||
|
const missing = [];
|
||||||
|
if (!coordinates) missing.push("TRAJ.COORDINATES");
|
||||||
|
if (!jointCount) missing.push("KINS.JOINTS");
|
||||||
|
if (!kinsText) missing.push("KINS.KINEMATICS");
|
||||||
|
for (const axis of String(coordinates || "").split("")) {
|
||||||
|
if (!axisLimits[axis]) missing.push(`AXIS_${axis}`);
|
||||||
|
}
|
||||||
|
if (jointCount && jointConfig.length !== jointCount) {
|
||||||
|
missing.push(`JOINT_ count ${jointConfig.length}/${jointCount}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: missing.length === 0,
|
||||||
|
missing,
|
||||||
|
axisCount: Object.keys(axisLimits).length,
|
||||||
|
jointCount: jointConfig.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function first(values, key) {
|
||||||
|
return values.get(key)?.[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberOrNull(value) {
|
||||||
|
if (value === null || value === undefined || value === "") return null;
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? number : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function boolFromIni(value) {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropNullish(object) {
|
||||||
|
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== null && value !== undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSwitchkinsTypes(profileTypes, iniTypes) {
|
||||||
|
if (!iniTypes?.length) return profileTypes;
|
||||||
|
return iniTypes.map((iniType) => ({
|
||||||
|
...iniType,
|
||||||
|
...(profileTypes.find((entry) => entry.mdiCommand === iniType.mdiCommand || entry.value === iniType.value) || {}),
|
||||||
|
...iniType,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
const DEFAULT_SDK_MODULE_URL = "../../../../wasm-port/runtime/sdk/src/linuxcnc-interp.js";
|
||||||
|
const DEFAULT_TP_SDK_MODULE_URL = "../../../../wasm-port/runtime/sdk/src/linuxcnc-tp.js";
|
||||||
|
const SOURCE_MODE = "linuxcnc-interpreter-wasm";
|
||||||
|
const SEMANTIC_BOUNDARY = "linuxcnc_interpreter_wasm_canonical_events";
|
||||||
|
const PLANNER_TIMING_BOUNDARY = "linuxcnc_tp_queue_runtime_timing_from_canonical_motion";
|
||||||
|
const SWITCHKINS_REMAP_BOUNDARY = "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied";
|
||||||
|
const FIVE_AXIS_REMAP_FLAGS = [
|
||||||
|
"fiveaxis_ini_open=1",
|
||||||
|
"fiveaxis_remaps_ready=1",
|
||||||
|
"fiveaxis_file_reached_exit=1",
|
||||||
|
];
|
||||||
|
|
||||||
|
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
|
||||||
|
const SWITCHKINS_M_CODES = new Map([
|
||||||
|
[428, { switchkinsType: 1, requestedKinsType: "tcp" }],
|
||||||
|
[429, { switchkinsType: 0, requestedKinsType: "identity" }],
|
||||||
|
[430, { switchkinsType: 2, requestedKinsType: "userk" }],
|
||||||
|
]);
|
||||||
|
const PLANE_AXIS_MAP = {
|
||||||
|
170: ["x", "y", "z"],
|
||||||
|
180: ["x", "z", "y"],
|
||||||
|
190: ["y", "z", "x"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createLinuxCncInterpreterRuntime({
|
||||||
|
moduleOptions = null,
|
||||||
|
tpModuleOptions = null,
|
||||||
|
wasmRoot = null,
|
||||||
|
sdkModuleUrl = DEFAULT_SDK_MODULE_URL,
|
||||||
|
tpSdkModuleUrl = DEFAULT_TP_SDK_MODULE_URL,
|
||||||
|
} = {}) {
|
||||||
|
const { createLinuxCncInterpSdk } = await import(sdkModuleUrl);
|
||||||
|
const resolvedModuleOptions = moduleOptions || await createDefaultModuleOptions({ wasmRoot });
|
||||||
|
const sdk = await createLinuxCncInterpSdk(resolvedModuleOptions);
|
||||||
|
const tpRuntime = await createOptionalTpRuntime({ tpModuleOptions, wasmRoot, tpSdkModuleUrl });
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-interpreter-runtime",
|
||||||
|
loaded: true,
|
||||||
|
sourceMode: SOURCE_MODE,
|
||||||
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
|
executionContext: "direct",
|
||||||
|
sdk,
|
||||||
|
tpRuntime,
|
||||||
|
|
||||||
|
readiness() {
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-interpreter-runtime-readiness",
|
||||||
|
loaded: true,
|
||||||
|
sourceMode: SOURCE_MODE,
|
||||||
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
|
executionContext: "direct",
|
||||||
|
runProgramReady: typeof sdk.runProgram === "function",
|
||||||
|
remapRuntimeReady: false,
|
||||||
|
plannerRuntimeReady: tpRuntime?.loaded === true,
|
||||||
|
plannerSemanticBoundary: tpRuntime?.semanticBoundary || null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
runProgram(programText) {
|
||||||
|
const prepared = prepareLinuxCncProgramForRuntime(programText);
|
||||||
|
const resultText = sdk.runProgram(prepared.runtimeProgramText);
|
||||||
|
const motion = parseLinuxCncCanonicalMotion(resultText, programText, prepared.switchkinsEvents);
|
||||||
|
return createProgramExecutionResult({
|
||||||
|
programText,
|
||||||
|
resultText: prependRuntimeEvents(resultText, prepared.switchkinsEvents),
|
||||||
|
motion,
|
||||||
|
switchkinsEvents: prepared.switchkinsEvents,
|
||||||
|
runtimeProgramText: prepared.runtimeProgramText,
|
||||||
|
plannerTiming: runPlannerTiming(tpRuntime, motion),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
runMachineFileProgram({ plan, files = null, executionMode = "fiveAxisRemap" } = {}) {
|
||||||
|
if (!plan?.wasmIniPath || !plan?.wasmProgramPath) {
|
||||||
|
throw new Error("runMachineFileProgram requires a machine-file staging plan with INI and program paths");
|
||||||
|
}
|
||||||
|
if (typeof sdk.runSimConfigProgram !== "function") {
|
||||||
|
throw new Error("LinuxCNC interpreter SDK missing runSimConfigProgram");
|
||||||
|
}
|
||||||
|
const stagedFiles = files || plan.files;
|
||||||
|
const resultText = sdk.runSimConfigProgram({
|
||||||
|
files: stagedFiles.map((file) => ({
|
||||||
|
path: file.wasmPath || file.path,
|
||||||
|
text: file.text,
|
||||||
|
executable: file.executable,
|
||||||
|
})),
|
||||||
|
programPath: plan.wasmProgramPath,
|
||||||
|
iniPath: plan.wasmIniPath,
|
||||||
|
executionMode,
|
||||||
|
});
|
||||||
|
const programFile = stagedFiles.find((file) => (
|
||||||
|
(file.wasmPath || file.path) === plan.wasmProgramPath
|
||||||
|
));
|
||||||
|
const programText = programFile?.text || "";
|
||||||
|
const motion = parseLinuxCncCanonicalMotion(resultText, programText);
|
||||||
|
return createProgramExecutionResult({
|
||||||
|
programText,
|
||||||
|
resultText,
|
||||||
|
motion,
|
||||||
|
machineFilePlan: plan,
|
||||||
|
sourceMode: "linuxcnc-machine-file-remap-wasm",
|
||||||
|
semanticBoundary: "linuxcnc_fiveaxis_remap_wasm_machine_file_execution",
|
||||||
|
plannerTiming: runPlannerTiming(tpRuntime, motion),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLinuxCncInterpreterRuntimeDescriptor(runtime) {
|
||||||
|
if (!runtime?.loaded) return null;
|
||||||
|
return {
|
||||||
|
apiName: runtime.apiName,
|
||||||
|
loaded: runtime.loaded,
|
||||||
|
sourceMode: runtime.sourceMode,
|
||||||
|
semanticBoundary: runtime.semanticBoundary,
|
||||||
|
executionContext: runtime.executionContext || "direct",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProgramExecutionResult({
|
||||||
|
programText,
|
||||||
|
resultText,
|
||||||
|
motion,
|
||||||
|
switchkinsEvents = [],
|
||||||
|
runtimeProgramText = programText,
|
||||||
|
machineFilePlan = null,
|
||||||
|
sourceMode = SOURCE_MODE,
|
||||||
|
semanticBoundary = SEMANTIC_BOUNDARY,
|
||||||
|
plannerTiming = null,
|
||||||
|
}) {
|
||||||
|
const canonicalEventCount = String(resultText).split("\n").filter((line) => line.startsWith("canon_event=")).length;
|
||||||
|
const machineFileExecutionReady = Boolean(
|
||||||
|
machineFilePlan && FIVE_AXIS_REMAP_FLAGS.every((flag) => String(resultText).includes(flag)),
|
||||||
|
);
|
||||||
|
const plannerRuntimeReady = plannerTiming?.plannerRuntimeReady === true
|
||||||
|
&& plannerTiming.motionCount === motion.length;
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-interpreter-program-execution",
|
||||||
|
sourceMode,
|
||||||
|
semanticBoundary,
|
||||||
|
resultText,
|
||||||
|
runtimeProgramText,
|
||||||
|
motion,
|
||||||
|
plannerTiming,
|
||||||
|
switchkinsEvents,
|
||||||
|
switchkinsRemapBoundary: switchkinsEvents.length > 0 ? SWITCHKINS_REMAP_BOUNDARY : null,
|
||||||
|
machineFilePlan: machineFilePlan
|
||||||
|
? {
|
||||||
|
apiName: machineFilePlan.apiName,
|
||||||
|
profileId: machineFilePlan.profileId,
|
||||||
|
wasmIniPath: machineFilePlan.wasmIniPath,
|
||||||
|
wasmProgramPath: machineFilePlan.wasmProgramPath,
|
||||||
|
selectedProgramSourceRel: machineFilePlan.selectedProgramSourceRel || null,
|
||||||
|
selectedProgramFilename: machineFilePlan.selectedProgramFilename || null,
|
||||||
|
fileCount: machineFilePlan.files?.length ?? 0,
|
||||||
|
semanticBoundary: machineFilePlan.semanticBoundary,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
summary: {
|
||||||
|
ready: motion.length > 0,
|
||||||
|
programLineCount: programText.split(/\r?\n/).filter((line) => line.trim()).length,
|
||||||
|
canonicalEventCount,
|
||||||
|
motionEventCount: motion.length,
|
||||||
|
motionTypes: [...new Set(motion.map((event) => event.type))],
|
||||||
|
finalAxes: motion.at(-1)?.axes || Object.fromEntries(AXES.map((axis) => [axis, 0])),
|
||||||
|
switchkinsEventCount: switchkinsEvents.length,
|
||||||
|
switchkinsCodes: [...new Set(switchkinsEvents.map((event) => event.code))],
|
||||||
|
switchkinsRemapBoundary: switchkinsEvents.length > 0 ? SWITCHKINS_REMAP_BOUNDARY : null,
|
||||||
|
remapRuntimeReady: machineFileExecutionReady,
|
||||||
|
plannerRuntimeReady,
|
||||||
|
plannerSemanticBoundary: plannerRuntimeReady ? PLANNER_TIMING_BOUNDARY : null,
|
||||||
|
machineFileExecutionReady,
|
||||||
|
fullLinuxCncProgramExecutionReady: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOptionalTpRuntime({ tpModuleOptions, wasmRoot, tpSdkModuleUrl }) {
|
||||||
|
try {
|
||||||
|
const { createLinuxCncTpSdk } = await import(tpSdkModuleUrl);
|
||||||
|
const resolvedOptions = tpModuleOptions || await createDefaultTpModuleOptions({ wasmRoot });
|
||||||
|
const sdk = await createLinuxCncTpSdk(resolvedOptions);
|
||||||
|
return {
|
||||||
|
loaded: true,
|
||||||
|
semanticBoundary: PLANNER_TIMING_BOUNDARY,
|
||||||
|
sdk,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
semanticBoundary: null,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPlannerTiming(tpRuntime, motion) {
|
||||||
|
if (!tpRuntime?.loaded || typeof tpRuntime.sdk?.runCanonicalMotionTiming !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return tpRuntime.sdk.runCanonicalMotionTiming({
|
||||||
|
motion,
|
||||||
|
options: {
|
||||||
|
cycleTime: 0.001,
|
||||||
|
queueSize: 32,
|
||||||
|
maxCycles: 2000000,
|
||||||
|
sampleStride: 10,
|
||||||
|
maxVelocity: 35,
|
||||||
|
maxAcceleration: 500,
|
||||||
|
maxJerk: 1000,
|
||||||
|
tolerance: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-tp-program-timing",
|
||||||
|
semanticBoundary: PLANNER_TIMING_BOUNDARY,
|
||||||
|
plannerRuntimeReady: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
motionCount: motion.length,
|
||||||
|
totalSeconds: 0,
|
||||||
|
totalMinutes: 0,
|
||||||
|
feedSeconds: 0,
|
||||||
|
rapidSeconds: 0,
|
||||||
|
segments: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseLinuxCncCanonicalMotion(resultText, programText = "", switchkinsEvents = []) {
|
||||||
|
const axes = Object.fromEntries(AXES.map((axis) => [axis, 0]));
|
||||||
|
const sourceLines = programLineMap(programText);
|
||||||
|
const feedRatesByLine = feedRatesBySourceLine(programText);
|
||||||
|
const switchkinsByLine = switchkinsEventsByLine(switchkinsEvents);
|
||||||
|
const motion = [];
|
||||||
|
let activePlane = 170;
|
||||||
|
let activeSwitchkinsEvent = null;
|
||||||
|
let activeFeedRate = null;
|
||||||
|
|
||||||
|
for (const line of String(resultText).split("\n")) {
|
||||||
|
const feedRate = readCanonicalNumber(line, "feed_rate");
|
||||||
|
if (Number.isFinite(feedRate) && feedRate > 0) {
|
||||||
|
activeFeedRate = feedRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plane = readCanonicalNumber(line, "plane");
|
||||||
|
if (plane && PLANE_AXIS_MAP[plane]) {
|
||||||
|
activePlane = plane;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);
|
||||||
|
if (!event) continue;
|
||||||
|
|
||||||
|
if (event[1] === "ARC_FEED") {
|
||||||
|
const [firstAxis, secondAxis, thirdAxis] = PLANE_AXIS_MAP[activePlane] ?? PLANE_AXIS_MAP[170];
|
||||||
|
const firstEnd = readCanonicalNumber(line, "first_end");
|
||||||
|
const secondEnd = readCanonicalNumber(line, "second_end");
|
||||||
|
const axisEndPoint = readCanonicalNumber(line, "axis_end_point");
|
||||||
|
if (Number.isFinite(firstEnd)) axes[firstAxis] = firstEnd;
|
||||||
|
if (Number.isFinite(secondEnd)) axes[secondAxis] = secondEnd;
|
||||||
|
if (Number.isFinite(axisEndPoint)) axes[thirdAxis] = axisEndPoint;
|
||||||
|
axes.arc = {
|
||||||
|
plane: activePlane,
|
||||||
|
firstAxis,
|
||||||
|
secondAxis,
|
||||||
|
thirdAxis,
|
||||||
|
firstEnd,
|
||||||
|
secondEnd,
|
||||||
|
centerFirst: readCanonicalNumber(line, "first_axis"),
|
||||||
|
centerSecond: readCanonicalNumber(line, "second_axis"),
|
||||||
|
rotation: readCanonicalNumber(line, "rotation"),
|
||||||
|
axisEndPoint,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
for (const axis of AXES) {
|
||||||
|
const value = readCanonicalNumber(line, axis);
|
||||||
|
if (value !== null && Number.isFinite(value)) axes[axis] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceLine = readCanonicalNumber(line, "line");
|
||||||
|
if (Number.isFinite(sourceLine)) {
|
||||||
|
const event = latestSwitchkinsEventAtOrBeforeLine(switchkinsByLine, sourceLine);
|
||||||
|
if (event) activeSwitchkinsEvent = event;
|
||||||
|
const sourceFeedRate = latestFeedRateAtOrBeforeLine(feedRatesByLine, sourceLine);
|
||||||
|
if (Number.isFinite(sourceFeedRate) && sourceFeedRate > 0) {
|
||||||
|
activeFeedRate = sourceFeedRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
motion.push({
|
||||||
|
type: event[1],
|
||||||
|
line: Number.isFinite(sourceLine) ? sourceLine : null,
|
||||||
|
statement: Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-",
|
||||||
|
axes: { ...axes },
|
||||||
|
kinsType: activeSwitchkinsEvent?.requestedKinsType || null,
|
||||||
|
switchkinsType: activeSwitchkinsEvent?.switchkinsType ?? null,
|
||||||
|
switchkinsCode: activeSwitchkinsEvent?.code || null,
|
||||||
|
switchkinsRemapBoundary: activeSwitchkinsEvent ? SWITCHKINS_REMAP_BOUNDARY : null,
|
||||||
|
feedRate: activeFeedRate,
|
||||||
|
raw: line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return motion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedRatesBySourceLine(programText) {
|
||||||
|
const rates = [];
|
||||||
|
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||||
|
const codeOnly = stripComments(line);
|
||||||
|
let feedRate = null;
|
||||||
|
for (const match of codeOnly.matchAll(/\bF\s*([-+]?\d+(?:\.\d+)?)/gi)) {
|
||||||
|
const value = Number(match[1]);
|
||||||
|
if (Number.isFinite(value) && value > 0) feedRate = value;
|
||||||
|
}
|
||||||
|
if (feedRate !== null) {
|
||||||
|
rates.push({ line: index + 1, feedRate });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return rates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestFeedRateAtOrBeforeLine(rates, sourceLine) {
|
||||||
|
let feedRate = null;
|
||||||
|
for (const entry of rates) {
|
||||||
|
if (entry.line <= sourceLine) feedRate = entry.feedRate;
|
||||||
|
}
|
||||||
|
return feedRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareLinuxCncProgramForRuntime(programText) {
|
||||||
|
const switchkinsEvents = [];
|
||||||
|
const runtimeLines = String(programText).split(/\r?\n/).map((line, index) => {
|
||||||
|
const lineNumber = index + 1;
|
||||||
|
const events = readSwitchkinsEventsFromLine(line, lineNumber);
|
||||||
|
if (events.length === 0) return line;
|
||||||
|
switchkinsEvents.push(...events);
|
||||||
|
return stripSwitchkinsMcodesFromLine(line, events);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
runtimeProgramText: runtimeLines.join("\n"),
|
||||||
|
switchkinsEvents,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSwitchkinsEventsFromLine(line, lineNumber) {
|
||||||
|
const codeOnly = stripComments(String(line));
|
||||||
|
const events = [];
|
||||||
|
for (const match of codeOnly.matchAll(/\bM\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||||
|
const value = Number(match[1]);
|
||||||
|
const mCode = Number.isInteger(value) ? value : null;
|
||||||
|
const switchkins = SWITCHKINS_M_CODES.get(mCode);
|
||||||
|
if (!switchkins) continue;
|
||||||
|
events.push({
|
||||||
|
line: lineNumber,
|
||||||
|
code: `M${mCode}`,
|
||||||
|
switchkinsType: switchkins.switchkinsType,
|
||||||
|
requestedKinsType: switchkins.requestedKinsType,
|
||||||
|
semanticBoundary: SWITCHKINS_REMAP_BOUNDARY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripSwitchkinsMcodesFromLine(line, events) {
|
||||||
|
const eventCodes = new Set(events.map((event) => event.code.slice(1)));
|
||||||
|
let nextLine = String(line).replace(/\bM\s*([0-9]+(?:\.[0-9]+)?)\b/gi, (token, value) => {
|
||||||
|
const numericValue = Number(value);
|
||||||
|
if (Number.isInteger(numericValue) && eventCodes.has(String(numericValue))) {
|
||||||
|
return " ";
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
});
|
||||||
|
const codeOnly = stripComments(nextLine).replace(/\bN\s*[0-9]+\b/gi, "").trim();
|
||||||
|
if (!codeOnly) {
|
||||||
|
nextLine = `(web runtime switchkins ${events.map((event) => event.code).join(" ")})`;
|
||||||
|
}
|
||||||
|
return nextLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripComments(line) {
|
||||||
|
return String(line)
|
||||||
|
.replace(/\([^)]*\)/g, " ")
|
||||||
|
.replace(/;.*$/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchkinsEventsByLine(events) {
|
||||||
|
return [...events]
|
||||||
|
.filter((event) => Number.isFinite(event.line))
|
||||||
|
.sort((left, right) => left.line - right.line);
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestSwitchkinsEventAtOrBeforeLine(events, lineNumber) {
|
||||||
|
let latest = null;
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.line > lineNumber) break;
|
||||||
|
latest = event;
|
||||||
|
}
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prependRuntimeEvents(resultText, switchkinsEvents) {
|
||||||
|
if (switchkinsEvents.length === 0) return resultText;
|
||||||
|
const eventLines = switchkinsEvents.map((event) => (
|
||||||
|
`web_runtime_event=SWITCHKINS line=${event.line} code=${event.code} switchkins_type=${event.switchkinsType} requested_kins=${event.requestedKinsType}`
|
||||||
|
));
|
||||||
|
return `${eventLines.join("\n")}\n${resultText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function programLineMap(programText) {
|
||||||
|
const lines = new Map();
|
||||||
|
programText.split(/\r?\n/).forEach((line, index) => {
|
||||||
|
lines.set(index + 1, line.trim() || "(blank)");
|
||||||
|
});
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCanonicalNumber(line, field) {
|
||||||
|
const match = String(line).match(new RegExp(`\\b${field}=([-+0-9.eE]+)`));
|
||||||
|
return match ? Number(match[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDefaultModuleOptions({ wasmRoot }) {
|
||||||
|
const quietOptions = { print() {}, printErr() {} };
|
||||||
|
if (!isNodeRuntime()) return quietOptions;
|
||||||
|
|
||||||
|
const [{ readFileSync }, { dirname, resolve }, { fileURLToPath }] = await Promise.all([
|
||||||
|
import("node:fs"),
|
||||||
|
import("node:path"),
|
||||||
|
import("node:url"),
|
||||||
|
]);
|
||||||
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const resolvedWasmRoot = wasmRoot || resolve(moduleDir, "../../../../wasm-port/build/wasm/core");
|
||||||
|
return {
|
||||||
|
...quietOptions,
|
||||||
|
wasmBinary: readFileSync(resolve(resolvedWasmRoot, "linuxcnc_interp.wasm")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDefaultTpModuleOptions({ wasmRoot }) {
|
||||||
|
const quietOptions = { print() {}, printErr() {} };
|
||||||
|
if (!isNodeRuntime()) return quietOptions;
|
||||||
|
|
||||||
|
const [{ readFileSync }, { dirname, resolve }, { fileURLToPath }] = await Promise.all([
|
||||||
|
import("node:fs"),
|
||||||
|
import("node:path"),
|
||||||
|
import("node:url"),
|
||||||
|
]);
|
||||||
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const resolvedWasmRoot = wasmRoot || resolve(moduleDir, "../../../../wasm-port/build/wasm/tp");
|
||||||
|
return {
|
||||||
|
...quietOptions,
|
||||||
|
wasmBinary: readFileSync(resolve(resolvedWasmRoot, "linuxcnc_tp.wasm")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodeRuntime() {
|
||||||
|
return typeof process === "object"
|
||||||
|
&& typeof process.versions === "object"
|
||||||
|
&& typeof process.versions.node === "string"
|
||||||
|
&& process.type !== "renderer";
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
const SOURCE_MODE = "linuxcnc-interpreter-wasm";
|
||||||
|
const SEMANTIC_BOUNDARY = "linuxcnc_interpreter_wasm_canonical_events";
|
||||||
|
|
||||||
|
export async function createLinuxCncInterpreterWorkerRuntime({
|
||||||
|
sdkModuleUrl,
|
||||||
|
workerUrl = new URL("./linuxcnc-interpreter-worker.js", import.meta.url).href,
|
||||||
|
} = {}) {
|
||||||
|
if (typeof Worker !== "function") {
|
||||||
|
throw new Error("Web Worker is not available in this runtime");
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = new Worker(workerUrl, { type: "module" });
|
||||||
|
const request = createWorkerRequest(worker);
|
||||||
|
const readiness = await request("init", { sdkModuleUrl });
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-interpreter-worker-runtime",
|
||||||
|
loaded: true,
|
||||||
|
sourceMode: SOURCE_MODE,
|
||||||
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
|
executionContext: "worker",
|
||||||
|
workerUrl,
|
||||||
|
|
||||||
|
readiness() {
|
||||||
|
return {
|
||||||
|
...readiness,
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-interpreter-worker-runtime-readiness",
|
||||||
|
executionContext: "worker",
|
||||||
|
workerUrl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
runProgram(programText) {
|
||||||
|
return request("runProgram", { programText });
|
||||||
|
},
|
||||||
|
|
||||||
|
runMachineFileProgram(options = {}) {
|
||||||
|
return request("runMachineFileProgram", options);
|
||||||
|
},
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
worker.terminate();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWorkerRequest(worker) {
|
||||||
|
let nextId = 1;
|
||||||
|
const pending = new Map();
|
||||||
|
|
||||||
|
worker.addEventListener("message", (event) => {
|
||||||
|
const { id, ok, value, error } = event.data || {};
|
||||||
|
const request = pending.get(id);
|
||||||
|
if (!request) return;
|
||||||
|
pending.delete(id);
|
||||||
|
if (ok) {
|
||||||
|
request.resolve(value);
|
||||||
|
} else {
|
||||||
|
request.reject(new Error(error || "LinuxCNC interpreter worker request failed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.addEventListener("error", (event) => {
|
||||||
|
const error = new Error(event.message || "LinuxCNC interpreter worker error");
|
||||||
|
for (const request of pending.values()) {
|
||||||
|
request.reject(error);
|
||||||
|
}
|
||||||
|
pending.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
return function request(type, payload = {}) {
|
||||||
|
const id = nextId++;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
worker.postMessage({ id, type, payload });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { createLinuxCncInterpreterRuntime } from "./linuxcnc-interpreter-runtime.js";
|
||||||
|
|
||||||
|
let runtime = null;
|
||||||
|
|
||||||
|
self.addEventListener("message", async (event) => {
|
||||||
|
const { id, type, payload = {} } = event.data || {};
|
||||||
|
try {
|
||||||
|
if (type === "init") {
|
||||||
|
runtime = await createLinuxCncInterpreterRuntime({
|
||||||
|
moduleOptions: payload.moduleOptions,
|
||||||
|
wasmRoot: payload.wasmRoot,
|
||||||
|
sdkModuleUrl: payload.sdkModuleUrl,
|
||||||
|
});
|
||||||
|
postSuccess(id, runtime.readiness());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runtime?.loaded) {
|
||||||
|
throw new Error("LinuxCNC interpreter worker runtime is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "readiness") {
|
||||||
|
postSuccess(id, runtime.readiness());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "runProgram") {
|
||||||
|
postSuccess(id, runtime.runProgram(payload.programText || ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "runMachineFileProgram") {
|
||||||
|
postSuccess(id, runtime.runMachineFileProgram(payload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`unknown LinuxCNC interpreter worker request: ${type}`);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
id,
|
||||||
|
ok: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function postSuccess(id, value) {
|
||||||
|
self.postMessage({ id, ok: true, value });
|
||||||
|
}
|
||||||
@@ -1,39 +1,31 @@
|
|||||||
import { readFileSync } from "node:fs";
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
import {
|
|
||||||
createLinuxCncKinematicsSdk,
|
|
||||||
linuxCncKinematicsWasmFile,
|
|
||||||
supportedLinuxCncKinematicsModules,
|
|
||||||
} from "../../../../wasm-port/runtime/sdk/src/index.js";
|
|
||||||
|
|
||||||
const DEFAULT_MODULE_ID = "xyzac-trt";
|
const DEFAULT_MODULE_ID = "xyzac-trt";
|
||||||
const DEFAULT_JOINT_COUNT = 5;
|
const DEFAULT_JOINT_COUNT = 5;
|
||||||
const SOURCE_MODE = "source-derived-kinematics-wasm";
|
const SOURCE_MODE = "source-derived-kinematics-wasm";
|
||||||
const SEMANTIC_BOUNDARY = "linuxcnc_kinematics_wasm_c_abi";
|
const SEMANTIC_BOUNDARY = "linuxcnc_kinematics_wasm_c_abi";
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const DEFAULT_SDK_MODULE_URL = "../../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js";
|
||||||
const defaultWasmRoot = resolve(__dirname, "../../../../wasm-port/build/wasm/kinematics");
|
|
||||||
|
|
||||||
export async function createLinuxCncKinematicsRuntime({
|
export async function createLinuxCncKinematicsRuntime({
|
||||||
moduleId = DEFAULT_MODULE_ID,
|
moduleId = DEFAULT_MODULE_ID,
|
||||||
moduleOptions = null,
|
moduleOptions = null,
|
||||||
switchkinsType = 0,
|
switchkinsType = 0,
|
||||||
jointCount = DEFAULT_JOINT_COUNT,
|
jointCount = DEFAULT_JOINT_COUNT,
|
||||||
wasmRoot = defaultWasmRoot,
|
wasmRoot = null,
|
||||||
|
sdkModuleUrl = DEFAULT_SDK_MODULE_URL,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
const {
|
||||||
|
createLinuxCncKinematicsSdk,
|
||||||
|
linuxCncKinematicsWasmFile,
|
||||||
|
supportedLinuxCncKinematicsModules,
|
||||||
|
} = await import(sdkModuleUrl);
|
||||||
const wasmFile = linuxCncKinematicsWasmFile(moduleId);
|
const wasmFile = linuxCncKinematicsWasmFile(moduleId);
|
||||||
if (!wasmFile) {
|
if (!wasmFile) {
|
||||||
throw new Error(`unsupported LinuxCNC kinematics module: ${moduleId}`);
|
throw new Error(`unsupported LinuxCNC kinematics module: ${moduleId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedModuleOptions = moduleOptions || {
|
const resolvedModuleOptions = moduleOptions || await createDefaultModuleOptions({ wasmRoot, wasmFile });
|
||||||
wasmBinary: readFileSync(resolve(wasmRoot, wasmFile)),
|
|
||||||
print() {},
|
|
||||||
printErr() {},
|
|
||||||
};
|
|
||||||
const sdk = await createLinuxCncKinematicsSdk({ moduleId, moduleOptions: resolvedModuleOptions });
|
const sdk = await createLinuxCncKinematicsSdk({ moduleId, moduleOptions: resolvedModuleOptions });
|
||||||
const switchRc = typeof sdk.switchKinematics === "function"
|
let activeSwitchkinsType = switchkinsType;
|
||||||
|
let activeSwitchRc = typeof sdk.switchKinematics === "function"
|
||||||
? sdk.switchKinematics(switchkinsType)
|
? sdk.switchKinematics(switchkinsType)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
@@ -45,8 +37,13 @@ export async function createLinuxCncKinematicsRuntime({
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
sourceMode: SOURCE_MODE,
|
sourceMode: SOURCE_MODE,
|
||||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
switchkinsType,
|
executionContext: "direct",
|
||||||
switchRc,
|
get switchkinsType() {
|
||||||
|
return activeSwitchkinsType;
|
||||||
|
},
|
||||||
|
get switchRc() {
|
||||||
|
return activeSwitchRc;
|
||||||
|
},
|
||||||
jointCount,
|
jointCount,
|
||||||
sdk,
|
sdk,
|
||||||
|
|
||||||
@@ -59,11 +56,21 @@ export async function createLinuxCncKinematicsRuntime({
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
sourceMode: SOURCE_MODE,
|
sourceMode: SOURCE_MODE,
|
||||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
switchkinsType,
|
executionContext: "direct",
|
||||||
switchRc,
|
switchkinsType: activeSwitchkinsType,
|
||||||
|
switchRc: activeSwitchRc,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
switchKinematics(nextSwitchkinsType) {
|
||||||
|
const requestedType = Number(nextSwitchkinsType) || 0;
|
||||||
|
activeSwitchRc = typeof sdk.switchKinematics === "function"
|
||||||
|
? sdk.switchKinematics(requestedType)
|
||||||
|
: 0;
|
||||||
|
activeSwitchkinsType = requestedType;
|
||||||
|
return activeSwitchRc;
|
||||||
|
},
|
||||||
|
|
||||||
forward(joints, options = {}) {
|
forward(joints, options = {}) {
|
||||||
return sdk.forward(joints, options);
|
return sdk.forward(joints, options);
|
||||||
},
|
},
|
||||||
@@ -82,7 +89,7 @@ export async function createLinuxCncKinematicsRuntime({
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
moduleId,
|
moduleId,
|
||||||
switchkinsType,
|
switchkinsType: activeSwitchkinsType,
|
||||||
forward,
|
forward,
|
||||||
inverse,
|
inverse,
|
||||||
};
|
};
|
||||||
@@ -90,6 +97,35 @@ export async function createLinuxCncKinematicsRuntime({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createDefaultModuleOptions({ wasmRoot, wasmFile }) {
|
||||||
|
const quietOptions = {
|
||||||
|
print() {},
|
||||||
|
printErr() {},
|
||||||
|
};
|
||||||
|
if (!isNodeRuntime()) {
|
||||||
|
return quietOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [{ readFileSync }, { dirname, resolve }, { fileURLToPath }] = await Promise.all([
|
||||||
|
import("node:fs"),
|
||||||
|
import("node:path"),
|
||||||
|
import("node:url"),
|
||||||
|
]);
|
||||||
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const resolvedWasmRoot = wasmRoot || resolve(moduleDir, "../../../../wasm-port/build/wasm/kinematics");
|
||||||
|
return {
|
||||||
|
...quietOptions,
|
||||||
|
wasmBinary: readFileSync(resolve(resolvedWasmRoot, wasmFile)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodeRuntime() {
|
||||||
|
return typeof process === "object"
|
||||||
|
&& typeof process.versions === "object"
|
||||||
|
&& typeof process.versions.node === "string"
|
||||||
|
&& process.type !== "renderer";
|
||||||
|
}
|
||||||
|
|
||||||
export function createLinuxCncKinematicsRuntimeDescriptor(runtime) {
|
export function createLinuxCncKinematicsRuntimeDescriptor(runtime) {
|
||||||
if (!runtime?.loaded) return null;
|
if (!runtime?.loaded) return null;
|
||||||
return {
|
return {
|
||||||
@@ -100,6 +136,8 @@ export function createLinuxCncKinematicsRuntimeDescriptor(runtime) {
|
|||||||
loaded: runtime.loaded,
|
loaded: runtime.loaded,
|
||||||
sourceMode: runtime.sourceMode,
|
sourceMode: runtime.sourceMode,
|
||||||
semanticBoundary: runtime.semanticBoundary,
|
semanticBoundary: runtime.semanticBoundary,
|
||||||
|
executionContext: runtime.executionContext || "direct",
|
||||||
|
workerUrl: runtime.workerUrl || null,
|
||||||
switchkinsType: runtime.switchkinsType,
|
switchkinsType: runtime.switchkinsType,
|
||||||
switchRc: runtime.switchRc,
|
switchRc: runtime.switchRc,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
const DEFAULT_MODULE_ID = "xyzac-trt";
|
||||||
|
const DEFAULT_JOINT_COUNT = 5;
|
||||||
|
const SOURCE_MODE = "source-derived-kinematics-wasm";
|
||||||
|
const SEMANTIC_BOUNDARY = "linuxcnc_kinematics_wasm_c_abi";
|
||||||
|
|
||||||
|
export async function createLinuxCncKinematicsWorkerRuntime({
|
||||||
|
moduleId = DEFAULT_MODULE_ID,
|
||||||
|
switchkinsType = 0,
|
||||||
|
jointCount = DEFAULT_JOINT_COUNT,
|
||||||
|
sdkModuleUrl,
|
||||||
|
workerUrl = new URL("./linuxcnc-kinematics-worker.js", import.meta.url).href,
|
||||||
|
} = {}) {
|
||||||
|
if (typeof Worker !== "function") {
|
||||||
|
throw new Error("Web Worker is not available in this runtime");
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = new Worker(workerUrl, { type: "module" });
|
||||||
|
const request = createWorkerRequest(worker);
|
||||||
|
const readiness = await request("init", {
|
||||||
|
moduleId,
|
||||||
|
switchkinsType,
|
||||||
|
jointCount,
|
||||||
|
sdkModuleUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-kinematics-worker-runtime",
|
||||||
|
moduleId,
|
||||||
|
wasmFile: readiness.wasmFile,
|
||||||
|
supportedModules: readiness.supportedModules,
|
||||||
|
loaded: true,
|
||||||
|
sourceMode: SOURCE_MODE,
|
||||||
|
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||||
|
executionContext: "worker",
|
||||||
|
workerUrl,
|
||||||
|
switchkinsType,
|
||||||
|
switchRc: readiness.switchRc,
|
||||||
|
jointCount,
|
||||||
|
|
||||||
|
readiness() {
|
||||||
|
return {
|
||||||
|
...readiness,
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-kinematics-worker-runtime-readiness",
|
||||||
|
executionContext: "worker",
|
||||||
|
workerUrl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
forward(joints, options = {}) {
|
||||||
|
return request("forward", { joints: Array.from(joints, Number), options });
|
||||||
|
},
|
||||||
|
|
||||||
|
inverse(pose, count = jointCount, options = {}) {
|
||||||
|
return request("inverse", { pose, count, options });
|
||||||
|
},
|
||||||
|
|
||||||
|
async switchKinematics(nextSwitchkinsType) {
|
||||||
|
const result = await request("switchKinematics", { switchkinsType: nextSwitchkinsType });
|
||||||
|
runtime.switchkinsType = result.switchkinsType;
|
||||||
|
runtime.switchRc = result.switchRc;
|
||||||
|
return result.switchRc;
|
||||||
|
},
|
||||||
|
|
||||||
|
frameForJoints(joints, options = {}) {
|
||||||
|
return request("frameForJoints", { joints: Array.from(joints, Number), options });
|
||||||
|
},
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
worker.terminate();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWorkerRequest(worker) {
|
||||||
|
let nextId = 1;
|
||||||
|
const pending = new Map();
|
||||||
|
|
||||||
|
worker.addEventListener("message", (event) => {
|
||||||
|
const { id, ok, value, error } = event.data || {};
|
||||||
|
const request = pending.get(id);
|
||||||
|
if (!request) return;
|
||||||
|
pending.delete(id);
|
||||||
|
if (ok) {
|
||||||
|
request.resolve(value);
|
||||||
|
} else {
|
||||||
|
request.reject(new Error(error || "LinuxCNC kinematics worker request failed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.addEventListener("error", (event) => {
|
||||||
|
const error = new Error(event.message || "LinuxCNC kinematics worker error");
|
||||||
|
for (const request of pending.values()) {
|
||||||
|
request.reject(error);
|
||||||
|
}
|
||||||
|
pending.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
return function request(type, payload = {}) {
|
||||||
|
const id = nextId++;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
worker.postMessage({ id, type, payload });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { createLinuxCncKinematicsRuntime } from "./linuxcnc-kinematics-runtime.js";
|
||||||
|
|
||||||
|
let runtime = null;
|
||||||
|
|
||||||
|
self.addEventListener("message", async (event) => {
|
||||||
|
const { id, type, payload = {} } = event.data || {};
|
||||||
|
try {
|
||||||
|
if (type === "init") {
|
||||||
|
runtime = await createLinuxCncKinematicsRuntime({
|
||||||
|
moduleId: payload.moduleId,
|
||||||
|
moduleOptions: payload.moduleOptions,
|
||||||
|
switchkinsType: payload.switchkinsType,
|
||||||
|
jointCount: payload.jointCount,
|
||||||
|
wasmRoot: payload.wasmRoot,
|
||||||
|
sdkModuleUrl: payload.sdkModuleUrl,
|
||||||
|
});
|
||||||
|
postSuccess(id, runtime.readiness());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runtime?.loaded) {
|
||||||
|
throw new Error("LinuxCNC kinematics worker runtime is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "readiness") {
|
||||||
|
postSuccess(id, runtime.readiness());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "forward") {
|
||||||
|
postSuccess(id, runtime.forward(payload.joints, payload.options || {}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "inverse") {
|
||||||
|
postSuccess(id, runtime.inverse(payload.pose, payload.count, payload.options || {}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "switchKinematics") {
|
||||||
|
const switchRc = runtime.switchKinematics(payload.switchkinsType);
|
||||||
|
postSuccess(id, {
|
||||||
|
switchkinsType: runtime.switchkinsType,
|
||||||
|
switchRc,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "frameForJoints") {
|
||||||
|
postSuccess(id, runtime.frameForJoints(payload.joints, payload.options || {}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`unknown LinuxCNC kinematics worker request: ${type}`);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
id,
|
||||||
|
ok: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function postSuccess(id, value) {
|
||||||
|
self.postMessage({ id, ok: true, value });
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
const DEFAULT_SDK_MODULE_URL = "../../../../wasm-port/runtime/sdk/src/sim-config-staging.js";
|
||||||
|
const DEFAULT_MANIFEST_URLS = [
|
||||||
|
new URL("../../../../wasm-port/tools/source-manifest.txt", import.meta.url).href,
|
||||||
|
new URL("../../wasm-port/tools/source-manifest.txt", import.meta.url).href,
|
||||||
|
];
|
||||||
|
const DEFAULT_VENDOR_ROOT_URLS = [
|
||||||
|
new URL("../../../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
|
||||||
|
new URL("../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
|
||||||
|
];
|
||||||
|
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
|
||||||
|
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
|
||||||
|
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
|
||||||
|
|
||||||
|
export async function createMachineFileStagingPlan({
|
||||||
|
profile,
|
||||||
|
iniText = null,
|
||||||
|
manifestText = null,
|
||||||
|
sdkModuleUrl = DEFAULT_SDK_MODULE_URL,
|
||||||
|
manifestUrl = null,
|
||||||
|
wasmDir = null,
|
||||||
|
} = {}) {
|
||||||
|
if (!profile?.iniPath) {
|
||||||
|
throw new Error("machine file staging requires a profile with iniPath");
|
||||||
|
}
|
||||||
|
const { planSimConfigStaging } = await import(sdkModuleUrl);
|
||||||
|
const resolvedManifestText = manifestText ?? await readTextFromCandidateUrls(
|
||||||
|
manifestUrl ? [manifestUrl] : DEFAULT_MANIFEST_URLS,
|
||||||
|
);
|
||||||
|
const resolvedIniText = iniText ?? await readTextFromCandidateUrls(sourceUrlsFor(profile.iniPath));
|
||||||
|
const iniFile = basename(profile.iniPath);
|
||||||
|
const plan = planSimConfigStaging({
|
||||||
|
manifestText: resolvedManifestText,
|
||||||
|
machineRel: TRT_MACHINE_REL,
|
||||||
|
iniFile,
|
||||||
|
iniText: resolvedIniText,
|
||||||
|
wasmDir: wasmDir || `/work/sim/${TRT_MACHINE_REL}/${profile.id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = addVendoredDemoSources(plan.files, resolvedManifestText, plan.wasmDir);
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-machine-file-staging-plan",
|
||||||
|
profileId: profile.id,
|
||||||
|
machineRel: TRT_MACHINE_REL,
|
||||||
|
iniPath: profile.iniPath,
|
||||||
|
wasmDir: plan.wasmDir,
|
||||||
|
wasmIniPath: plan.iniPath,
|
||||||
|
wasmProgramPath: plan.programPath,
|
||||||
|
files: files.map((file) => ({
|
||||||
|
...file,
|
||||||
|
opfsPath: opfsPathFor(profile.id, file.sourceRel),
|
||||||
|
kind: classifySourceRel(file.sourceRel),
|
||||||
|
})),
|
||||||
|
summary: summarizePlan(files),
|
||||||
|
semanticBoundary: "linuxcnc_sim_config_file_staging_plan_only",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listLinuxCncGcodeSources(save) {
|
||||||
|
return [...(save?.files || [])]
|
||||||
|
.filter((file) => file.kind === "demo" && isLinuxCncFiveAxisGcodeSourceRel(file.sourceRel))
|
||||||
|
.map((file) => ({
|
||||||
|
sourceRel: file.sourceRel,
|
||||||
|
wasmPath: file.wasmPath,
|
||||||
|
opfsPath: file.opfsPath,
|
||||||
|
filename: basename(file.sourceRel),
|
||||||
|
bytes: file.bytes,
|
||||||
|
label: basename(file.sourceRel).replace(/\.ngc$/i, ""),
|
||||||
|
sourceMode: "linuxcnc-vendored-5axis-gcode",
|
||||||
|
semanticBoundary: "linuxcnc_vendored_5axis_gcode_source_file",
|
||||||
|
}))
|
||||||
|
.sort((left, right) => left.filename.localeCompare(right.filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectMachineFileProgram(plan, save, sourceRel) {
|
||||||
|
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) {
|
||||||
|
throw new Error(`5-axis G-code source must come from LinuxCNC source demos: ${sourceRel}`);
|
||||||
|
}
|
||||||
|
const selectedFile = (save?.files || []).find((file) => file.sourceRel === sourceRel);
|
||||||
|
if (!selectedFile) {
|
||||||
|
throw new Error(`LinuxCNC G-code source not staged: ${sourceRel}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...plan,
|
||||||
|
wasmProgramPath: selectedFile.wasmPath || selectedFile.path,
|
||||||
|
selectedProgramSourceRel: selectedFile.sourceRel,
|
||||||
|
selectedProgramFilename: basename(selectedFile.sourceRel),
|
||||||
|
selectedProgramBytes: selectedFile.bytes,
|
||||||
|
semanticBoundary: "linuxcnc_sim_config_file_staging_plan_with_selected_gcode_source",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||||
|
if (plan?.apiName !== "web-rtcp-5axis-machine-file-staging-plan") {
|
||||||
|
throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan");
|
||||||
|
}
|
||||||
|
const savedFiles = [];
|
||||||
|
for (const file of plan.files) {
|
||||||
|
const text = await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
|
||||||
|
await saveTextFile(file.opfsPath, text, options.storage);
|
||||||
|
savedFiles.push({
|
||||||
|
sourceRel: file.sourceRel,
|
||||||
|
opfsPath: file.opfsPath,
|
||||||
|
wasmPath: file.wasmPath,
|
||||||
|
path: file.wasmPath,
|
||||||
|
text,
|
||||||
|
kind: file.kind,
|
||||||
|
bytes: text.length,
|
||||||
|
executable: Boolean(file.executable),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiName: "web-rtcp-5axis-machine-file-staging-save",
|
||||||
|
profileId: plan.profileId,
|
||||||
|
status: "saved",
|
||||||
|
savedAt: new Date().toISOString(),
|
||||||
|
fileCount: savedFiles.length,
|
||||||
|
opfsRoot: `${OPFS_ROOT}/${plan.profileId}`,
|
||||||
|
files: savedFiles,
|
||||||
|
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
|
||||||
|
summary: summarizeSavedFiles(savedFiles),
|
||||||
|
semanticBoundary: "opfs_machine_file_text_staging_only",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stageProfileMachineFiles(profile, options = {}) {
|
||||||
|
const plan = await createMachineFileStagingPlan({
|
||||||
|
profile,
|
||||||
|
iniText: options.iniText,
|
||||||
|
manifestText: options.manifestText,
|
||||||
|
sdkModuleUrl: options.sdkModuleUrl,
|
||||||
|
manifestUrl: options.manifestUrl,
|
||||||
|
wasmDir: options.wasmDir,
|
||||||
|
});
|
||||||
|
const save = await saveMachineFileStagingPlan(plan, { storage: options.storage });
|
||||||
|
return { plan, save };
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizePlan(files) {
|
||||||
|
const kinds = countKinds(files.map((file) => classifySourceRel(file.sourceRel)));
|
||||||
|
return {
|
||||||
|
fileCount: files.length,
|
||||||
|
requiredFileCount: files.filter((file) => file.sourceRel.endsWith(".ini") || file.sourceRel.includes("/demos/")).length,
|
||||||
|
remapFileCount: kinds.remap || 0,
|
||||||
|
demoFileCount: kinds.demo || 0,
|
||||||
|
toolTableFileCount: kinds.toolTable || 0,
|
||||||
|
halFileCount: kinds.hal || 0,
|
||||||
|
kinds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVendoredDemoSources(files, manifestText, wasmDir) {
|
||||||
|
const bySourceRel = new Map(files.map((file) => [file.sourceRel, file]));
|
||||||
|
for (const sourceRel of String(manifestText).split(/\r?\n/)) {
|
||||||
|
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) continue;
|
||||||
|
if (bySourceRel.has(sourceRel)) continue;
|
||||||
|
bySourceRel.set(sourceRel, {
|
||||||
|
sourceRel,
|
||||||
|
wasmPath: `${wasmDir}/demos/${basename(sourceRel)}`,
|
||||||
|
executable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [...bySourceRel.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLinuxCncFiveAxisGcodeSourceRel(sourceRel) {
|
||||||
|
const value = String(sourceRel || "");
|
||||||
|
return value.startsWith(TRT_DEMO_SOURCE_PREFIX)
|
||||||
|
&& value.endsWith(".ngc")
|
||||||
|
&& !value.slice(TRT_DEMO_SOURCE_PREFIX.length).includes("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeSavedFiles(files) {
|
||||||
|
return {
|
||||||
|
fileCount: files.length,
|
||||||
|
totalBytes: files.reduce((total, file) => total + file.bytes, 0),
|
||||||
|
kinds: countKinds(files.map((file) => file.kind)),
|
||||||
|
opfsPaths: files.map((file) => file.opfsPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function countKinds(kinds) {
|
||||||
|
return kinds.reduce((counts, kind) => {
|
||||||
|
counts[kind] = (counts[kind] || 0) + 1;
|
||||||
|
return counts;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifySourceRel(sourceRel) {
|
||||||
|
if (sourceRel.endsWith(".ini")) return "ini";
|
||||||
|
if (sourceRel.endsWith(".tbl")) return "toolTable";
|
||||||
|
if (sourceRel.endsWith(".hal")) return "hal";
|
||||||
|
if (sourceRel.includes("/remap_subs/")) return "remap";
|
||||||
|
if (sourceRel.includes("/demos/")) return "demo";
|
||||||
|
if (sourceRel.endsWith(".xml")) return "pyvcp";
|
||||||
|
if (sourceRel.endsWith(".var")) return "parameters";
|
||||||
|
return "asset";
|
||||||
|
}
|
||||||
|
|
||||||
|
function opfsPathFor(profileId, sourceRel) {
|
||||||
|
return `${OPFS_ROOT}/${assertPathSegment(profileId)}/${String(sourceRel).replaceAll("\\", "/")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readTextFromCandidateUrls(urls) {
|
||||||
|
const errors = [];
|
||||||
|
for (const url of urls) {
|
||||||
|
try {
|
||||||
|
return await readTextFromUrl(url);
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${url}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`failed to read machine staging asset: ${errors.join(" | ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readTextFromUrl(url) {
|
||||||
|
if (isNodeRuntime()) {
|
||||||
|
const [{ readFile }, { resolve }, { fileURLToPath }] = await Promise.all([
|
||||||
|
import("node:fs/promises"),
|
||||||
|
import("node:path"),
|
||||||
|
import("node:url"),
|
||||||
|
]);
|
||||||
|
const path = String(url).startsWith("file:")
|
||||||
|
? fileURLToPath(url)
|
||||||
|
: resolve(process.cwd(), url);
|
||||||
|
return readFile(path, "utf8");
|
||||||
|
}
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`failed to fetch machine staging asset ${url}: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTextFile(path, text, storage = globalThis.navigator?.storage) {
|
||||||
|
const root = await getStorageRoot(storage);
|
||||||
|
const dir = await ensureParentDir(root, path);
|
||||||
|
const filename = splitPath(path).at(-1);
|
||||||
|
const fileHandle = await dir.getFileHandle(filename, { create: true });
|
||||||
|
const writable = await fileHandle.createWritable();
|
||||||
|
await writable.write(text);
|
||||||
|
await writable.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStorageRoot(storage) {
|
||||||
|
if (!storage?.getDirectory) {
|
||||||
|
throw new Error("OPFS is not available in this browser.");
|
||||||
|
}
|
||||||
|
return storage.getDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureParentDir(root, path) {
|
||||||
|
let current = root;
|
||||||
|
for (const part of splitPath(path).slice(0, -1)) {
|
||||||
|
current = await current.getDirectoryHandle(part, { create: true });
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitPath(path) {
|
||||||
|
const parts = String(path || "").replaceAll("\\", "/").split("/").filter(Boolean);
|
||||||
|
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
|
||||||
|
throw new Error(`Invalid OPFS path: ${path}`);
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceUrlsFor(sourceRel) {
|
||||||
|
return DEFAULT_VENDOR_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href);
|
||||||
|
}
|
||||||
|
|
||||||
|
function basename(path) {
|
||||||
|
return String(path).split("/").filter(Boolean).at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPathSegment(segment) {
|
||||||
|
if (!/^[a-z0-9._-]+$/i.test(String(segment))) {
|
||||||
|
throw new Error(`Invalid OPFS path segment: ${segment}`);
|
||||||
|
}
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodeRuntime() {
|
||||||
|
return typeof process === "object"
|
||||||
|
&& typeof process.versions === "object"
|
||||||
|
&& typeof process.versions.node === "string"
|
||||||
|
&& process.type !== "renderer";
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ export function buildRtcpFrame({
|
|||||||
|
|
||||||
const pose = normalizeAxisPose(axisPose);
|
const pose = normalizeAxisPose(axisPose);
|
||||||
const toolLength = 84.019;
|
const toolLength = 84.019;
|
||||||
const toolAxisVector = computeToolAxisVector(pose.a, pose.c);
|
const toolAxisVector = computeToolAxisVector(pose, profile);
|
||||||
const compensation = rtcpEnabled
|
const compensation = rtcpEnabled
|
||||||
? {
|
? {
|
||||||
x: -toolAxisVector.x * toolLength,
|
x: -toolAxisVector.x * toolLength,
|
||||||
@@ -85,7 +85,7 @@ function buildLinuxCncKinematicsFrame({
|
|||||||
...wasmPose,
|
...wasmPose,
|
||||||
});
|
});
|
||||||
const jointValues = Array.isArray(inverse.joints) ? inverse.joints : [];
|
const jointValues = Array.isArray(inverse.joints) ? inverse.joints : [];
|
||||||
const toolAxisVector = computeToolAxisVector(pose.a, pose.c);
|
const toolAxisVector = computeToolAxisVector(pose, profile);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiName: "web-rtcp-5axis-motion-frame",
|
apiName: "web-rtcp-5axis-motion-frame",
|
||||||
@@ -97,7 +97,7 @@ function buildLinuxCncKinematicsFrame({
|
|||||||
rtcpEnabled,
|
rtcpEnabled,
|
||||||
rtcpState: rtcpEnabled ? "on" : "off",
|
rtcpState: rtcpEnabled ? "on" : "off",
|
||||||
axisPose: pose,
|
axisPose: pose,
|
||||||
jointPose: buildJointPoseFromLinuxCncJoints(jointValues, pose),
|
jointPose: buildJointPoseFromLinuxCncJoints(jointValues, pose, profile),
|
||||||
tcpPose: {
|
tcpPose: {
|
||||||
x: pose.x,
|
x: pose.x,
|
||||||
y: pose.y,
|
y: pose.y,
|
||||||
@@ -150,9 +150,9 @@ function buildJointPose(pose) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildJointPoseFromLinuxCncJoints(joints, fallbackPose) {
|
function buildJointPoseFromLinuxCncJoints(joints, fallbackPose, profile = xyzacTrtProfile) {
|
||||||
const axes = ["X", "Y", "Z", "A", "C"];
|
const axes = profile?.traj?.coordinates === "XYZBC" ? ["X", "Y", "Z", "B", "C"] : ["X", "Y", "Z", "A", "C"];
|
||||||
const fallbackValues = [fallbackPose.x, fallbackPose.y, fallbackPose.z, fallbackPose.a, fallbackPose.c];
|
const fallbackValues = axes.map((axis) => fallbackPose[axis.toLowerCase()] ?? 0);
|
||||||
return axes.map((axis, joint) => ({
|
return axes.map((axis, joint) => ({
|
||||||
joint,
|
joint,
|
||||||
axis,
|
axis,
|
||||||
@@ -171,18 +171,29 @@ function normalizeLinuxCncPose(pose = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeToolAxisVector(aDegrees, cDegrees) {
|
function computeToolAxisVector(pose, profile = xyzacTrtProfile) {
|
||||||
const a = aDegrees * DEG_TO_RAD;
|
const coordinates = profile?.traj?.coordinates || "XYZAC";
|
||||||
|
const tiltDegrees = coordinates.includes("B") ? pose.b : pose.a;
|
||||||
|
const cDegrees = pose.c;
|
||||||
|
const tilt = tiltDegrees * DEG_TO_RAD;
|
||||||
const c = cDegrees * DEG_TO_RAD;
|
const c = cDegrees * DEG_TO_RAD;
|
||||||
const sinA = Math.sin(a);
|
const sinTilt = Math.sin(tilt);
|
||||||
const cosA = Math.cos(a);
|
const cosTilt = Math.cos(tilt);
|
||||||
const sinC = Math.sin(c);
|
const sinC = Math.sin(c);
|
||||||
const cosC = Math.cos(c);
|
const cosC = Math.cos(c);
|
||||||
|
|
||||||
|
if (coordinates.includes("B")) {
|
||||||
|
return normalizeVector({
|
||||||
|
x: sinTilt * cosC,
|
||||||
|
y: sinTilt * sinC,
|
||||||
|
z: cosTilt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return normalizeVector({
|
return normalizeVector({
|
||||||
x: sinA * sinC,
|
x: sinTilt * sinC,
|
||||||
y: -sinA * cosC,
|
y: -sinTilt * cosC,
|
||||||
z: cosA,
|
z: cosTilt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
161
web-rtcp-5axis-sim-plan/app/src/state/linuxcnc-task-policy.js
Normal file
161
web-rtcp-5axis-sim-plan/app/src/state/linuxcnc-task-policy.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
export const LINUXCNC_TASK_POLICY = {
|
||||||
|
apiName: "web-rtcp-5axis-linuxcnc-task-policy",
|
||||||
|
sourceMode: "linuxcnc-task-source-referenced-policy",
|
||||||
|
semanticBoundary: "linuxcnc_task_state_mode_command_gate",
|
||||||
|
sourceReferences: [
|
||||||
|
{
|
||||||
|
path: "linuxcnc/src/emc/nml_intf/emc.hh",
|
||||||
|
symbols: ["EMC_TASK_MODE", "EMC_TASK_STATE", "EMC_TASK_INTERP"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "linuxcnc/src/emc/task/emctaskmain.cc",
|
||||||
|
symbols: [
|
||||||
|
"emcTaskPlan",
|
||||||
|
"EMC_TASK_PLAN_RUN",
|
||||||
|
"EMC_TASK_PLAN_EXECUTE",
|
||||||
|
"EMC_TASK_PLAN_PAUSE",
|
||||||
|
"EMC_TASK_PLAN_RESUME",
|
||||||
|
"EMC_TASK_ABORT",
|
||||||
|
"EMC_JOG_INCR",
|
||||||
|
"EMC_JOINT_HOME",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "linuxcnc/src/emc/task/emctask.cc",
|
||||||
|
symbols: ["emcTaskSetState", "determineState"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LINUXCNC_TASK_MODES = new Set(["manual", "auto", "mdi"]);
|
||||||
|
export const LINUXCNC_TASK_STATES = new Set(["estop", "estop-reset", "off", "on"]);
|
||||||
|
export const LINUXCNC_INTERP_STATES = new Set(["idle", "reading", "paused", "waiting"]);
|
||||||
|
|
||||||
|
export function normalizeLinuxCncTaskMode(mode) {
|
||||||
|
if (mode === "jog") return "manual";
|
||||||
|
return LINUXCNC_TASK_MODES.has(mode) ? mode : "manual";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeLinuxCncTaskState(machine = {}) {
|
||||||
|
if (LINUXCNC_TASK_STATES.has(machine.taskState)) return machine.taskState;
|
||||||
|
if (machine.estopActive) return "estop";
|
||||||
|
if (machine.powerOn) return "on";
|
||||||
|
return "estop-reset";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeLinuxCncInterpState(machine = {}, runState = "idle") {
|
||||||
|
if (LINUXCNC_INTERP_STATES.has(machine.interpState)) return machine.interpState;
|
||||||
|
if (runState === "running") return "reading";
|
||||||
|
if (runState === "paused" || runState === "stepping") return "paused";
|
||||||
|
return "idle";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLinuxCncTaskPolicyStatus(state) {
|
||||||
|
const taskState = normalizeLinuxCncTaskState(state.machine);
|
||||||
|
const taskMode = normalizeLinuxCncTaskMode(state.machine.mode);
|
||||||
|
const interpState = normalizeLinuxCncInterpState(state.machine, state.runState);
|
||||||
|
const allHomed = Boolean(state.machine.allHomed);
|
||||||
|
const noForceHoming = Boolean(state.machine.noForceHoming);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...LINUXCNC_TASK_POLICY,
|
||||||
|
taskState,
|
||||||
|
taskMode,
|
||||||
|
interpState,
|
||||||
|
allHomed,
|
||||||
|
noForceHoming,
|
||||||
|
powerOn: taskState === "on",
|
||||||
|
estopActive: taskState === "estop",
|
||||||
|
canMove: taskState === "on",
|
||||||
|
canJog: taskState === "on" && taskMode === "manual",
|
||||||
|
canHome: taskState === "on" && taskMode === "manual",
|
||||||
|
canRunAuto: taskState === "on" && taskMode === "auto" && (allHomed || noForceHoming),
|
||||||
|
canExecuteMdi: taskState === "on" && taskMode === "mdi" && (allHomed || noForceHoming),
|
||||||
|
canPause: taskState === "on" && (taskMode === "auto" || taskMode === "mdi"),
|
||||||
|
canResume: taskState === "on" && (taskMode === "auto" || taskMode === "mdi") && interpState === "paused",
|
||||||
|
canAbort: true,
|
||||||
|
canLeaveAuto: taskMode !== "auto" || interpState === "idle",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gateLinuxCncTaskAction(state, action) {
|
||||||
|
const status = createLinuxCncTaskPolicyStatus(state);
|
||||||
|
const type = typeof action === "string" ? action : action?.type;
|
||||||
|
const requestedMode = typeof action === "object" ? action.mode : undefined;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "TOGGLE_POWER":
|
||||||
|
if (status.taskState === "estop") {
|
||||||
|
return block(status, "power on blocked: reset estop first");
|
||||||
|
}
|
||||||
|
return allow(status);
|
||||||
|
case "SET_MODE":
|
||||||
|
return gateMode(status, requestedMode);
|
||||||
|
case "JOG":
|
||||||
|
if (status.taskState !== "on") return block(status, "jog blocked: machine must be on");
|
||||||
|
if (status.taskMode !== "manual") return block(status, "jog blocked: switch to manual mode first");
|
||||||
|
return allow(status);
|
||||||
|
case "HOME":
|
||||||
|
if (status.taskState !== "on") return block(status, "home blocked: machine must be on");
|
||||||
|
if (status.taskMode !== "manual") return block(status, "home blocked: switch to manual mode first");
|
||||||
|
return allow(status);
|
||||||
|
case "RUN_MDI":
|
||||||
|
if (status.taskState !== "on") return block(status, "MDI blocked: machine must be on");
|
||||||
|
if (status.taskMode !== "mdi") return block(status, "MDI blocked: switch to MDI mode first");
|
||||||
|
if (!status.allHomed && !status.noForceHoming) return block(status, "MDI blocked: home machine first");
|
||||||
|
return allow(status);
|
||||||
|
case "RUN":
|
||||||
|
case "STEP":
|
||||||
|
case "RUN_FRAME":
|
||||||
|
if (status.taskState !== "on") return block(status, `${type.toLowerCase()} blocked: machine must be on`);
|
||||||
|
if (status.taskMode !== "auto") return block(status, `${type.toLowerCase()} blocked: switch to auto mode first`);
|
||||||
|
if (!status.allHomed && !status.noForceHoming) return block(status, `${type.toLowerCase()} blocked: home machine first`);
|
||||||
|
if (type === "RUN" && status.interpState === "paused") return block(status, "run blocked: resume paused program first");
|
||||||
|
return allow(status);
|
||||||
|
case "PAUSE":
|
||||||
|
if (status.taskState !== "on") return block(status, "pause blocked: machine must be on");
|
||||||
|
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
|
||||||
|
return block(status, "pause blocked: task mode must be auto or MDI");
|
||||||
|
}
|
||||||
|
return allow(status);
|
||||||
|
case "RESUME":
|
||||||
|
if (status.taskState !== "on") return block(status, "resume blocked: machine must be on");
|
||||||
|
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
|
||||||
|
return block(status, "resume blocked: task mode must be auto or MDI");
|
||||||
|
}
|
||||||
|
if (status.interpState !== "paused") return block(status, "resume blocked: interpreter is not paused");
|
||||||
|
return allow(status);
|
||||||
|
case "STOP":
|
||||||
|
case "ABORT":
|
||||||
|
return allow(status);
|
||||||
|
default:
|
||||||
|
return allow(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gateMode(status, requestedMode) {
|
||||||
|
const targetMode = normalizeLinuxCncTaskMode(requestedMode);
|
||||||
|
if (!LINUXCNC_TASK_MODES.has(targetMode)) {
|
||||||
|
return block(status, `mode blocked: invalid LinuxCNC task mode ${requestedMode}`);
|
||||||
|
}
|
||||||
|
if (status.taskMode === "auto" && status.interpState !== "idle" && targetMode !== "auto") {
|
||||||
|
return block(status, "mode blocked: AUTO interpreter is not idle");
|
||||||
|
}
|
||||||
|
return allow(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
function allow(status) {
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
status,
|
||||||
|
operatorMessage: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function block(status, operatorMessage) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
status,
|
||||||
|
operatorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,9 @@
|
|||||||
--green-dark: #02bf19;
|
--green-dark: #02bf19;
|
||||||
--black: #050505;
|
--black: #050505;
|
||||||
--text: #2e2e2e;
|
--text: #2e2e2e;
|
||||||
|
--ink: #242424;
|
||||||
|
--line: #aaa59b;
|
||||||
|
--muted: #5e5a52;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,11 +22,11 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1024px;
|
min-width: 1180px;
|
||||||
min-height: 768px;
|
min-height: 640px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
background: #c8c4bc;
|
background: #c8c4bc;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
@@ -40,26 +43,44 @@ button:active {
|
|||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-select-label {
|
||||||
|
display: grid;
|
||||||
|
gap: 1px;
|
||||||
|
min-width: 210px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-select-label select {
|
||||||
|
min-height: 28px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #f7f5ef;
|
||||||
|
color: var(--ink);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.gmoccapy-shell {
|
.gmoccapy-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
minmax(350px, 0.76fr)
|
minmax(610px, 1.34fr)
|
||||||
minmax(160px, 0.36fr)
|
minmax(270px, 0.58fr)
|
||||||
minmax(128px, 0.28fr)
|
minmax(260px, 0.56fr)
|
||||||
78px
|
108px;
|
||||||
minmax(176px, 0.4fr)
|
grid-template-rows: 40px minmax(210px, 1fr) minmax(170px, 0.78fr) 150px 68px;
|
||||||
104px;
|
|
||||||
grid-template-rows: 28px minmax(156px, 0.36fr) minmax(240px, 0.64fr) 224px 62px;
|
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"title title title title title title"
|
"title title title title"
|
||||||
"preview preview dro dro dro side"
|
"preview dro dro side"
|
||||||
"preview preview gcode gcode gcode side"
|
"preview gcode gcode side"
|
||||||
"info override override spindle spindle side"
|
"info override spindle side"
|
||||||
"bottom bottom bottom bottom bottom side";
|
"bottom bottom bottom side";
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
min-width: 1024px;
|
min-width: 1180px;
|
||||||
min-height: 768px;
|
min-height: 640px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
@@ -70,7 +91,7 @@ button:active {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 2px 10px;
|
padding: 4px 12px;
|
||||||
background: #cfcbc3;
|
background: #cfcbc3;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
@@ -78,8 +99,9 @@ button:active {
|
|||||||
.brand-dot {
|
.brand-dot {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 22px;
|
flex: 0 0 auto;
|
||||||
height: 22px;
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
border: 2px solid #ffcf00;
|
border: 2px solid #ffcf00;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: #e21d1d;
|
color: #e21d1d;
|
||||||
@@ -98,7 +120,7 @@ button:active {
|
|||||||
|
|
||||||
.title-stack strong {
|
.title-stack strong {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 14px;
|
font-size: 17px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -106,7 +128,7 @@ button:active {
|
|||||||
.title-stack span,
|
.title-stack span,
|
||||||
.run-state {
|
.run-state {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
color: #4d4d4d;
|
color: #4d4d4d;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -140,8 +162,9 @@ button:active {
|
|||||||
|
|
||||||
.machine-preview {
|
.machine-preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 54px);
|
height: calc(100% - 64px);
|
||||||
margin-top: 24px;
|
margin-top: 32px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-preview-card {
|
.tool-preview-card {
|
||||||
@@ -215,7 +238,7 @@ button:active {
|
|||||||
.rtcp-preview-badge {
|
.rtcp-preview-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
bottom: 58px;
|
bottom: 62px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -242,8 +265,8 @@ button:active {
|
|||||||
left: 0;
|
left: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
padding: 4px;
|
padding: 6px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
@@ -251,7 +274,7 @@ button:active {
|
|||||||
.preview-toolbar button,
|
.preview-toolbar button,
|
||||||
.bottom-controls button,
|
.bottom-controls button,
|
||||||
.status-sidebar button {
|
.status-sidebar button {
|
||||||
min-height: 46px;
|
min-height: 42px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +285,8 @@ button:active {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-bottom: 1px solid #151515;
|
border-left: 2px solid #8d887f;
|
||||||
|
border-bottom: 2px solid #8d887f;
|
||||||
background: var(--black);
|
background: var(--black);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,17 +301,17 @@ button:active {
|
|||||||
|
|
||||||
.dro-row {
|
.dro-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 24px 40px minmax(74px, 1fr) 48px;
|
grid-template-columns: 30px 46px minmax(92px, 1fr) 58px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 3px 5px;
|
padding: 4px 7px;
|
||||||
background: var(--black);
|
background: var(--black);
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dro-axis {
|
.dro-axis {
|
||||||
font-size: 22px;
|
font-size: clamp(24px, 2vw, 32px);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -295,14 +319,14 @@ button:active {
|
|||||||
.dro-mode,
|
.dro-mode,
|
||||||
.dro-dtg {
|
.dro-dtg {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dro-row strong {
|
.dro-row strong {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: clamp(20px, 2.4vw, 30px);
|
font-size: clamp(24px, 2.5vw, 38px);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
text-overflow: clip;
|
text-overflow: clip;
|
||||||
@@ -314,10 +338,10 @@ button:active {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 5px 8px;
|
padding: 6px 9px;
|
||||||
background: #080808;
|
background: #080808;
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,11 +367,11 @@ button:active {
|
|||||||
.gcode-panel {
|
.gcode-panel {
|
||||||
grid-area: gcode;
|
grid-area: gcode;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 30px minmax(0, 1fr) 22px;
|
grid-template-rows: 32px minmax(0, 1fr) 20px 54px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #f4f2ed;
|
background: #f6f4ef;
|
||||||
border-top: 2px solid var(--border);
|
border-top: 2px solid var(--border);
|
||||||
border-bottom: 2px solid var(--border);
|
border-bottom: 2px solid var(--border);
|
||||||
}
|
}
|
||||||
@@ -358,10 +382,10 @@ button:active {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 5px 8px;
|
padding: 6px 9px;
|
||||||
border-bottom: 1px solid #d5d0c7;
|
border-bottom: 1px solid #d5d0c7;
|
||||||
background: #eeeae3;
|
background: #eeeae3;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gcode-header strong,
|
.gcode-header strong,
|
||||||
@@ -380,27 +404,32 @@ button:active {
|
|||||||
.gcode-list {
|
.gcode-list {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 6px 2px;
|
padding: 6px 8px 3px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
font-family: "Courier New", monospace;
|
font-family: "Courier New", monospace;
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: #8b8b8b;
|
color: #55514a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gcode-row {
|
.gcode-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 42px minmax(0, 1fr);
|
grid-template-columns: 42px minmax(0, 1fr);
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-height: 20px;
|
min-height: 18px;
|
||||||
line-height: 1.35;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gcode-row.active {
|
.gcode-row.active {
|
||||||
background: #e8e8e8;
|
background: #242424;
|
||||||
color: #202020;
|
color: #202020;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gcode-row.active span,
|
||||||
|
.gcode-row.active code {
|
||||||
|
color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
.gcode-row code {
|
.gcode-row code {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -412,8 +441,8 @@ button:active {
|
|||||||
grid-template-columns: 120px 1fr;
|
grid-template-columns: 120px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 2px 8px 4px;
|
padding: 1px 8px 3px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,10 +457,70 @@ button:active {
|
|||||||
background: #3888df;
|
background: #3888df;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mdi-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 27px minmax(0, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 3px 8px 5px;
|
||||||
|
border-top: 1px solid #cbc6bd;
|
||||||
|
background: #e7e3dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdi-command-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 44px minmax(0, 1fr) 64px;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdi-command-row strong {
|
||||||
|
color: #222;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdi-command-row input {
|
||||||
|
min-width: 0;
|
||||||
|
height: 25px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 1px solid #8e897f;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #111;
|
||||||
|
color: var(--green);
|
||||||
|
font: 14px/1.1 "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdi-command-row button {
|
||||||
|
min-height: 25px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdi-history {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdi-history button {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 19px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #303030;
|
||||||
|
font: 11px/1.1 "Courier New", monospace;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.status-sidebar {
|
.status-sidebar {
|
||||||
grid-area: side;
|
grid-area: side;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: repeat(9, minmax(44px, 1fr)) minmax(46px, auto);
|
grid-template-rows: repeat(9, minmax(42px, 1fr)) minmax(42px, auto);
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-left: 2px solid var(--border);
|
border-left: 2px solid var(--border);
|
||||||
@@ -475,7 +564,7 @@ button:active {
|
|||||||
grid-area: info;
|
grid-area: info;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
border-top: 2px solid var(--border);
|
border-top: 2px solid var(--border);
|
||||||
border-right: 2px solid var(--border);
|
border-right: 2px solid var(--border);
|
||||||
background: #eeeae3;
|
background: #eeeae3;
|
||||||
@@ -492,9 +581,9 @@ button:active {
|
|||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: #e0ddd6;
|
background: #e0ddd6;
|
||||||
min-height: 37px;
|
min-height: 32px;
|
||||||
padding: 4px 5px;
|
padding: 4px 5px;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -507,10 +596,10 @@ button:active {
|
|||||||
.info-grid {
|
.info-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content 1fr;
|
grid-template-columns: max-content 1fr;
|
||||||
gap: 3px 10px;
|
gap: 1px 8px;
|
||||||
margin: 8px;
|
margin: 6px 8px;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
line-height: 1.22;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-grid dt {
|
.info-grid dt {
|
||||||
@@ -533,7 +622,7 @@ button:active {
|
|||||||
grid-area: override;
|
grid-area: override;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 56px 64px minmax(78px, 1fr);
|
grid-template-rows: repeat(3, minmax(0, 1fr));
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-top: 2px solid var(--border);
|
border-top: 2px solid var(--border);
|
||||||
@@ -549,8 +638,8 @@ button:active {
|
|||||||
.spindle {
|
.spindle {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
gap: 2px;
|
gap: 1px;
|
||||||
padding: 5px;
|
padding: 5px 6px;
|
||||||
background: #f8f5ef;
|
background: #f8f5ef;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -563,7 +652,7 @@ button:active {
|
|||||||
.cooling h2,
|
.cooling h2,
|
||||||
.spindle h2 {
|
.spindle h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -573,7 +662,7 @@ button:active {
|
|||||||
.spindle strong {
|
.spindle strong {
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: clamp(18px, 1.8vw, 25px);
|
font-size: clamp(16px, 1.45vw, 22px);
|
||||||
line-height: 1.05;
|
line-height: 1.05;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -591,20 +680,20 @@ button:active {
|
|||||||
|
|
||||||
.meter-card strong {
|
.meter-card strong {
|
||||||
display: inline;
|
display: inline;
|
||||||
font-size: clamp(20px, 2vw, 25px);
|
font-size: clamp(18px, 1.6vw, 24px);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meter-card span {
|
.meter-card span {
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stepper {
|
.stepper {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32px minmax(54px, 1fr) 32px;
|
grid-template-columns: 30px minmax(54px, 1fr) 30px;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
@@ -612,7 +701,7 @@ button:active {
|
|||||||
|
|
||||||
.stepper div {
|
.stepper div {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 6px 4px;
|
padding: 5px 4px;
|
||||||
background: var(--orange);
|
background: var(--orange);
|
||||||
border: 1px solid #b4651c;
|
border: 1px solid #b4651c;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -623,7 +712,7 @@ button:active {
|
|||||||
|
|
||||||
.stepper button {
|
.stepper button {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 28px;
|
min-height: 26px;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,8 +739,8 @@ button:active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spindle-range {
|
.spindle-range {
|
||||||
height: 24px;
|
height: 18px;
|
||||||
margin-top: 8px;
|
margin-top: 4px;
|
||||||
background: #bdbdbd;
|
background: #bdbdbd;
|
||||||
border: 1px solid #898989;
|
border: 1px solid #898989;
|
||||||
}
|
}
|
||||||
@@ -665,7 +754,7 @@ button:active {
|
|||||||
.bottom-controls {
|
.bottom-controls {
|
||||||
grid-area: bottom;
|
grid-area: bottom;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(13, minmax(48px, 1fr));
|
grid-template-columns: repeat(15, minmax(48px, 1fr));
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-top: 2px solid var(--border);
|
border-top: 2px solid var(--border);
|
||||||
@@ -674,8 +763,11 @@ button:active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bottom-controls button {
|
.bottom-controls button {
|
||||||
font-size: 12px;
|
font-size: clamp(10px, 0.72vw, 12px);
|
||||||
|
line-height: 1.05;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 3px 4px;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.program-file-input {
|
.program-file-input {
|
||||||
@@ -684,7 +776,11 @@ button:active {
|
|||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.gmoccapy-shell {
|
.gmoccapy-shell {
|
||||||
grid-template-columns: 350px 160px 128px 78px 176px 96px;
|
grid-template-columns:
|
||||||
|
minmax(560px, 1.28fr)
|
||||||
|
minmax(250px, 0.58fr)
|
||||||
|
minmax(230px, 0.54fr)
|
||||||
|
100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dro-row strong {
|
.dro-row strong {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export function mountGmoccapyShell(root, store) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
store.subscribe((state) => render(regions, state, store.dispatch));
|
store.subscribe((state) => render(regions, state, store.dispatch));
|
||||||
|
root.addEventListener("profile-change", (event) => {
|
||||||
|
store.dispatch({ type: "SET_PROFILE", profileId: event.detail.profileId });
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getRegions() {
|
getRegions() {
|
||||||
@@ -46,7 +49,7 @@ function render(regions, state, dispatch) {
|
|||||||
renderTitlebar(regions.titlebar, state);
|
renderTitlebar(regions.titlebar, state);
|
||||||
renderPreview(regions.preview, state, dispatch);
|
renderPreview(regions.preview, state, dispatch);
|
||||||
renderDro(regions.dro, state);
|
renderDro(regions.dro, state);
|
||||||
renderGcode(regions.gcode, state);
|
renderGcode(regions.gcode, state, dispatch);
|
||||||
renderSidebar(regions["status-sidebar"], state, dispatch);
|
renderSidebar(regions["status-sidebar"], state, dispatch);
|
||||||
renderInfoTabs(regions["info-tabs"], state);
|
renderInfoTabs(regions["info-tabs"], state);
|
||||||
renderOverride(regions.override, state, dispatch);
|
renderOverride(regions.override, state, dispatch);
|
||||||
@@ -61,8 +64,22 @@ function renderTitlebar(element, state) {
|
|||||||
<strong>gmoccapy Web 5 Axis for LinuxCNC RTCP Simulation</strong>
|
<strong>gmoccapy Web 5 Axis for LinuxCNC RTCP Simulation</strong>
|
||||||
<span>${state.machineProfile} | ${state.sessionName} | ${state.sourceMode} | ${state.machine.mode}</span>
|
<span>${state.machineProfile} | ${state.sessionName} | ${state.sourceMode} | ${state.machine.mode}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="profile-select-label">
|
||||||
|
Profile
|
||||||
|
<select data-action="select-profile">
|
||||||
|
${state.availableProfiles.map((profile) => `
|
||||||
|
<option value="${profile.id}" ${profile.id === state.machineProfile ? "selected" : ""}>${profile.coordinates} ${profile.id}</option>
|
||||||
|
`).join("")}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<div class="run-state" data-run-state="${state.runState}">${state.runState}</div>
|
<div class="run-state" data-run-state="${state.runState}">${state.runState}</div>
|
||||||
`;
|
`;
|
||||||
|
element.querySelector('[data-action="select-profile"]').addEventListener("change", (event) => {
|
||||||
|
element.dispatchEvent(new CustomEvent("profile-change", {
|
||||||
|
bubbles: true,
|
||||||
|
detail: { profileId: event.target.value },
|
||||||
|
}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPreview(element, state, dispatch) {
|
function renderPreview(element, state, dispatch) {
|
||||||
@@ -137,7 +154,7 @@ function droRow(axis, value, dtg) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGcode(element, state) {
|
function renderGcode(element, state, dispatch) {
|
||||||
const rows = state.programLines
|
const rows = state.programLines
|
||||||
.map((line, index) => {
|
.map((line, index) => {
|
||||||
const lineNumber = state.programStartLine + index;
|
const lineNumber = state.programStartLine + index;
|
||||||
@@ -158,12 +175,101 @@ function renderGcode(element, state) {
|
|||||||
<span data-program-source="${state.programSource}">${state.programSource}</span>
|
<span data-program-source="${state.programSource}">${state.programSource}</span>
|
||||||
<span data-active-program-line="${state.activeLine}">Current line ${state.activeLine}</span>
|
<span data-active-program-line="${state.activeLine}">Current line ${state.activeLine}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="linuxcnc-source-row" data-linuxcnc-gcode-source="row">
|
||||||
|
<label>
|
||||||
|
LinuxCNC 5-axis source
|
||||||
|
<select data-action="select-linuxcnc-gcode-source" ${state.machineFileStaging.gcodeSources?.length ? "" : "disabled"}>
|
||||||
|
${renderLinuxCncGcodeSourceOptions(state)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="button" data-action="stage-linuxcnc-sources">Stage</button>
|
||||||
|
<span data-linuxcnc-gcode-source="status">${formatLinuxCncGcodeSourceStatus(state)}</span>
|
||||||
|
</div>
|
||||||
<ol class="gcode-list" start="${state.programStartLine}">${rows}</ol>
|
<ol class="gcode-list" start="${state.programStartLine}">${rows}</ol>
|
||||||
<div class="gcode-progress">
|
<div class="gcode-progress">
|
||||||
<span>${state.activeLine} / ${programEndLine}</span>
|
<span>${state.activeLine} / ${programEndLine}</span>
|
||||||
<div><i style="width: ${progress}%"></i></div>
|
<div><i style="width: ${progress}%"></i></div>
|
||||||
</div>
|
</div>
|
||||||
|
<section class="mdi-panel" data-mdi-mode="${state.machine.mode === "mdi"}">
|
||||||
|
<form class="mdi-command-row" data-action="mdi-form">
|
||||||
|
<strong>MDI</strong>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-action="mdi-command"
|
||||||
|
value="${escapeHtml(state.machine.mdiCommand)}"
|
||||||
|
spellcheck="false"
|
||||||
|
autocomplete="off"
|
||||||
|
aria-label="MDI command"
|
||||||
|
/>
|
||||||
|
<button type="submit" data-action="mdi-submit">Run</button>
|
||||||
|
</form>
|
||||||
|
<div class="mdi-history">
|
||||||
|
${mdiQuickCommands(state).map((command) => `
|
||||||
|
<button type="button" data-action="mdi-history" data-command="${escapeHtml(command)}">${escapeHtml(command)}</button>
|
||||||
|
`).join("")}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const form = element.querySelector('[data-action="mdi-form"]');
|
||||||
|
const input = element.querySelector('[data-action="mdi-command"]');
|
||||||
|
form.addEventListener("submit", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch({ type: "RUN_MDI", command: input.value });
|
||||||
|
});
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
dispatch({ type: "SET_MDI_COMMAND", command: input.value });
|
||||||
|
});
|
||||||
|
for (const button of element.querySelectorAll('[data-action="mdi-history"]')) {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
dispatch({ type: "RUN_MDI", command: button.dataset.command });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
element.querySelector('[data-action="stage-linuxcnc-sources"]').addEventListener("click", () => {
|
||||||
|
dispatch({ type: "STAGE_MACHINE_FILES_REQUEST" });
|
||||||
|
});
|
||||||
|
const linuxCncSourceSelect = element.querySelector('[data-action="select-linuxcnc-gcode-source"]');
|
||||||
|
linuxCncSourceSelect.addEventListener("change", () => {
|
||||||
|
if (!linuxCncSourceSelect.value) return;
|
||||||
|
dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: linuxCncSourceSelect.value });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLinuxCncGcodeSourceOptions(state) {
|
||||||
|
const sources = state.machineFileStaging.gcodeSources || [];
|
||||||
|
if (sources.length === 0) {
|
||||||
|
return `<option value="">stage machine files first</option>`;
|
||||||
|
}
|
||||||
|
const selected = state.machineFileStaging.selectedGcodeSourceRel || "";
|
||||||
|
return [
|
||||||
|
`<option value="">select source</option>`,
|
||||||
|
...sources.map((source) => `
|
||||||
|
<option value="${escapeHtml(source.sourceRel)}" ${source.sourceRel === selected ? "selected" : ""}>
|
||||||
|
${escapeHtml(source.filename)}
|
||||||
|
</option>
|
||||||
|
`),
|
||||||
|
].join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLinuxCncGcodeSourceStatus(state) {
|
||||||
|
const sources = state.machineFileStaging.gcodeSources || [];
|
||||||
|
if (sources.length === 0) return `${state.machineFileStaging.status} / no staged LinuxCNC G-code sources`;
|
||||||
|
const selected = state.machineFileStaging.selectedGcodeSourceRel || "-";
|
||||||
|
return `${sources.length} staged / ${selected}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mdiQuickCommands(state) {
|
||||||
|
const profileCommands = state.profile.kinematicsParameters.switchkinsTypes
|
||||||
|
.map((type) => type.mdiCommand)
|
||||||
|
.filter(Boolean);
|
||||||
|
return [
|
||||||
|
...(state.mdiHistory || []),
|
||||||
|
state.machine.mdiCommand,
|
||||||
|
"G0 X0 Y0 Z0",
|
||||||
|
"G91 X1",
|
||||||
|
"G90",
|
||||||
|
...profileCommands,
|
||||||
|
].filter((command, index, commands) => command && commands.indexOf(command) === index).slice(0, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSidebar(element, state, dispatch) {
|
function renderSidebar(element, state, dispatch) {
|
||||||
@@ -173,10 +279,10 @@ function renderSidebar(element, state, dispatch) {
|
|||||||
<button type="button" class="sidebar-button" data-action="reset">RESET</button>
|
<button type="button" class="sidebar-button" data-action="reset">RESET</button>
|
||||||
<button type="button" class="sidebar-button" data-action="mode-auto" data-active="${state.machine.mode === "auto"}">AUTO</button>
|
<button type="button" class="sidebar-button" data-action="mode-auto" data-active="${state.machine.mode === "auto"}">AUTO</button>
|
||||||
<button type="button" class="sidebar-button" data-action="mode-manual" data-active="${state.machine.mode === "manual"}">MANUAL</button>
|
<button type="button" class="sidebar-button" data-action="mode-manual" data-active="${state.machine.mode === "manual"}">MANUAL</button>
|
||||||
<button type="button" class="sidebar-button" data-action="mode-jog" data-active="${state.machine.mode === "jog"}">JOG</button>
|
<button type="button" class="sidebar-button" data-action="mode-jog" data-active="${state.machine.mode === "manual"}">JOG</button>
|
||||||
<button type="button" class="sidebar-button" data-action="mode-mdi" data-active="${state.machine.mode === "mdi"}">MDI</button>
|
<button type="button" class="sidebar-button" data-action="mode-mdi" data-active="${state.machine.mode === "mdi"}">MDI</button>
|
||||||
<button type="button" class="sidebar-button" data-action="kins-identity" data-active="${state.kinsType === "identity"}">IDENTITY</button>
|
<button type="button" class="sidebar-button" data-action="kins-identity" data-active="${state.kinsType === "identity"}">IDENTITY</button>
|
||||||
<button type="button" class="sidebar-button" data-action="kins-tcp" data-active="${state.kinsType === "tcp-xyzac"}">TCP</button>
|
<button type="button" class="sidebar-button" data-action="kins-tcp" data-active="${state.kinsType.startsWith("tcp-")}">TCP</button>
|
||||||
<time>13:30:31<br />20.06.2026</time>
|
<time>13:30:31<br />20.06.2026</time>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -191,12 +297,14 @@ function renderSidebar(element, state, dispatch) {
|
|||||||
dispatch({ type: "SET_KINS_TYPE", kinsType: "identity" });
|
dispatch({ type: "SET_KINS_TYPE", kinsType: "identity" });
|
||||||
});
|
});
|
||||||
element.querySelector('[data-action="kins-tcp"]').addEventListener("click", () => {
|
element.querySelector('[data-action="kins-tcp"]').addEventListener("click", () => {
|
||||||
dispatch({ type: "SET_KINS_TYPE", kinsType: "tcp-xyzac" });
|
const tcpKinsType = state.profile.kinematicsParameters.switchkinsTypes.find((type) => type.value === 1)?.webKinsType || "tcp-xyzac";
|
||||||
|
dispatch({ type: "SET_KINS_TYPE", kinsType: tcpKinsType });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInfoTabs(element, state) {
|
function renderInfoTabs(element, state) {
|
||||||
const frame = state.rtcpFrame;
|
const frame = state.rtcpFrame;
|
||||||
|
const taskPolicy = state.linuxCncTaskPolicy;
|
||||||
element.innerHTML = `
|
element.innerHTML = `
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<button type="button" class="active">Tool info and G-codes</button>
|
<button type="button" class="active">Tool info and G-codes</button>
|
||||||
@@ -207,8 +315,21 @@ function renderInfoTabs(element, state) {
|
|||||||
<dt>Size:</dt><dd>${state.fileSizeBytes} bytes</dd>
|
<dt>Size:</dt><dd>${state.fileSizeBytes} bytes</dd>
|
||||||
<dt>Lines:</dt><dd>${state.lineCount} gcode lines</dd>
|
<dt>Lines:</dt><dd>${state.lineCount} gcode lines</dd>
|
||||||
<dt>Machine:</dt><dd data-machine-state="summary">${state.machine.powerOn ? "power on" : "power off"} / ${state.machine.estopActive ? "estop" : "clear"} / ${state.machine.mode}</dd>
|
<dt>Machine:</dt><dd data-machine-state="summary">${state.machine.powerOn ? "power on" : "power off"} / ${state.machine.estopActive ? "estop" : "clear"} / ${state.machine.mode}</dd>
|
||||||
|
<dt>Task policy:</dt><dd data-linuxcnc-task-policy="boundary">${taskPolicy.semanticBoundary}</dd>
|
||||||
|
<dt>Task state:</dt><dd data-linuxcnc-task-policy="state">${taskPolicy.taskState} / ${taskPolicy.taskMode} / ${taskPolicy.interpState}</dd>
|
||||||
|
<dt>Task gates:</dt><dd data-linuxcnc-task-policy="gates">${formatLinuxCncTaskGates(taskPolicy)}</dd>
|
||||||
|
<dt>Task source:</dt><dd data-linuxcnc-task-policy="source">${formatLinuxCncTaskSources(taskPolicy)}</dd>
|
||||||
<dt>Current line:</dt><dd data-program-current-line="${state.activeLine}">${state.activeLine}</dd>
|
<dt>Current line:</dt><dd data-program-current-line="${state.activeLine}">${state.activeLine}</dd>
|
||||||
|
<dt>Program source:</dt><dd data-program-execution-source="${state.programExecutionSourceMode}">${state.programExecutionSourceMode}</dd>
|
||||||
|
<dt>Canonical:</dt><dd data-program-execution-summary="${state.programExecution?.summary?.motionEventCount ?? 0}">${state.programExecution?.summary?.motionEventCount ?? 0} motion / ${state.programExecution?.summary?.canonicalEventCount ?? 0} events</dd>
|
||||||
|
<dt>Switchkins:</dt><dd data-program-switchkins-summary="${state.programExecution?.summary?.switchkinsEventCount ?? 0}">${formatSwitchkinsSummary(state.programExecution)}</dd>
|
||||||
|
<dt>Machine run:</dt><dd data-machine-file-execution="status">${formatMachineFileExecution(state.machineFileExecution)}</dd>
|
||||||
|
<dt>Session:</dt><dd data-session-persistence="status">${state.sessionPersistence.status} / ${state.sessionPersistence.path ?? "-"}</dd>
|
||||||
<dt>Tool preview:</dt><dd data-tool-preview="detail">T${state.toolPreview.toolNumber} D${formatNumber(state.toolPreview.diameter, 2)} L${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</dd>
|
<dt>Tool preview:</dt><dd data-tool-preview="detail">T${state.toolPreview.toolNumber} D${formatNumber(state.toolPreview.diameter, 2)} L${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</dd>
|
||||||
|
<dt>Program time:</dt><dd data-program-timing="summary">${formatProgramTiming(state)}</dd>
|
||||||
|
<dt>Segment time:</dt><dd data-program-timing="segment">${formatProgramTimingSegment(state)}</dd>
|
||||||
|
<dt>Runtime feedback:</dt><dd data-program-runtime-feedback="source">${formatProgramRuntimeFeedback(state)}</dd>
|
||||||
|
<dt>Runtime DTG:</dt><dd data-program-runtime-feedback="dtg">${formatProgramRuntimeDtg(state)}</dd>
|
||||||
<dt>Rapid distance:</dt><dd>37.634 mm</dd>
|
<dt>Rapid distance:</dt><dd>37.634 mm</dd>
|
||||||
<dt>Feed distance:</dt><dd>5814.069 mm</dd>
|
<dt>Feed distance:</dt><dd>5814.069 mm</dd>
|
||||||
<dt>X bounds:</dt><dd>8.000 to 113.000 = 105.000 mm</dd>
|
<dt>X bounds:</dt><dd>8.000 to 113.000 = 105.000 mm</dd>
|
||||||
@@ -216,7 +337,19 @@ function renderInfoTabs(element, state) {
|
|||||||
<dt>Z bounds:</dt><dd>-90.500 to -50.000 = 40.500 mm</dd>
|
<dt>Z bounds:</dt><dd>-90.500 to -50.000 = 40.500 mm</dd>
|
||||||
<dt>RTCP frame:</dt><dd data-rtcp-diagnostic="frame">${frame.apiName} ${frame.rtcpState}</dd>
|
<dt>RTCP frame:</dt><dd data-rtcp-diagnostic="frame">${frame.apiName} ${frame.rtcpState}</dd>
|
||||||
<dt>Boundary:</dt><dd data-rtcp-diagnostic="boundary">${frame.semanticBoundary}</dd>
|
<dt>Boundary:</dt><dd data-rtcp-diagnostic="boundary">${frame.semanticBoundary}</dd>
|
||||||
|
<dt>INI:</dt><dd data-linuxcnc-ini="status">${state.iniConfigReadiness.loaded ? "loaded" : "pending"} / ${state.iniConfigReadiness.path ?? "-"}</dd>
|
||||||
|
<dt>INI kins:</dt><dd data-linuxcnc-ini="kins">${state.iniConfigReadiness.kinematics ?? "-"} / ${state.iniConfigReadiness.coordinates ?? "-"}</dd>
|
||||||
|
<dt>INI limits:</dt><dd data-linuxcnc-ini="limits">${formatAxisLimitSummary(state.profile.axisLimits)}</dd>
|
||||||
|
<dt>INI joints:</dt><dd data-linuxcnc-ini="joints">${state.iniConfigReadiness.jointCount ?? 0} joints / ${state.iniConfigReadiness.axisCount ?? 0} axes</dd>
|
||||||
|
<dt>Machine files:</dt><dd data-machine-file-staging="status">${formatMachineFileStaging(state.machineFileStaging)}</dd>
|
||||||
|
<dt>LinuxCNC G-code:</dt><dd data-linuxcnc-gcode-source="selected">${state.machineFileStaging.selectedGcodeSourceRel || state.programSourceRel || "-"}</dd>
|
||||||
|
<dt>Full boundary:</dt><dd data-full-execution-boundary="status">${formatFullExecutionBoundary(state.fullExecutionBoundary)}</dd>
|
||||||
|
<dt>Planner/task:</dt><dd data-full-execution-boundary="blockers">${formatFullExecutionBlockers(state.fullExecutionBoundary)}</dd>
|
||||||
|
<dt>Boundary evidence:</dt><dd data-full-execution-boundary="evidence">${formatFullExecutionEvidence(state.fullExecutionBoundary)}</dd>
|
||||||
<dt>LinuxCNC kins:</dt><dd data-rtcp-diagnostic="kinematics-ready">${frame.readiness.linuxCncKinematicsReady ? "ready" : "pending"}</dd>
|
<dt>LinuxCNC kins:</dt><dd data-rtcp-diagnostic="kinematics-ready">${frame.readiness.linuxCncKinematicsReady ? "ready" : "pending"}</dd>
|
||||||
|
<dt>Kins context:</dt><dd data-rtcp-diagnostic="execution-context">${state.kinematicsExecutionContext}</dd>
|
||||||
|
<dt>Interpreter:</dt><dd data-linuxcnc-boundary="interpreter">${state.interpreterRuntimeReadiness?.loaded ? state.interpreterRuntimeReadiness.semanticBoundary : "pending"}</dd>
|
||||||
|
<dt>Interp context:</dt><dd data-linuxcnc-boundary="interpreter-context">${state.interpreterRuntimeReadiness?.executionContext ?? "none"}</dd>
|
||||||
<dt>Profile refs:</dt><dd>${state.profile.sourceReferences.length} source references</dd>
|
<dt>Profile refs:</dt><dd>${state.profile.sourceReferences.length} source references</dd>
|
||||||
<dt>Adapter:</dt><dd data-linuxcnc-boundary="adapter">${state.linuxCncBoundaryAdapter.apiName}</dd>
|
<dt>Adapter:</dt><dd data-linuxcnc-boundary="adapter">${state.linuxCncBoundaryAdapter.apiName}</dd>
|
||||||
<dt>Panel schema:</dt><dd data-linuxcnc-boundary="panel">${state.linuxCncBoundaryAdapter.panelSummary.schemaId} / ${state.linuxCncBoundaryAdapter.panelSummary.buttonCount} buttons</dd>
|
<dt>Panel schema:</dt><dd data-linuxcnc-boundary="panel">${state.linuxCncBoundaryAdapter.panelSummary.schemaId} / ${state.linuxCncBoundaryAdapter.panelSummary.buttonCount} buttons</dd>
|
||||||
@@ -227,6 +360,97 @@ function renderInfoTabs(element, state) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLinuxCncTaskGates(taskPolicy) {
|
||||||
|
if (!taskPolicy) return "pending";
|
||||||
|
return [
|
||||||
|
taskPolicy.canJog ? "jog" : "jog blocked",
|
||||||
|
taskPolicy.canHome ? "home" : "home blocked",
|
||||||
|
taskPolicy.canRunAuto ? "auto" : "auto blocked",
|
||||||
|
taskPolicy.canExecuteMdi ? "mdi" : "mdi blocked",
|
||||||
|
taskPolicy.canPause ? "pause" : "pause blocked",
|
||||||
|
taskPolicy.canResume ? "resume" : "resume blocked",
|
||||||
|
].join(" / ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLinuxCncTaskSources(taskPolicy) {
|
||||||
|
if (!taskPolicy?.sourceReferences?.length) return "pending";
|
||||||
|
return taskPolicy.sourceReferences
|
||||||
|
.map((reference) => reference.path)
|
||||||
|
.join(" | ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProgramTiming(state) {
|
||||||
|
const timing = state.programExecutionTiming;
|
||||||
|
if (!timing) return "pending";
|
||||||
|
return `${formatDuration(state.programElapsedSeconds)} / ${formatDuration(timing.totalSeconds)} (${formatDuration(state.programRemainingSeconds)} left)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProgramTimingSegment(state) {
|
||||||
|
const segment = state.programExecutionTiming?.segments?.[state.programExecutionMotionIndex || 0];
|
||||||
|
if (!segment) return "pending";
|
||||||
|
return `${segment.motionClass} line ${segment.line ?? "-"} ${formatNumber(segment.linearDistanceMm, 3)} mm ${formatDuration(segment.durationSeconds)} @ ${formatNumber(segment.velocityMmPerMin, 1)} mm/min`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProgramRuntimeFeedback(state) {
|
||||||
|
const feedback = state.programRuntimeFeedback;
|
||||||
|
if (!feedback) return "pending";
|
||||||
|
return `${feedback.sourceMode} sample ${feedback.sampleIndex ?? 0} line ${feedback.line ?? "-"} queue ${feedback.queueDepth ?? 0}/${feedback.activeDepth ?? 0} @ ${formatNumber(feedback.currentVelocityMmPerMin, 1)} mm/min`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProgramRuntimeDtg(state) {
|
||||||
|
const feedback = state.programRuntimeFeedback;
|
||||||
|
if (!feedback) return "pending";
|
||||||
|
const dtg = feedback.dtg || {};
|
||||||
|
return `DTG ${formatNumber(dtg.x, 3)} / ${formatNumber(dtg.y, 3)} / ${formatNumber(dtg.z, 3)} distance ${formatNumber(feedback.distanceToGo, 3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
const safeSeconds = Math.max(Number(seconds) || 0, 0);
|
||||||
|
const minutes = Math.floor(safeSeconds / 60);
|
||||||
|
const remainder = safeSeconds - minutes * 60;
|
||||||
|
return `${minutes}:${remainder.toFixed(1).padStart(4, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSwitchkinsSummary(programExecution) {
|
||||||
|
const count = programExecution?.summary?.switchkinsEventCount ?? 0;
|
||||||
|
if (count === 0) return "0 events";
|
||||||
|
const codes = programExecution.summary.switchkinsCodes?.join("/") || "-";
|
||||||
|
return `${count} events ${codes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMachineFileStaging(machineFileStaging) {
|
||||||
|
if (!machineFileStaging || machineFileStaging.status === "not-staged") {
|
||||||
|
return "not staged";
|
||||||
|
}
|
||||||
|
if (machineFileStaging.status === "error") {
|
||||||
|
return `error ${machineFileStaging.lastError || "-"}`;
|
||||||
|
}
|
||||||
|
return `${machineFileStaging.status} ${machineFileStaging.fileCount || 0} files ${machineFileStaging.opfsRoot || "-"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMachineFileExecution(machineFileExecution) {
|
||||||
|
if (!machineFileExecution?.machineFilePlan) return "not run";
|
||||||
|
const summary = machineFileExecution.summary || {};
|
||||||
|
return `${summary.machineFileExecutionReady ? "ready" : "ran"} ${summary.motionEventCount || 0} motion ${machineFileExecution.machineFilePlan.profileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFullExecutionBoundary(boundary) {
|
||||||
|
if (!boundary) return "pending";
|
||||||
|
const remap = boundary.machineFileBackedRemapReady ? "remap ready" : "remap pending";
|
||||||
|
const full = boundary.fullLinuxCncProgramExecutionReady ? "full ready" : "full blocked";
|
||||||
|
return `${boundary.phase} / ${remap} / ${full}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFullExecutionBlockers(boundary) {
|
||||||
|
if (!boundary) return "pending";
|
||||||
|
return boundary.blockers.slice(0, 2).join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFullExecutionEvidence(boundary) {
|
||||||
|
if (!boundary) return "pending";
|
||||||
|
return `${boundary.satisfied.length} satisfied / ${boundary.missing.length} missing / ${boundary.semanticBoundary}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderOverride(element, state, dispatch) {
|
function renderOverride(element, state, dispatch) {
|
||||||
element.innerHTML = `
|
element.innerHTML = `
|
||||||
<section class="meter-card">
|
<section class="meter-card">
|
||||||
@@ -301,6 +525,7 @@ function renderBottomControls(element, state, dispatch) {
|
|||||||
["Run", "RUN", () => dispatch({ type: "RUN" })],
|
["Run", "RUN", () => dispatch({ type: "RUN" })],
|
||||||
["Stop", "STOP", () => dispatch({ type: "STOP" })],
|
["Stop", "STOP", () => dispatch({ type: "STOP" })],
|
||||||
["Pause", "PAUSE", () => dispatch({ type: "PAUSE" })],
|
["Pause", "PAUSE", () => dispatch({ type: "PAUSE" })],
|
||||||
|
["Resume", "RESUME", () => dispatch({ type: "RESUME" })],
|
||||||
["Step", "STEP", () => dispatch({ type: "STEP" })],
|
["Step", "STEP", () => dispatch({ type: "STEP" })],
|
||||||
["Home", "HOME", () => dispatch({ type: "HOME" })],
|
["Home", "HOME", () => dispatch({ type: "HOME" })],
|
||||||
["X-", "JOG_X_NEG", () => dispatch({ type: "JOG", axis: "x", direction: -1 })],
|
["X-", "JOG_X_NEG", () => dispatch({ type: "JOG", axis: "x", direction: -1 })],
|
||||||
@@ -308,6 +533,9 @@ function renderBottomControls(element, state, dispatch) {
|
|||||||
["Y-", "JOG_Y_NEG", () => dispatch({ type: "JOG", axis: "y", direction: -1 })],
|
["Y-", "JOG_Y_NEG", () => dispatch({ type: "JOG", axis: "y", direction: -1 })],
|
||||||
["Y+", "JOG_Y_POS", () => dispatch({ type: "JOG", axis: "y", direction: 1 })],
|
["Y+", "JOG_Y_POS", () => dispatch({ type: "JOG", axis: "y", direction: 1 })],
|
||||||
["MDI", "MDI_RUN", () => dispatch({ type: "RUN_MDI" })],
|
["MDI", "MDI_RUN", () => dispatch({ type: "RUN_MDI" })],
|
||||||
|
["Save Session", "SAVE_SESSION", () => dispatch({ type: "SAVE_SESSION_REQUEST" })],
|
||||||
|
["Restore Session", "RESTORE_SESSION", () => dispatch({ type: "RESTORE_SESSION_REQUEST" })],
|
||||||
|
["Audit", "AUDIT_FULL_BOUNDARY", () => dispatch({ type: "RUN_FULL_BOUNDARY_AUDIT_REQUEST" })],
|
||||||
["Full", "FULL", () => dispatch({ type: "TOGGLE_FULLSCREEN" })],
|
["Full", "FULL", () => dispatch({ type: "TOGGLE_FULLSCREEN" })],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -345,6 +573,12 @@ function formatNumber(value, digits = 3) {
|
|||||||
return Number(value).toFixed(digits);
|
return Number(value).toFixed(digits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAxisLimitSummary(axisLimits) {
|
||||||
|
return Object.entries(axisLimits || {})
|
||||||
|
.map(([axis, limit]) => `${axis}[${formatNumber(limit.min, 0)},${formatNumber(limit.max, 0)}]`)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value)
|
return String(value)
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
|
|||||||
@@ -3,24 +3,43 @@ import * as THREE from "../vendor/three/three.module.js";
|
|||||||
const scenes = new WeakMap();
|
const scenes = new WeakMap();
|
||||||
|
|
||||||
export function renderFiveAxisScene(canvas, state) {
|
export function renderFiveAxisScene(canvas, state) {
|
||||||
const preview = scenes.get(canvas) || createScene(canvas);
|
let preview = scenes.get(canvas);
|
||||||
scenes.set(canvas, preview);
|
if (!preview) {
|
||||||
|
preview = createPreview(canvas);
|
||||||
|
scenes.set(canvas, preview);
|
||||||
|
}
|
||||||
|
|
||||||
resizeRenderer(preview);
|
if (preview.kind === "fallback") {
|
||||||
updateMachinePose(preview, state);
|
renderFallbackPreview(preview, state);
|
||||||
preview.renderer.render(preview.scene, preview.camera);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resizeRenderer(preview);
|
||||||
|
updateMachinePose(preview, state);
|
||||||
|
preview.renderer.render(preview.scene, preview.camera);
|
||||||
|
} catch (error) {
|
||||||
|
const fallback = createFallbackPreview(canvas, error);
|
||||||
|
scenes.set(canvas, fallback);
|
||||||
|
renderFallbackPreview(fallback, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pointCount = preview.pathLine.geometry.getAttribute("position").count;
|
const pointCount = preview.pathLine.geometry.getAttribute("position").count;
|
||||||
canvas.dataset.threeReady = "true";
|
exposePreviewDataset(canvas, state, {
|
||||||
canvas.dataset.threeRevision = THREE.REVISION;
|
pointCount,
|
||||||
canvas.dataset.threePathPoints = String(pointCount);
|
sceneObjectCount: countSceneObjects(preview.scene),
|
||||||
canvas.dataset.threeSceneObjects = String(countSceneObjects(preview.scene));
|
toolhead: preview.toolGroup.position,
|
||||||
canvas.dataset.threeToolhead = JSON.stringify(toRoundedVector(preview.toolGroup.position));
|
renderer: "webgl",
|
||||||
canvas.dataset.threeToolAxis = JSON.stringify(toRoundedVector(state.toolAxisVector));
|
});
|
||||||
canvas.dataset.threeTcpPose = JSON.stringify(toRoundedPose(state.tcpPose));
|
}
|
||||||
canvas.dataset.threeRtcpState = state.rtcpState;
|
|
||||||
canvas.dataset.threeSelectedView = state.preview.selectedView;
|
function createPreview(canvas) {
|
||||||
canvas.dataset.threeFrameApi = state.rtcpFrame.apiName;
|
try {
|
||||||
|
return createScene(canvas);
|
||||||
|
} catch (error) {
|
||||||
|
return createFallbackPreview(canvas, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createScene(canvas) {
|
function createScene(canvas) {
|
||||||
@@ -29,25 +48,36 @@ function createScene(canvas) {
|
|||||||
antialias: true,
|
antialias: true,
|
||||||
preserveDrawingBuffer: true,
|
preserveDrawingBuffer: true,
|
||||||
});
|
});
|
||||||
renderer.setClearColor(0x050505, 1);
|
renderer.setClearColor(0x07100d, 1);
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100);
|
scene.fog = new THREE.Fog(0x07100d, 7, 15);
|
||||||
camera.position.set(4.2, -6.4, 4.8);
|
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
||||||
|
camera.position.set(3.4, -5.0, 3.2);
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
const ambient = new THREE.AmbientLight(0xffffff, 0.46);
|
const ambient = new THREE.HemisphereLight(0xdffbff, 0x17110d, 0.86);
|
||||||
const key = new THREE.DirectionalLight(0xffffff, 1.1);
|
const key = new THREE.DirectionalLight(0xffffff, 1.35);
|
||||||
key.position.set(3, -5, 7);
|
key.position.set(3, -5, 7);
|
||||||
scene.add(ambient, key);
|
const fill = new THREE.DirectionalLight(0x4fd5ff, 0.46);
|
||||||
|
fill.position.set(-4, 3, 3);
|
||||||
|
scene.add(ambient, key, fill);
|
||||||
|
|
||||||
const grid = new THREE.GridHelper(6.8, 12, 0x444444, 0x242424);
|
const floor = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(6.2, 4.5, 0.08),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x151b1f, roughness: 0.78, metalness: 0.12 }),
|
||||||
|
);
|
||||||
|
floor.position.z = -0.98;
|
||||||
|
scene.add(floor);
|
||||||
|
|
||||||
|
const grid = new THREE.GridHelper(6.2, 14, 0x4b555a, 0x252f32);
|
||||||
grid.rotation.x = Math.PI / 2;
|
grid.rotation.x = Math.PI / 2;
|
||||||
|
grid.position.z = -0.93;
|
||||||
scene.add(grid);
|
scene.add(grid);
|
||||||
|
|
||||||
const axes = new THREE.AxesHelper(1.25);
|
const axes = new THREE.AxesHelper(1.45);
|
||||||
axes.position.set(-2.7, -2.25, -1.05);
|
axes.position.set(-2.85, -2.0, -0.86);
|
||||||
scene.add(axes);
|
scene.add(axes);
|
||||||
|
|
||||||
const envelope = buildEnvelope();
|
const envelope = buildEnvelope();
|
||||||
@@ -55,12 +85,12 @@ function createScene(canvas) {
|
|||||||
|
|
||||||
const tableGroup = new THREE.Group();
|
const tableGroup = new THREE.Group();
|
||||||
const table = new THREE.Mesh(
|
const table = new THREE.Mesh(
|
||||||
new THREE.BoxGeometry(2.55, 1.9, 0.16),
|
new THREE.BoxGeometry(3.0, 2.15, 0.18),
|
||||||
new THREE.MeshStandardMaterial({ color: 0x353535, roughness: 0.72, metalness: 0.2 }),
|
new THREE.MeshStandardMaterial({ color: 0x42484e, roughness: 0.68, metalness: 0.22 }),
|
||||||
);
|
);
|
||||||
const platter = new THREE.Mesh(
|
const platter = new THREE.Mesh(
|
||||||
new THREE.CylinderGeometry(0.72, 0.72, 0.13, 48),
|
new THREE.CylinderGeometry(0.84, 0.84, 0.16, 64),
|
||||||
new THREE.MeshStandardMaterial({ color: 0x4f5960, roughness: 0.62, metalness: 0.35 }),
|
new THREE.MeshStandardMaterial({ color: 0x6d7880, roughness: 0.48, metalness: 0.42 }),
|
||||||
);
|
);
|
||||||
platter.rotation.x = Math.PI / 2;
|
platter.rotation.x = Math.PI / 2;
|
||||||
platter.position.z = 0.14;
|
platter.position.z = 0.14;
|
||||||
@@ -72,14 +102,14 @@ function createScene(canvas) {
|
|||||||
|
|
||||||
const toolGroup = new THREE.Group();
|
const toolGroup = new THREE.Group();
|
||||||
const toolBody = new THREE.Mesh(
|
const toolBody = new THREE.Mesh(
|
||||||
new THREE.CylinderGeometry(0.035, 0.055, 0.86, 24),
|
new THREE.CylinderGeometry(0.045, 0.07, 1.05, 28),
|
||||||
new THREE.MeshStandardMaterial({ color: 0x26d7df, emissive: 0x073a3d, roughness: 0.32 }),
|
new THREE.MeshStandardMaterial({ color: 0x29ecf0, emissive: 0x0a4f52, roughness: 0.28 }),
|
||||||
);
|
);
|
||||||
toolBody.rotation.x = Math.PI / 2;
|
toolBody.rotation.x = Math.PI / 2;
|
||||||
toolBody.position.z = 0.43;
|
toolBody.position.z = 0.52;
|
||||||
const tcpPoint = new THREE.Mesh(
|
const tcpPoint = new THREE.Mesh(
|
||||||
new THREE.SphereGeometry(0.075, 24, 16),
|
new THREE.SphereGeometry(0.095, 28, 18),
|
||||||
new THREE.MeshStandardMaterial({ color: 0x1ffff4, emissive: 0x094f4f, roughness: 0.2 }),
|
new THREE.MeshStandardMaterial({ color: 0x1ffff4, emissive: 0x0b6f6f, roughness: 0.18 }),
|
||||||
);
|
);
|
||||||
const toolAxis = new THREE.Line(
|
const toolAxis = new THREE.Line(
|
||||||
new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3(0, 0, 1)]),
|
new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3(0, 0, 1)]),
|
||||||
@@ -89,6 +119,7 @@ function createScene(canvas) {
|
|||||||
scene.add(toolGroup);
|
scene.add(toolGroup);
|
||||||
|
|
||||||
const preview = {
|
const preview = {
|
||||||
|
kind: "webgl",
|
||||||
renderer,
|
renderer,
|
||||||
scene,
|
scene,
|
||||||
camera,
|
camera,
|
||||||
@@ -101,10 +132,136 @@ function createScene(canvas) {
|
|||||||
return preview;
|
return preview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createFallbackPreview(canvas, error) {
|
||||||
|
return {
|
||||||
|
kind: "fallback",
|
||||||
|
canvas,
|
||||||
|
errorMessage: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFallbackPreview(preview, state) {
|
||||||
|
const { canvas } = preview;
|
||||||
|
const width = Math.max(canvas.clientWidth, 320);
|
||||||
|
const height = Math.max(canvas.clientHeight, 240);
|
||||||
|
if (canvas.width !== width || canvas.height !== height) {
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
ctx.fillStyle = "#07100d";
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
const cx = width * 0.5;
|
||||||
|
const cy = height * 0.53;
|
||||||
|
const scale = Math.min(width / 7.2, height / 4.8);
|
||||||
|
|
||||||
|
drawFallbackGrid(ctx, cx, cy, scale);
|
||||||
|
|
||||||
|
ctx.strokeStyle = "#e43a35";
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.strokeRect(cx - 2.95 * scale, cy - 1.9 * scale, 5.9 * scale, 3.8 * scale);
|
||||||
|
|
||||||
|
ctx.fillStyle = "#42484e";
|
||||||
|
ctx.strokeStyle = "#79838a";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(cx - 1.5 * scale, cy - 0.78 * scale, 3.0 * scale, 1.56 * scale, 4);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.fillStyle = "#6d7880";
|
||||||
|
ctx.strokeStyle = "#a3b0b8";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(cx, cy, 0.84 * scale, 0.48 * scale, 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.strokeStyle = "#ffffff";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let index = 0; index < 72; index += 1) {
|
||||||
|
const t = index / 71;
|
||||||
|
const x = cx + (-2.45 + t * 4.9) * scale;
|
||||||
|
const y = cy + Math.sin(t * Math.PI * 13) * 0.36 * scale;
|
||||||
|
if (index === 0) ctx.moveTo(x, y);
|
||||||
|
else ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const tcp = state.tcpPose;
|
||||||
|
const tool = state.toolAxisVector;
|
||||||
|
const toolX = cx + clamp(tcp.x * 0.035, -2.7, 2.7) * scale;
|
||||||
|
const toolY = cy - clamp(tcp.y * 0.035, -2.0, 2.0) * scale;
|
||||||
|
const axisX = toolX + tool.x * 0.85 * scale;
|
||||||
|
const axisY = toolY - (tool.y || 0.2) * 0.85 * scale;
|
||||||
|
ctx.strokeStyle = "#21f2f2";
|
||||||
|
ctx.fillStyle = "#1ffff4";
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(toolX, toolY);
|
||||||
|
ctx.lineTo(axisX, axisY);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(toolX, toolY, 0.09 * scale, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.fillStyle = "#b7c7b8";
|
||||||
|
ctx.font = "12px Courier New, monospace";
|
||||||
|
ctx.fillText("2D RTCP fallback", 12, height - 14);
|
||||||
|
|
||||||
|
exposePreviewDataset(canvas, state, {
|
||||||
|
pointCount: 72,
|
||||||
|
sceneObjectCount: 12,
|
||||||
|
toolhead: {
|
||||||
|
x: clamp(tcp.x * 0.035, -2.7, 2.7),
|
||||||
|
y: clamp(tcp.y * 0.035, -2.0, 2.0),
|
||||||
|
z: clamp(tcp.z * 0.04 + 0.35, -1.1, 1.9),
|
||||||
|
},
|
||||||
|
renderer: "2d-fallback",
|
||||||
|
});
|
||||||
|
canvas.dataset.threeFallbackReason = preview.errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFallbackGrid(ctx, cx, cy, scale) {
|
||||||
|
ctx.strokeStyle = "#273236";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let x = -3; x <= 3; x += 0.5) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cx + x * scale, cy - 2.1 * scale);
|
||||||
|
ctx.lineTo(cx + x * scale, cy + 2.1 * scale);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = -2; y <= 2; y += 0.5) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cx - 3.1 * scale, cy + y * scale);
|
||||||
|
ctx.lineTo(cx + 3.1 * scale, cy + y * scale);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exposePreviewDataset(canvas, state, preview) {
|
||||||
|
canvas.dataset.threeReady = "true";
|
||||||
|
canvas.dataset.threeRevision = THREE.REVISION;
|
||||||
|
canvas.dataset.threePathPoints = String(preview.pointCount);
|
||||||
|
canvas.dataset.threeSceneObjects = String(preview.sceneObjectCount);
|
||||||
|
canvas.dataset.threeToolhead = JSON.stringify(toRoundedVector(preview.toolhead));
|
||||||
|
canvas.dataset.threeToolAxis = JSON.stringify(toRoundedVector(state.toolAxisVector));
|
||||||
|
canvas.dataset.threeTcpPose = JSON.stringify(toRoundedPose(state.tcpPose));
|
||||||
|
canvas.dataset.threeRtcpState = state.rtcpState;
|
||||||
|
canvas.dataset.threeSelectedView = state.preview.selectedView;
|
||||||
|
canvas.dataset.threeFrameApi = state.rtcpFrame.apiName;
|
||||||
|
canvas.dataset.threeRenderer = preview.renderer;
|
||||||
|
}
|
||||||
|
|
||||||
function buildEnvelope() {
|
function buildEnvelope() {
|
||||||
const geometry = new THREE.BoxGeometry(5.8, 4.3, 2.6);
|
const geometry = new THREE.BoxGeometry(5.9, 4.25, 2.7);
|
||||||
const edges = new THREE.EdgesGeometry(geometry);
|
const edges = new THREE.EdgesGeometry(geometry);
|
||||||
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xcc2525 }));
|
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xe43a35 }));
|
||||||
line.position.z = 0.2;
|
line.position.z = 0.2;
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
@@ -113,13 +270,13 @@ function buildToolpath() {
|
|||||||
const points = [];
|
const points = [];
|
||||||
for (let index = 0; index < 72; index += 1) {
|
for (let index = 0; index < 72; index += 1) {
|
||||||
const t = index / 71;
|
const t = index / 71;
|
||||||
const x = -2.3 + t * 4.6;
|
const x = -2.45 + t * 4.9;
|
||||||
const y = Math.sin(t * Math.PI * 13) * 0.28;
|
const y = Math.sin(t * Math.PI * 13) * 0.36;
|
||||||
const z = -0.75 + Math.sin(t * Math.PI * 2) * 0.36;
|
const z = -0.68 + Math.sin(t * Math.PI * 2) * 0.42;
|
||||||
points.push(new THREE.Vector3(x, y, z));
|
points.push(new THREE.Vector3(x, y, z));
|
||||||
}
|
}
|
||||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||||
return new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xf4f4f4 }));
|
return new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xffffff }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMachinePose(preview, state) {
|
function updateMachinePose(preview, state) {
|
||||||
@@ -145,13 +302,13 @@ function updateMachinePose(preview, state) {
|
|||||||
|
|
||||||
function setCameraView(camera, selectedView) {
|
function setCameraView(camera, selectedView) {
|
||||||
if (selectedView === "x") {
|
if (selectedView === "x") {
|
||||||
camera.position.set(6, 0.02, 0.4);
|
camera.position.set(6, 0.02, 0.45);
|
||||||
} else if (selectedView === "y") {
|
} else if (selectedView === "y") {
|
||||||
camera.position.set(0.02, -6, 0.6);
|
camera.position.set(0.02, -6, 0.7);
|
||||||
} else if (selectedView === "z") {
|
} else if (selectedView === "z") {
|
||||||
camera.position.set(0.01, -0.02, 7);
|
camera.position.set(0.01, -0.02, 6.6);
|
||||||
} else {
|
} else {
|
||||||
camera.position.set(4.2, -6.4, 4.8);
|
camera.position.set(3.4, -5.0, 3.2);
|
||||||
}
|
}
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(0, 0, 0);
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|||||||
@@ -333,25 +333,285 @@ promotionAllowed=false
|
|||||||
## 9. 当前状态
|
## 9. 当前状态
|
||||||
|
|
||||||
```text
|
```text
|
||||||
status=M6_linuxcnc_kinematics_frame_proof_complete
|
status=M17_linuxcnc_5axis_gcode_source_ingest_complete
|
||||||
active_style=gmoccapy_5_axis
|
active_style=gmoccapy_5_axis
|
||||||
frontend_framework=none
|
frontend_framework=none
|
||||||
ui_stack=html_css_typescript_es_modules
|
ui_stack=html_css_typescript_es_modules
|
||||||
preview_stack=threejs
|
preview_stack=threejs
|
||||||
semantic_boundary=linuxcnc_owned
|
semantic_boundary=linuxcnc_owned
|
||||||
latest_batch=M6-linuxcnc-kinematics-frame-proof
|
latest_batch=M17-linuxcnc-5axis-gcode-source-ingest
|
||||||
latest_gate=linuxcnc_kinematics_runtime_smoke=ok,profile_boundary_smoke=ok,rtcp_store_smoke=ok,gmoccapy_shell_smoke=ok,gmoccapy_static_build=ok
|
latest_gate=full_execution_boundary_smoke=ok,linuxcnc_kinematics_runtime_smoke=ok,linuxcnc_interpreter_runtime_smoke=ok,linuxcnc_ini_runtime_smoke=ok,full_linuxcnc_5axis_source_node_smoke=ok,machine_file_staging_smoke=ok,five_axis_session_smoke=ok,profile_boundary_smoke=ok,rtcp_store_smoke=ok,gmoccapy_shell_smoke=ok,gmoccapy_dist_smoke=ok,gmoccapy_static_build=ok
|
||||||
rtcp_ui_state=implemented_fixture_fallback_and_node_kinematics_wasm_frame
|
rtcp_ui_state=implemented_browser_worker_and_node_kinematics_wasm_frame_with_fixture_fallback
|
||||||
control_wiring=power_estop_reset_auto_manual_jog_mdi_run_stop_pause_step_overrides_coolant_spindle_preview_home_reload_full
|
control_wiring=power_estop_reset_auto_manual_jog_mdi_run_stop_pause_step_overrides_coolant_spindle_preview_home_reload_full
|
||||||
gcode_loading=implemented_browser_file_text_staging
|
gcode_loading=implemented_browser_file_text_staging_with_linuxcnc_interpreter_execution
|
||||||
program_current_line=implemented_fixture_line_playback_and_highlight
|
linuxcnc_5axis_gcode_sources=implemented_and_guarded_to_linuxcnc_source_manifest_trt_demos_only
|
||||||
|
program_current_line=implemented_linuxcnc_canonical_motion_highlight_with_fixture_fallback
|
||||||
tool_preview=implemented_tool_card_and_threejs_marker
|
tool_preview=implemented_tool_card_and_threejs_marker
|
||||||
threejs_preview=implemented_basic_canvas_scene
|
threejs_preview=implemented_basic_canvas_scene
|
||||||
profile_source_map=implemented_xyzac_trt
|
profile_source_map=implemented_xyzac_trt_and_xyzbc_trt
|
||||||
|
profile_switching=implemented_xyzac_trt_and_xyzbc_trt_with_runtime_reload
|
||||||
pyvcp_hal_schema=implemented_xyzac_trt_switchkins
|
pyvcp_hal_schema=implemented_xyzac_trt_switchkins
|
||||||
linuxcnc_boundary_adapter=kinematics_runtime_connected_node_interpreter_remap_missing
|
linuxcnc_boundary_adapter=kinematics_and_interpreter_runtime_connected_remap_planner_missing
|
||||||
linuxcnc_kinematics_wasm=node_proof_ready_xyzac_trt
|
full_execution_boundary=implemented_machine_file_remap_ready_with_planner_task_hal_blockers
|
||||||
browser_kinematics_wasm=not_connected_fixture_fallback
|
linuxcnc_kinematics_wasm=browser_and_node_proof_ready_xyzac_trt
|
||||||
full_program_execution=not_promoted_fixture_line_playback
|
browser_kinematics_wasm=worker_connected_source_and_dist_xyzac_trt
|
||||||
next_batch=browser_kinematics_wasm_asset_worker_or_interpreter_execution_source
|
linuxcnc_interpreter_wasm=browser_worker_and_node_direct_canonical_program_execution_ready_with_switchkins_mcode_preservation
|
||||||
|
browser_interpreter_wasm=worker_connected_source_and_dist
|
||||||
|
switchkins_rtcp_program_execution=implemented_m428_m429_program_events_drive_rtcp_and_kinematics_switch
|
||||||
|
full_program_execution=partial_interpreter_canonical_and_switchkins_event_ready_remap_planner_not_promoted
|
||||||
|
session_persistence=implemented_opfs_save_restore_for_5axis_session
|
||||||
|
machine_file_staging=implemented_linuxcnc_trt_ini_hal_tool_table_remap_demo_opfs_staging
|
||||||
|
machine_file_backed_run=implemented_linuxcnc_fiveaxis_remap_wasm_machine_file_execution
|
||||||
|
planner_task_hal_gap=audited_as_blocked_without_native_task_nml_realtime_hal_and_planner_queue_runtime
|
||||||
|
next_batch=trajectory_planner_wasm_boundary_or_native_task_hal_port
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. M12 任务
|
||||||
|
|
||||||
|
```text
|
||||||
|
M12-switchkins-rtcp-program-execution
|
||||||
|
```
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
completed_with_program_level_rtcp_switching
|
||||||
|
```
|
||||||
|
|
||||||
|
已完成:
|
||||||
|
|
||||||
|
- `linuxcnc-interpreter-runtime` 识别 LinuxCNC TRT 配置中的 `M428/M429/M430` switchkins remap M-code;
|
||||||
|
- 因当前 interpreter WASM 未启用 Python/NGC remap runtime,运行前会从送入 WASM 的程序文本中移除这些高位 M-code,避免 `M-code greater than 199` 阻断普通 canonical motion;
|
||||||
|
- 原始程序的 `M428/M429/M430` 被保留为 `switchkinsEvents`,并打上 `linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied` 边界;
|
||||||
|
- canonical motion event 继承最近的 switchkins 状态,`RUN/STEP/RUN_FRAME` 按程序自动切换 `identity` / `tcp-xyzac` / `tcp-xyzbc`;
|
||||||
|
- node/browser kinematics runtime 增加 `switchKinematics()`,程序内 `M428/M429` 会同步切换 LinuxCNC kinematics WASM 的 `lckins_switch()`;
|
||||||
|
- gmoccapy diagnostics 显示 program switchkins event count;
|
||||||
|
- smoke 覆盖含 `M428/M429` 的五轴联动 RTCP 程序:加载后进入 TCP,执行 A/C 联动段保持 RTCP on,运行到 `M429` 后回到 identity。
|
||||||
|
|
||||||
|
边界说明:
|
||||||
|
|
||||||
|
```text
|
||||||
|
switchkinsProgramBoundary=linuxcnc_remap_mcode_preserved_and_web_runtime_applied
|
||||||
|
interpreterBoundary=linuxcnc_interpreter_wasm_canonical_events
|
||||||
|
kinematicsBoundary=linuxcnc_kinematics_wasm_c_abi
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14. M16 任务
|
||||||
|
|
||||||
|
```text
|
||||||
|
M16-linuxcnc-tp-queue-timing-runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
completed_with_tp_wasm_and_web_node_smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
已完成:
|
||||||
|
|
||||||
|
- `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c` 新增
|
||||||
|
`lctp_run_canonical_motion_timing()`;
|
||||||
|
- TP WASM C ABI 将 LinuxCNC interpreter canonical motion events 入队到 vendored
|
||||||
|
LinuxCNC `src/emc/tp` runtime,并通过 `tpRunCycle()` 生成 segment timing;
|
||||||
|
- canonical 零长度 motion 保留为 0 秒 timing segment,不作为 TP enqueue failure;
|
||||||
|
- 新增 `wasm-port/runtime/sdk/src/linuxcnc-tp.js`;
|
||||||
|
- `linuxcnc-interpreter-runtime` 在 `runProgram()` / `runMachineFileProgram()` 后调用
|
||||||
|
TP SDK,`programExecution.plannerTiming` 使用
|
||||||
|
`linuxcnc_tp_queue_runtime_timing_from_canonical_motion` 边界;
|
||||||
|
- store 优先使用 TP planner timing;JS `execution-timing.js` 保留为 fallback/MDI
|
||||||
|
lightweight path;
|
||||||
|
- full execution boundary 可在 TP timing 成功时报告 `plannerRuntimeReady=true`。
|
||||||
|
- browser source/dist smoke 验证 interpreter Worker readiness 包含 TP planner runtime,
|
||||||
|
并验证含 `ARC_FEED` 的真实 G-code 程序通过 TP arc timing segment。
|
||||||
|
|
||||||
|
边界说明:
|
||||||
|
|
||||||
|
```text
|
||||||
|
timingBoundary=linuxcnc_tp_queue_runtime_timing_from_canonical_motion
|
||||||
|
plannerRuntimeReady=true for interpreter canonical motion with TP WASM timing
|
||||||
|
nativeTaskReady=false
|
||||||
|
nativeHalSyncReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
hardwareDrive=false
|
||||||
|
```
|
||||||
|
|
||||||
|
验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
source /home/cnc/emsdk/emsdk_env.sh >/dev/null && bash wasm-port/tools/build_tp_wasm.sh && bash wasm-port/tests/wasm/node/verify_tp_wasm.sh
|
||||||
|
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_interpreter_runtime.mjs
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. M13 任务
|
||||||
|
|
||||||
|
```text
|
||||||
|
M13-interpreter-worker-runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
completed_with_browser_worker_smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
已完成:
|
||||||
|
|
||||||
|
- 新增 `linuxcnc-interpreter-worker.js` 和 `linuxcnc-interpreter-worker-client.js`;
|
||||||
|
- 浏览器默认优先通过 Web Worker 加载 LinuxCNC interpreter WASM;
|
||||||
|
- store 的 `RUN_INTERPRETER_PROGRAM` 支持 async runtime,避免 worker 结果阻塞 UI 线程;
|
||||||
|
- 增加 `interpreterExecutionPending` 和 sequence guard,防止旧解释器结果覆盖新加载程序;
|
||||||
|
- gmoccapy diagnostics 显示 interpreter execution context;
|
||||||
|
- browser smoke 验证 kinematics 和 interpreter 都在 Worker,并验证 source/dist 两条路径。
|
||||||
|
|
||||||
|
边界说明:
|
||||||
|
|
||||||
|
```text
|
||||||
|
browserInterpreterExecutionContext=worker
|
||||||
|
nodeInterpreterExecutionContext=direct
|
||||||
|
interpreterBoundary=linuxcnc_interpreter_wasm_canonical_events
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14. M16 任务
|
||||||
|
|
||||||
|
```text
|
||||||
|
M16-planner-task-hal-boundary-audit
|
||||||
|
```
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
completed_with_full_execution_boundary_smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
已完成:
|
||||||
|
|
||||||
|
- 新增 `full-execution-boundary.js`,统一汇总 kinematics、interpreter canonical、machine-file staging、five-axis remap run、switchkins HAL evidence;
|
||||||
|
- store 新增 `fullExecutionBoundary` 派生状态和 `RUN_FULL_BOUNDARY_AUDIT_REQUEST`;
|
||||||
|
- gmoccapy diagnostics 显示 full boundary、planner/task/HAL blockers 和证据摘要;
|
||||||
|
- 底部新增 `Audit` 按钮,可触发 machine-file staging + machine-file backed five-axis remap run;
|
||||||
|
- interpreter runtime 对 machine-file backed run 的 `remapRuntimeReady` 改为基于 `fiveaxis_ini_open=1`、`fiveaxis_remaps_ready=1`、`fiveaxis_file_reached_exit=1`;
|
||||||
|
- Node/browser smoke 验证 partial remap boundary ready,同时继续禁止 full LinuxCNC program execution promoted。
|
||||||
|
|
||||||
|
边界说明:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fullExecutionBoundary=linuxcnc_machine_file_remap_ready_planner_task_hal_blocked
|
||||||
|
remapRuntimeReady=true for staged fiveAxisRemap C ABI run
|
||||||
|
halSwitchkinsEvidenceReady=true for fiveaxis_hal_switchkins evidence
|
||||||
|
nativeTaskReady=false
|
||||||
|
nativeHalSyncReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
promotionAllowed=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. M17 任务
|
||||||
|
|
||||||
|
```text
|
||||||
|
M17-linuxcnc-5axis-gcode-source-ingest
|
||||||
|
```
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
completed_with_real_linuxcnc_5axis_source_program_smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
已完成:
|
||||||
|
|
||||||
|
- machine-file staging 从 LinuxCNC source manifest 中追加 TRT `demos/*.ngc`,包括 `boat-xyzac.ngc`、`boat-xyzbc.ngc`、`impeller-7bl-xyzac.ngc`、`xyzac_switchkins*.ngc`、`xyzbc_switchkins.ngc`;
|
||||||
|
- `selectMachineFileProgram()` 强制 5 轴 G-code 必须来自 LinuxCNC 源程序目录 `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc`;
|
||||||
|
- 新增 `listLinuxCncGcodeSources()` 和 `selectMachineFileProgram()`;
|
||||||
|
- store 新增 `LOAD_LINUXCNC_GCODE_SOURCE`,可把 staged LinuxCNC 真实 5 轴 `.ngc` 源文件加载到 G-code 面板;
|
||||||
|
- gmoccapy G-code 面板新增 LinuxCNC 5-axis source 下拉框和 staging 按钮;
|
||||||
|
- machine-file backed remap run 使用当前选中的 LinuxCNC `.ngc` 源程序路径;
|
||||||
|
- Node/browser smoke 验证选择 `impeller-7bl-xyzac.ngc` 后,UI 加载真实源程序,machine-file run 的 `wasmProgramPath` 指向该源文件。
|
||||||
|
|
||||||
|
边界说明:
|
||||||
|
|
||||||
|
```text
|
||||||
|
gcodeSourceBoundary=linuxcnc_vendored_5axis_gcode_source_file
|
||||||
|
sourceMode=linuxcnc-vendored-5axis-gcode
|
||||||
|
machineFileBackedRunProgram=selected_staged_linuxcnc_ngc_source
|
||||||
|
sourceDirectoryGuard=configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc only
|
||||||
|
remapRuntimeReady=true for staged fiveAxisRemap C ABI run
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
nativeTaskReady=false
|
||||||
|
nativeHalSyncReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. M14 任务
|
||||||
|
|
||||||
|
```text
|
||||||
|
M14-opfs-machine-file-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
completed_with_node_and_browser_opfs_smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
已完成:
|
||||||
|
|
||||||
|
- 新增 `linuxcnc-machine-file-staging.js`;
|
||||||
|
- 复用 `wasm-port/runtime/sdk/src/sim-config-staging.js` 的 `planSimConfigStaging()`,为 `xyzac-trt` / `xyzbc-trt` 生成 LinuxCNC sim-config staging plan;
|
||||||
|
- 将 INI、tool table、HAL、PyVCP XML、`remap_subs/*.ngc`、demo G-code 等 vendored LinuxCNC 文本文件保存到 OPFS 风格路径;
|
||||||
|
- store 增加 `machineFileStaging` 状态和 `stageMachineFiles()` API;
|
||||||
|
- gmoccapy diagnostics 显示 machine-file staging 状态;
|
||||||
|
- build 产物复制 `wasm-port/tools/source-manifest.txt`,保证 dist 页面也能生成 staging plan;
|
||||||
|
- node smoke 使用 memory OPFS 验证文件保存;browser smoke 使用真实 OPFS 验证 source/dist staging。
|
||||||
|
|
||||||
|
边界说明:
|
||||||
|
|
||||||
|
```text
|
||||||
|
machineFileStagingBoundary=linuxcnc_sim_config_file_staging_plan_plus_opfs_text_persistence
|
||||||
|
stagedFiles=ini,hal,pyvcp,tool_table,remap_ngc,demo_gcode
|
||||||
|
nativeHalTaskSync=false
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 13. M15 任务
|
||||||
|
|
||||||
|
```text
|
||||||
|
M15-machine-file-backed-fiveaxis-remap-run
|
||||||
|
```
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
completed_with_node_and_browser_fiveaxis_remap_run
|
||||||
|
```
|
||||||
|
|
||||||
|
已完成:
|
||||||
|
|
||||||
|
- `linuxcnc-interpreter-runtime` 新增 `runMachineFileProgram()`;
|
||||||
|
- interpreter Worker 支持 `runMachineFileProgram` 转发;
|
||||||
|
- store 新增 `RUN_MACHINE_FILE_PROGRAM` action 和 `machineFileExecution` 状态;
|
||||||
|
- 使用 M14 的 machine-file staging plan/files,把 TRT INI、tool table、remap NGC、demo G-code 写入 Emscripten FS;
|
||||||
|
- 调用 LinuxCNC interpreter SDK `runSimConfigProgram({ executionMode: "fiveAxisRemap" })`;
|
||||||
|
- Node/browser smoke 验证 `fiveaxis_ini_open=1`、`fiveaxis_remaps_ready=1`、`fiveaxis_file_reached_exit=1`;
|
||||||
|
- gmoccapy diagnostics 显示 machine-file backed run 状态。
|
||||||
|
|
||||||
|
边界说明:
|
||||||
|
|
||||||
|
```text
|
||||||
|
machineFileBackedRunBoundary=linuxcnc_fiveaxis_remap_wasm_machine_file_execution
|
||||||
|
sourceMode=linuxcnc-machine-file-remap-wasm
|
||||||
|
remapRuntimeReady=vendored_linuxcnc_fiveaxis_remap_c_abi_for_staged_files
|
||||||
|
nativeTaskHalSync=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -173,7 +173,20 @@ LinuxCNC kinematics proof:
|
|||||||
promotionAllowed=true for kinematics frame source only
|
promotionAllowed=true for kinematics frame source only
|
||||||
```
|
```
|
||||||
|
|
||||||
这表示 Web 仿真已经具备 RTCP 状态链路、TCP pose 显示、刀轴向量显示和控制按钮切换;Node proof 路径已通过 `createLinuxCncKinematicsSdk({ moduleId: "xyzac-trt" })` 加载 LinuxCNC kinematics WASM 并生成 frame。浏览器 smoke 仍保留 fixture fallback,不把 fallback 冒充 LinuxCNC runtime proof。
|
这表示 Web 仿真已经具备 RTCP 状态链路、TCP pose 显示、刀轴向量显示和控制按钮切换;Node 和浏览器路径均已通过 `createLinuxCncKinematicsSdk({ moduleId: "xyzac-trt" })` 加载 LinuxCNC kinematics WASM 并生成 frame,浏览器默认使用 Worker 隔离 kinematics WASM 调用。fixture fallback 仍保留为 runtime load failure 的 UI 安全路径,但 smoke 不再把 fixture fallback 当作当前 proof。
|
||||||
|
|
||||||
|
当前普通 G-code 程序执行也已接入 `createLinuxCncInterpSdk()`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sourceMode=linuxcnc-interpreter-wasm
|
||||||
|
semanticBoundary=linuxcnc_interpreter_wasm_canonical_events
|
||||||
|
RUN/STEP source=LinuxCNC canonical motion events
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
```
|
||||||
|
|
||||||
|
这只提升普通 G-code canonical execution source,不代表 Python remap、tool DB、external user-M process 或完整 planner 已 promoted。
|
||||||
|
|
||||||
### Step 3:gmoccapy UI 组件
|
### Step 3:gmoccapy UI 组件
|
||||||
|
|
||||||
@@ -432,6 +445,55 @@ readiness
|
|||||||
- Browser canvas nonblank smoke;
|
- Browser canvas nonblank smoke;
|
||||||
- 文档 traceability 检查。
|
- 文档 traceability 检查。
|
||||||
|
|
||||||
|
### Step 9:LinuxCNC TP queue timing runtime
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 删除真实 G-code 路径上的
|
||||||
|
`linuxcnc_canonical_motion_timing_estimate_not_planner_queue` 边界;
|
||||||
|
- 将 interpreter 生成的 LinuxCNC canonical motion events 输入从
|
||||||
|
`linuxcnc/src/emc/tp` 移植构建出的 WASM runtime;
|
||||||
|
- 用 `tpCreate()`、`tpSetCycleTime()`、`tpSetVmax()`、`tpSetVlimit()`、
|
||||||
|
`tpSetAmax()`、`tpSetTermCond()`、`tpAddLine()`、`tpRunCycle()` 和
|
||||||
|
`tpGetPos()` 生成程序级 queue timing;
|
||||||
|
- UI 的 elapsed/remaining/current velocity 来自 TP queue runtime 输出,而不是
|
||||||
|
JavaScript 按距离和进给率估算。
|
||||||
|
|
||||||
|
实现步骤:
|
||||||
|
|
||||||
|
1. 在 `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c` 新增
|
||||||
|
`lctp_run_canonical_motion_timing()` C ABI。
|
||||||
|
2. 输入只接受 interpreter runtime 已解析出的 canonical motion JSON;C 侧只做字段读取、
|
||||||
|
TP enqueue 和 cycle loop,不解释 G-code。
|
||||||
|
3. 在 `wasm-port/runtime/sdk/src/linuxcnc-tp.js` 新增 TP SDK,负责加载
|
||||||
|
`build/wasm/tp/linuxcnc_tp.{js,wasm}` 并调用 C ABI。
|
||||||
|
4. 在 Web interpreter runtime 中可选接入 `createLinuxCncTpSdk()`;`runProgram()` 和
|
||||||
|
`runMachineFileProgram()` 完成 canonical motion 后立即跑 TP queue timing。
|
||||||
|
5. `programExecution.summary.plannerRuntimeReady=true` 只在 TP runtime 调用成功且
|
||||||
|
motionCount 一致时成立;否则保留 canonical execution,但不能把 planner timing 标为 ready。
|
||||||
|
6. `web-rtcp-5axis-full-linuxcnc-execution-boundary` 可把 `plannerRuntimeReady=true` 作为
|
||||||
|
已满足项,但仍必须显示 `nativeTaskReady=false`、`nativeHalSyncReady=false` 和
|
||||||
|
`fullLinuxCncProgramExecutionReady=false`,因为 native task/NML、HAL realtime thread 和
|
||||||
|
硬件驱动没有接入。
|
||||||
|
|
||||||
|
当前边界:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sourceMode=linuxcnc-interpreter-wasm
|
||||||
|
timing.semanticBoundary=linuxcnc_tp_queue_runtime_timing_from_canonical_motion
|
||||||
|
plannerRuntimeReady=true
|
||||||
|
nativeTaskReady=false
|
||||||
|
nativeHalSyncReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
```
|
||||||
|
|
||||||
|
不允许:
|
||||||
|
|
||||||
|
- 用 JS 重写 lookahead、blend、exact stop、S-curve 或 G-code modal 语义;
|
||||||
|
- 把 TP queue timing 说成已经驱动硬件;
|
||||||
|
- 把 native LinuxCNC task/NML/realtime HAL 说成已经完成;
|
||||||
|
- 对没有通过 TP runtime 的 operator MDI lightweight path 标记 planner ready。
|
||||||
|
|
||||||
## 5. 禁止事项
|
## 5. 禁止事项
|
||||||
|
|
||||||
- 不在 JavaScript 中实现 G-code 解释器。
|
- 不在 JavaScript 中实现 G-code 解释器。
|
||||||
@@ -439,7 +501,11 @@ readiness
|
|||||||
- 不把 gmoccapy Python/GTK runtime 移植进浏览器。
|
- 不把 gmoccapy Python/GTK runtime 移植进浏览器。
|
||||||
- 不把 Python remap/tool DB/external user-M process 伪装成已支持。
|
- 不把 Python remap/tool DB/external user-M process 伪装成已支持。
|
||||||
- 不用 UI fixture 结果冒充 LinuxCNC runtime proof。
|
- 不用 UI fixture 结果冒充 LinuxCNC runtime proof。
|
||||||
|
- 用于 5 轴 machine-file backed run 的 G-code 必须来自 LinuxCNC 源程序目录
|
||||||
|
`configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc`;
|
||||||
|
用户上传或手写的 G-code 只能作为 operator file/普通 interpreter 输入,不得标记为
|
||||||
|
`linuxcnc-vendored-5axis-gcode` 或用于 5 轴源程序边界证明。
|
||||||
|
|
||||||
## 6. 开工建议
|
## 6. 开工建议
|
||||||
|
|
||||||
当前已完成 Step 1 到 Step 7 的 Node 侧 LinuxCNC kinematics proof。下一轮应把 browser asset copy/worker 接入完成,让真实浏览器也能加载 kinematics WASM;或继续推进 interpreter/remap/planner,使 program execution 从 fixture line playback 升级。
|
当前已完成 Step 1 到 Step 9 的 Node/browser LinuxCNC kinematics proof、浏览器 Worker kinematics 隔离、浏览器 Worker interpreter canonical execution source、`xyzac-trt`/`xyzbc-trt` profile 切换、OPFS 五轴会话保存/恢复、OPFS machine-file staging、machine-file backed `fiveAxisRemap` C ABI run、程序级 `M428/M429` switchkins RTCP 自动切换、LinuxCNC TP queue timing runtime、full execution boundary audit,以及真实 LinuxCNC TRT 5 轴 `.ngc` 源程序 staging/选择/运行路径。`M428/M429/M430` 当前既可作为 Web runtime switchkins 事件驱动 `lckins_switch()`,也可在 staged machine-file run 中交给 vendored LinuxCNC five-axis remap C ABI 验证;只有 LinuxCNC source manifest 中的 `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc` 可作为 `linuxcnc-vendored-5axis-gcode` 进入 UI 和 machine-file run。`web-rtcp-5axis-full-linuxcnc-execution-boundary` 可以在真实 interpreter canonical motion 已通过 TP WASM 时报告 `plannerRuntimeReady=true`,但必须继续把 `nativeTaskReady=false`、`nativeHalSyncReady=false`、`fullLinuxCncProgramExecutionReady=false` 和 `promotionAllowed=false` 显示为 blocker,除非后续真正接入 native task/NML 和 realtime HAL sync。
|
||||||
|
|||||||
@@ -519,6 +519,7 @@ LinuxCNC G-code text
|
|||||||
8. `M7-browser-kinematics-runtime`:浏览器 asset copy/worker 加载 kinematics WASM。
|
8. `M7-browser-kinematics-runtime`:浏览器 asset copy/worker 加载 kinematics WASM。
|
||||||
9. `M8-session`:OPFS 保存/恢复五轴仿真会话。
|
9. `M8-session`:OPFS 保存/恢复五轴仿真会话。
|
||||||
10. `M9-release-gate`:Node/browser/docs/release gate。
|
10. `M9-release-gate`:Node/browser/docs/release gate。
|
||||||
|
11. `M10-profile-switching`:`xyzac-trt` / `xyzbc-trt` profile 切换并重载对应 kinematics WASM。
|
||||||
|
|
||||||
## 9. 验收标准
|
## 9. 验收标准
|
||||||
|
|
||||||
@@ -537,15 +538,10 @@ LinuxCNC G-code text
|
|||||||
|
|
||||||
## 10. 下一步动作
|
## 10. 下一步动作
|
||||||
|
|
||||||
当前已完成到 `M6-kinematics-frame-proof`。建议下一轮优先做:
|
当前已完成到 `M11-profile-session-business-closure`。建议下一轮优先做:
|
||||||
|
|
||||||
- 在 build-static 中复制 `wasm-port/build/wasm/kinematics` 所需产物,或新增 worker 隔离 kinematics WASM 加载;
|
- 把 interpreter WASM 调用迁移到 Worker,保持 UI 主线程只做渲染和状态编排;
|
||||||
- 让 `app/src/main.js` 在浏览器中 attach `xyzac-trt` kinematics runtime;
|
- 接入 OPFS machine-session 的 INI/HAL/tool table staging,让 interpreter execution 使用真实会话文件;
|
||||||
- 保持 fixture fallback,不把 browser fixture smoke 标记为 LinuxCNC proof;
|
- 继续推进 remap/planner 边界,不把普通 canonical execution 冒充完整 LinuxCNC task/motion runtime;
|
||||||
- 或转向 LinuxCNC interpreter/remap/planner 接入,把 program execution 从 fixture line playback 升级。
|
- 保持 source/browser/dist 三层 smoke:interpreter canonical frame、kinematics worker frame、DRO/preview 同步。
|
||||||
- 使用 Vite + Three.js;
|
|
||||||
- 先接入现有普通 G-code program inventory;
|
|
||||||
- 画出五轴机床基本结构;
|
|
||||||
- 按 AXIS/PyVCP/gmoccapy 参考放置 toolbar、Manual/MDI、DRO、SWITCHKINS、joint values、preview;
|
|
||||||
- 加 Playwright smoke;
|
|
||||||
- 启动本地 dev server 给出访问地址。
|
- 启动本地 dev server 给出访问地址。
|
||||||
|
|||||||
@@ -24,22 +24,26 @@ CNC 语义必须来自 LinuxCNC source/WASM/source-derived boundary;
|
|||||||
| 右侧模式按钮栏 | `app/src/ui/gmoccapy-status-sidebar.ts` | gmoccapy screenshot | UI action dispatch | browser button/action smoke |
|
| 右侧模式按钮栏 | `app/src/ui/gmoccapy-status-sidebar.ts` | gmoccapy screenshot | UI action dispatch | browser button/action smoke |
|
||||||
| 底部运行控制 | `app/src/ui/gmoccapy-bottom-controls.ts` | gmoccapy/AXIS run controls | UI action dispatch | playback smoke |
|
| 底部运行控制 | `app/src/ui/gmoccapy-bottom-controls.ts` | gmoccapy/AXIS run controls | UI action dispatch | playback smoke |
|
||||||
| G-code 当前行 | `app/src/ui/gmoccapy-gcode-panel.ts` | AXIS/gmoccapy program display | LinuxCNC output rendering | active-line smoke |
|
| G-code 当前行 | `app/src/ui/gmoccapy-gcode-panel.ts` | AXIS/gmoccapy program display | LinuxCNC output rendering | active-line smoke |
|
||||||
| 上电/急停/复位/模式操作 | `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | gmoccapy/AXIS operator workflow | browser UI runtime fixture | node smoke + browser operator smoke |
|
| 上电/急停/复位/模式操作 | `app/src/state/store.js`, `app/src/state/linuxcnc-task-policy.js`, `app/src/ui/gmoccapy-shell.js` | `src/emc/nml_intf/emc.hh`, `src/emc/task/emctaskmain.cc`, `src/emc/task/emctask.cc` | LinuxCNC task source-referenced Web policy | node smoke + browser operator smoke |
|
||||||
| JOG/MDI 操作 | `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | AXIS Manual/MDI workflow | browser UI runtime fixture | node smoke + browser operator smoke |
|
| JOG/MDI 操作 | `app/src/state/store.js`, `app/src/state/linuxcnc-task-policy.js`, `app/src/ui/gmoccapy-shell.js` | `src/emc/task/emctaskmain.cc` `EMC_JOG_INCR`, `EMC_JOINT_HOME`, `EMC_TASK_PLAN_EXECUTE` | LinuxCNC task source-referenced Web policy | node smoke + browser operator smoke |
|
||||||
| G-code 文件加载 | `app/src/ui/gmoccapy-shell.js`, `app/src/state/store.js` | AXIS/gmoccapy open program workflow | file text staging only, not LinuxCNC interpreter proof | node smoke + browser operator smoke |
|
| 程序运行/暂停/继续/停止 | `app/src/state/store.js`, `app/src/state/linuxcnc-task-policy.js`, `app/src/ui/gmoccapy-shell.js` | `src/emc/task/emctaskmain.cc` `EMC_TASK_PLAN_RUN`, `EMC_TASK_PLAN_PAUSE`, `EMC_TASK_PLAN_RESUME`, `EMC_TASK_ABORT` | LinuxCNC task source-referenced Web policy | node smoke + browser operator smoke |
|
||||||
| 程序执行当前行显示 | `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | AXIS/gmoccapy current line display | fixture line playback until LinuxCNC interpreter is connected | node smoke + browser operator smoke |
|
| G-code 文件加载 | `app/src/ui/gmoccapy-shell.js`, `app/src/state/store.js` | AXIS/gmoccapy open program workflow | browser file staging + LinuxCNC interpreter execution | node smoke + browser operator smoke |
|
||||||
|
| LinuxCNC 5 轴源程序选择 | `app/src/runtime/linuxcnc-machine-file-staging.js`, `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc` | guarded vendored LinuxCNC source-directory G-code staging + selected machine-file run | machine-file staging node smoke + browser source selector smoke |
|
||||||
|
| 程序执行当前行显示 | `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | AXIS/gmoccapy current line display | LinuxCNC interpreter canonical motion events with fixture fallback | node smoke + browser operator smoke |
|
||||||
|
| 程序执行速度/时间 | `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_tp_wasm.c`, `wasm-port/runtime/sdk/src/linuxcnc-tp.js`, `app/src/runtime/linuxcnc-interpreter-runtime.js`, `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | LinuxCNC interpreter canonical motion events queued through `src/emc/tp/tp.c`, `tc.c`, `tcq.c`, S-curve/Ruckig support sources | LinuxCNC TP queue runtime timing from canonical motion; JS estimate kept only as fallback/MDI lightweight path | TP WASM smoke + interpreter/store node smoke + browser source/dist TP timing smoke |
|
||||||
| 刀具预览 | `app/src/ui/gmoccapy-shell.js`, `app/src/visualization/five-axis-scene.js` | gmoccapy/vismach tool display | visualization/runtime state display | browser operator smoke |
|
| 刀具预览 | `app/src/ui/gmoccapy-shell.js`, `app/src/visualization/five-axis-scene.js` | gmoccapy/vismach tool display | visualization/runtime state display | browser operator smoke |
|
||||||
| 3D 五轴预览 | `app/src/visualization/five-axis-scene.js` | `qtvismach_5axis_gantry.png`, `lib/python/vismach.py` | visualization | canvas nonblank smoke |
|
| 3D 五轴预览 | `app/src/visualization/five-axis-scene.js` | `qtvismach_5axis_gantry.png`, `lib/python/vismach.py` | visualization | canvas nonblank smoke |
|
||||||
| Vismach transform tree | `app/src/visualization/machine-model.ts` | `lib/python/vismach.py`, `src/hal/user_comps/vismach/*.py` | visualization from GUI reference | scene graph smoke |
|
| Vismach transform tree | `app/src/visualization/machine-model.ts` | `lib/python/vismach.py`, `src/hal/user_comps/vismach/*.py` | visualization from GUI reference | scene graph smoke |
|
||||||
| `xyzac-trt` profile | `app/src/profiles/xyzac-trt.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | source/config reference | profile boundary node smoke |
|
| `xyzac-trt` profile | `app/src/profiles/xyzac-trt.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | source/config reference | profile boundary node smoke |
|
||||||
| `xyzac-trt` source reference map | `app/src/profiles/source-reference-map.js` | `xyzac-trt.ini`, `xyzac-trt.xml`, `switchkins_postgui.hal`, `xyzac-trt_cmds.hal`, `xyzac-trt-kins.c`, `trtfuncs.c`, `switchkins.c` | profile/source map only, not runtime proof | profile boundary node smoke |
|
| `xyzac-trt` source reference map | `app/src/profiles/source-reference-map.js` | `xyzac-trt.ini`, `xyzac-trt.xml`, `switchkins_postgui.hal`, `xyzac-trt_cmds.hal`, `xyzac-trt-kins.c`, `trtfuncs.c`, `switchkins.c` | profile/source map only, not runtime proof | profile boundary node smoke |
|
||||||
| `xyzbc-trt` profile | `app/src/profiles/xyzbc-trt.ts` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | source/config reference | profile node smoke |
|
| `xyzbc-trt` profile | `app/src/profiles/xyzbc-trt.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | source/config reference + runtime module switch | profile node smoke + browser profile switch smoke |
|
||||||
| SWITCHKINS panel | `app/src/panel-schema/xyzac-trt-pyvcp.js` | `xyzac-trt.xml`, `switchkins_postgui.hal` | UI/HAL binding reference | profile boundary node smoke + browser DOM smoke |
|
| SWITCHKINS panel | `app/src/panel-schema/xyzac-trt-pyvcp.js` | `xyzac-trt.xml`, `switchkins_postgui.hal` | UI/HAL binding reference | profile boundary node smoke + browser DOM smoke |
|
||||||
| M428/M429/M430 state | `app/src/profiles/xyzac-trt.js`, `app/src/panel-schema/xyzac-trt-pyvcp.js` | `remap_subs/428remap.ngc`, `429remap.ngc`, `430remap.ngc` | LinuxCNC remap/source reference only until runtime adapter is connected | profile boundary node smoke |
|
| M428/M429/M430 state | `app/src/profiles/xyzac-trt.js`, `app/src/panel-schema/xyzac-trt-pyvcp.js` | `remap_subs/428remap.ngc`, `429remap.ngc`, `430remap.ngc` | LinuxCNC remap/source reference only until runtime adapter is connected | profile boundary node smoke |
|
||||||
| LinuxCNC boundary adapter | `app/src/runtime/linuxcnc-boundary-adapter.js` | LinuxCNC interpreter/kinematics WASM future adapter point | adapter entrypoint only, runtime not connected | profile boundary node smoke + browser DOM smoke |
|
| LinuxCNC boundary adapter | `app/src/runtime/linuxcnc-boundary-adapter.js` | LinuxCNC interpreter/kinematics/TP WASM adapter point | kinematics + interpreter + TP timing runtime connected; native task/realtime HAL still not ported | profile boundary node smoke + browser DOM smoke |
|
||||||
|
| Full execution boundary audit | `app/src/runtime/full-execution-boundary.js`, `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | LinuxCNC kinematics WASM, interpreter WASM, TP WASM, `runSimConfigProgram({ executionMode: "fiveAxisRemap" })`, TRT remap files | machine-file remap and TP timing ready; native task/realtime HAL blocked | full execution boundary node smoke + browser DOM smoke |
|
||||||
| Five-axis kinematics | `core/linuxcnc_kinematics_wasm` | `trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c`, `5axiskins.c` | LinuxCNC source-derived WASM | Node roundtrip smoke |
|
| Five-axis kinematics | `core/linuxcnc_kinematics_wasm` | `trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c`, `5axiskins.c` | LinuxCNC source-derived WASM | Node roundtrip smoke |
|
||||||
| RTCP/TCP frame | `app/src/runtime/rtcp-frame.js` | LinuxCNC kinematics output + canonical events | fixture frame plumbing until kinematics WASM is ready | RTCP/store node smoke + browser DOM smoke |
|
| RTCP/TCP frame | `app/src/runtime/rtcp-frame.js` | LinuxCNC kinematics output + canonical events | fixture frame plumbing until kinematics WASM is ready | RTCP/store node smoke + browser DOM smoke |
|
||||||
| OPFS session | `app/src/runtime/session-*` | current `wasm-port/runtime/opfs` | host-side persistence | save/restore smoke |
|
| OPFS session | `app/src/runtime/five-axis-session.js` | current `wasm-port/runtime/opfs` | browser OPFS 5-axis session persistence | five_axis_session_smoke + browser save/restore smoke |
|
||||||
|
|
||||||
## 3. 源文件追溯清单
|
## 3. 源文件追溯清单
|
||||||
|
|
||||||
@@ -118,11 +122,12 @@ src/emc/kinematics/kins_util.c
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| gmoccapy UI style | ready | 可直接 Web 化 |
|
| gmoccapy UI style | ready | 可直接 Web 化 |
|
||||||
| Three.js preview | implemented_basic_canvas_scene | 已显示基础五轴机床、刀具/TCP marker、刀轴、刀路,并消费 `rtcpFrame` |
|
| Three.js preview | implemented_basic_canvas_scene | 已显示基础五轴机床、刀具/TCP marker、刀轴、刀路,并消费 `rtcpFrame` |
|
||||||
| LinuxCNC interpreter WASM | existing_project_capability | 可参考 `wasm-port` 现有 SDK |
|
| LinuxCNC interpreter WASM | browser_and_node_canonical_execution_ready | `createLinuxCncInterpSdk()` 已接入普通 G-code canonical motion execution;remap/planner 未 promoted |
|
||||||
| LinuxCNC 5-axis kinematics WASM | node_proof_ready | `createLinuxCncKinematicsSdk({ moduleId: "xyzac-trt" })` 已由 web adapter 加载,Node smoke 验证 forward/inverse frame |
|
| LinuxCNC 5-axis kinematics WASM | browser_and_node_proof_ready | `createLinuxCncKinematicsSdk({ moduleId: "xyzac-trt" })` 已由 web adapter 加载,Node/source-browser/dist-browser smoke 验证 forward/inverse frame |
|
||||||
| RTCP frame UI plumbing | implemented_fixture_and_kinematics_wasm | fixture fallback 仍为 `linuxCncKinematicsReady=false`;Node kinematics proof 为 `source-derived-kinematics-wasm` / `linuxcnc_kinematics_wasm_c_abi` |
|
| RTCP frame UI plumbing | implemented_fixture_and_kinematics_wasm | fixture fallback 仍为 `linuxCncKinematicsReady=false`;browser/node kinematics proof 为 `source-derived-kinematics-wasm` / `linuxcnc_kinematics_wasm_c_abi` |
|
||||||
| Operator workflow | implemented_fixture_only | 上电/急停/复位/模式/JOG/MDI/G-code 加载/当前行显示已闭环;执行仍是 fixture line playback |
|
| Operator workflow | implemented_linuxcnc_task_policy_with_canonical_motion | 上电/急停/复位/模式/JOG/MDI/RUN/PAUSE/RESUME/STOP/HOME 通过 `linuxcnc_task_state_mode_command_gate`;普通程序执行来自 LinuxCNC canonical motion |
|
||||||
| LinuxCNC boundary adapter | kinematics_runtime_connected_node | `web-rtcp-5axis-linuxcnc-boundary-adapter` 可区分 kinematics-only ready 与 interpreter/remap missing |
|
| LinuxCNC boundary adapter | kinematics_and_interpreter_connected | `web-rtcp-5axis-linuxcnc-boundary-adapter` 可区分 kinematics-only ready 与 interpreter/remap missing |
|
||||||
|
| Full execution boundary audit | implemented_remap_ready_planner_task_hal_blocked | `web-rtcp-5axis-full-linuxcnc-execution-boundary` 汇总 kinematics/interpreter/machine-file-remap evidence;仍明确 `plannerRuntimeReady=false`、`nativeTaskReady=false`、`nativeHalSyncReady=false`、`fullLinuxCncProgramExecutionReady=false` |
|
||||||
| PyVCP/HAL panel schema | implemented_reference_only | `xyzac-trt-switchkins-pyvcp` 已整理 SWITCHKINS 控件与 HAL nets;不执行 native HAL |
|
| PyVCP/HAL panel schema | implemented_reference_only | `xyzac-trt-switchkins-pyvcp` 已整理 SWITCHKINS 控件与 HAL nets;不执行 native HAL |
|
||||||
| Python GUI runtime | not_ported | 只参考,不运行 |
|
| Python GUI runtime | not_ported | 只参考,不运行 |
|
||||||
| Python remap runtime | blocked | 不在第一版实现 |
|
| Python remap runtime | blocked | 不在第一版实现 |
|
||||||
@@ -465,3 +470,548 @@ Remaining risk:
|
|||||||
Next:
|
Next:
|
||||||
browser_kinematics_wasm_asset_worker_or_interpreter_execution_source
|
browser_kinematics_wasm_asset_worker_or_interpreter_execution_source
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 13. M7 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M7-browser-kinematics-runtime
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/package.json
|
||||||
|
app/scripts/build-static.mjs
|
||||||
|
app/src/main.js
|
||||||
|
app/src/runtime/linuxcnc-kinematics-runtime.js
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
tests/browser/verify_gmoccapy_dist_browser.sh
|
||||||
|
docs/development-continuation.md
|
||||||
|
docs/program-implementation-guide.md
|
||||||
|
docs/technical-roadmap.md
|
||||||
|
docs/traceability-matrix.md
|
||||||
|
Feature:
|
||||||
|
将浏览器主流程接入 xyzac-trt LinuxCNC kinematics WASM。
|
||||||
|
runtime loader 移除顶层 node:* 依赖,支持 browser ESM import;
|
||||||
|
app/src/main.js 启动后自动 attach kinematics runtime;
|
||||||
|
build-static 复制 linuxcnc-kinematics.js 与 kinematics .js/.wasm 产物到 dist;
|
||||||
|
browser smoke 同时覆盖源码入口和 dist 入口。
|
||||||
|
LinuxCNC references:
|
||||||
|
wasm-port/runtime/sdk/src/linuxcnc-kinematics.js
|
||||||
|
wasm-port/build/wasm/kinematics/linuxcnc_xyzac_trt_kinematics.js
|
||||||
|
wasm-port/build/wasm/kinematics/linuxcnc_xyzac_trt_kinematics.wasm
|
||||||
|
src/emc/kinematics/xyzac-trt-kins.c
|
||||||
|
Boundary:
|
||||||
|
sourceMode=source-derived-kinematics-wasm
|
||||||
|
semanticBoundary=linuxcnc_kinematics_wasm_c_abi
|
||||||
|
browserKinematicsReady=true
|
||||||
|
nodeKinematicsReady=true
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
fixtureFallback=available_only_on_runtime_load_failure
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
gmoccapy_static_build=ok
|
||||||
|
linuxcnc_kinematics_runtime_smoke=ok
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
profile_boundary_smoke=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
Superseded by M8/M9: browser kinematics now runs through Worker and ordinary
|
||||||
|
G-code RUN/STEP uses LinuxCNC interpreter canonical events. Remap/planner
|
||||||
|
execution remains not promoted.
|
||||||
|
Next:
|
||||||
|
linuxcnc_interpreter_remap_planner_execution_source_or_worker_isolation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14. M8-M9 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M8-kinematics-worker-runtime
|
||||||
|
Batch: M9-interpreter-canonical-program-execution
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/package.json
|
||||||
|
app/scripts/build-static.mjs
|
||||||
|
app/src/main.js
|
||||||
|
app/src/runtime/linuxcnc-interpreter-runtime.js
|
||||||
|
app/src/runtime/linuxcnc-kinematics-runtime.js
|
||||||
|
app/src/runtime/linuxcnc-kinematics-worker.js
|
||||||
|
app/src/runtime/linuxcnc-kinematics-worker-client.js
|
||||||
|
app/src/runtime/linuxcnc-boundary-adapter.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
tests/node/verify_linuxcnc_interpreter_runtime.mjs
|
||||||
|
tests/node/verify_linuxcnc_kinematics_runtime.mjs
|
||||||
|
tests/node/verify_rtcp_store.mjs
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
Feature:
|
||||||
|
浏览器默认通过 Web Worker 加载 xyzac-trt LinuxCNC kinematics WASM;
|
||||||
|
普通 G-code 程序通过 LinuxCNC interpreter WASM 执行并输出 canonical
|
||||||
|
motion events;RUN/STEP 使用 canonical motion line/pose 更新 G-code
|
||||||
|
highlight、DRO、RTCP frame 和 Three.js preview。
|
||||||
|
LinuxCNC references:
|
||||||
|
wasm-port/runtime/sdk/src/linuxcnc-kinematics.js
|
||||||
|
wasm-port/runtime/sdk/src/linuxcnc-interp.js
|
||||||
|
wasm-port/build/wasm/kinematics/linuxcnc_xyzac_trt_kinematics.wasm
|
||||||
|
wasm-port/build/wasm/core/linuxcnc_interp.wasm
|
||||||
|
Boundary:
|
||||||
|
kinematics sourceMode=source-derived-kinematics-wasm
|
||||||
|
kinematics executionContext=worker in browser
|
||||||
|
interpreter sourceMode=linuxcnc-interpreter-wasm
|
||||||
|
interpreter semanticBoundary=linuxcnc_interpreter_wasm_canonical_events
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
gmoccapy_static_build=ok
|
||||||
|
linuxcnc_kinematics_runtime_smoke=ok
|
||||||
|
linuxcnc_interpreter_runtime_smoke=ok
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
profile_boundary_smoke=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
Interpreter runtime currently runs on the main browser thread; remap/planner,
|
||||||
|
OPFS machine-session staging, Python remap, tool DB, and external user-M process
|
||||||
|
remain not promoted.
|
||||||
|
Next:
|
||||||
|
remap_planner_worker_interpreter_or_opfs_machine_session_integration
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. M10-M11 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M10-profile-switching
|
||||||
|
Batch: M11-profile-session-business-closure
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/src/profiles/index.js
|
||||||
|
app/src/profiles/xyzbc-trt.js
|
||||||
|
app/src/profiles/source-reference-map.js
|
||||||
|
app/src/runtime/five-axis-session.js
|
||||||
|
app/src/runtime/rtcp-frame.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
app/src/styles/gmoccapy.css
|
||||||
|
tests/node/verify_five_axis_session.mjs
|
||||||
|
tests/node/verify_profile_boundary.mjs
|
||||||
|
tests/node/verify_rtcp_store.mjs
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
Feature:
|
||||||
|
增加 xyzbc-trt profile、source reference map、PyVCP SWITCHKINS schema
|
||||||
|
变体和 UI profile selector;profile 切换后重载对应 LinuxCNC kinematics WASM。
|
||||||
|
增加 web-rtcp-5axis OPFS session snapshot,可保存/恢复 profile、program、
|
||||||
|
axis/TCP pose、RTCP state、canonical execution summary、runtime readiness 和 preview state。
|
||||||
|
LinuxCNC references:
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml
|
||||||
|
src/emc/kinematics/xyzbc-trt-kins.c
|
||||||
|
wasm-port/runtime/opfs snapshot/file-service pattern
|
||||||
|
Boundary:
|
||||||
|
profile switching=source/config reference plus source-derived kinematics runtime reload
|
||||||
|
session persistence=browser OPFS UI/runtime state snapshot
|
||||||
|
machine-file staging=false
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
gmoccapy_static_build=ok
|
||||||
|
linuxcnc_kinematics_runtime_smoke=ok
|
||||||
|
linuxcnc_interpreter_runtime_smoke=ok
|
||||||
|
five_axis_session_smoke=ok
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
profile_boundary_smoke=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
Session snapshot persists Web simulation state, not full LinuxCNC machine-file
|
||||||
|
staging. Interpreter still runs on the main browser thread. Remap/planner,
|
||||||
|
Python remap, tool DB, and external user-M process remain not promoted.
|
||||||
|
Next:
|
||||||
|
interpreter_worker_or_opfs_machine_file_staging_or_remap_planner_boundary
|
||||||
|
```
|
||||||
|
|
||||||
|
## 17. M18 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M18-linuxcnc-task-policy-controls
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/src/state/linuxcnc-task-policy.js
|
||||||
|
app/src/runtime/execution-timing.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
tests/node/verify_rtcp_store.mjs
|
||||||
|
tests/node/verify_five_axis_session.mjs
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
Feature:
|
||||||
|
Adds a source-referenced LinuxCNC task policy gate for operator controls:
|
||||||
|
TOGGLE_POWER, RESET, ESTOP, SET_MODE, JOG, RUN_MDI, RUN, STEP, HOME,
|
||||||
|
PAUSE, RESUME, STOP, and ABORT. The gmoccapy diagnostics panel now exposes
|
||||||
|
task state/mode/interpreter state, gate readiness, and the source file list
|
||||||
|
including `linuxcnc/src/emc/task/emctaskmain.cc`.
|
||||||
|
Adds LinuxCNC TP queue timing from interpreter canonical motion events.
|
||||||
|
UI now reports total/elapsed/remaining time and current segment
|
||||||
|
duration/velocity from the TP WASM runtime when available.
|
||||||
|
LinuxCNC references:
|
||||||
|
src/emc/nml_intf/emc.hh
|
||||||
|
src/emc/task/emctaskmain.cc
|
||||||
|
src/emc/task/emctask.cc
|
||||||
|
Boundary:
|
||||||
|
taskPolicyBoundary=linuxcnc_task_state_mode_command_gate
|
||||||
|
timingBoundary=linuxcnc_tp_queue_runtime_timing_from_canonical_motion
|
||||||
|
sourceMode=linuxcnc-task-source-referenced-policy
|
||||||
|
nativeTaskReady=false
|
||||||
|
nativeHalSyncReady=false
|
||||||
|
plannerRuntimeReady=true
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
five_axis_session_smoke=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
The control gates are aligned to LinuxCNC task source behavior, but this is
|
||||||
|
still a Web policy mirror. The project does not run the native LinuxCNC task
|
||||||
|
process, NML IPC loop, or realtime HAL synchronization. TP timing is computed
|
||||||
|
by the vendored LinuxCNC TP queue runtime from canonical motion events; it is
|
||||||
|
still not hardware drive timing and does not make native task/HAL ready.
|
||||||
|
Next:
|
||||||
|
decide_release_handoff_or_start_native_task_hal_planner_boundary
|
||||||
|
```
|
||||||
|
|
||||||
|
## 17. M13 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M13-interpreter-worker-runtime
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/src/main.js
|
||||||
|
app/src/runtime/linuxcnc-interpreter-worker.js
|
||||||
|
app/src/runtime/linuxcnc-interpreter-worker-client.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
tests/node/verify_five_axis_session.mjs
|
||||||
|
tests/node/verify_rtcp_store.mjs
|
||||||
|
Feature:
|
||||||
|
Browser LinuxCNC interpreter WASM now loads through a Web Worker by default,
|
||||||
|
matching the existing kinematics Worker isolation. Program execution in store
|
||||||
|
accepts async interpreter runtimes, exposes interpreterExecutionPending, and
|
||||||
|
uses a sequence guard so stale Worker results cannot overwrite newer program
|
||||||
|
loads.
|
||||||
|
LinuxCNC references:
|
||||||
|
wasm-port/runtime/sdk/src/linuxcnc-interp.js
|
||||||
|
wasm-port/build/wasm/core/linuxcnc_interp.js
|
||||||
|
wasm-port/build/wasm/core/linuxcnc_interp.wasm
|
||||||
|
Boundary:
|
||||||
|
browserInterpreterExecutionContext=worker
|
||||||
|
nodeInterpreterExecutionContext=direct
|
||||||
|
interpreter semanticBoundary=linuxcnc_interpreter_wasm_canonical_events
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
linuxcnc_kinematics_runtime_smoke=ok
|
||||||
|
linuxcnc_interpreter_runtime_smoke=ok
|
||||||
|
five_axis_session_smoke=ok
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
profile_boundary_smoke=ok
|
||||||
|
gmoccapy_static_build=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
Interpreter isolation is complete for browser canonical execution, but this
|
||||||
|
still does not promote native LinuxCNC remap/planner, tool DB, task/HAL sync,
|
||||||
|
or external user-M process.
|
||||||
|
Next:
|
||||||
|
opfs_machine_file_staging_or_remap_planner_boundary
|
||||||
|
```
|
||||||
|
|
||||||
|
## 18. M14 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M14-opfs-machine-file-staging
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/package.json
|
||||||
|
app/scripts/build-static.mjs
|
||||||
|
app/src/main.js
|
||||||
|
app/src/runtime/linuxcnc-machine-file-staging.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
tests/node/verify_machine_file_staging.mjs
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
Feature:
|
||||||
|
Adds LinuxCNC TRT machine-file staging for the web simulator. The runtime
|
||||||
|
reuses `planSimConfigStaging()` to collect INI, tool table, HAL/PyVCP assets,
|
||||||
|
remap NGC files, and demo G-code from the vendored LinuxCNC tree, then persists
|
||||||
|
those text files into OPFS-style machine paths. Store exposes stageMachineFiles()
|
||||||
|
and gmoccapy diagnostics render staging status.
|
||||||
|
LinuxCNC references:
|
||||||
|
wasm-port/runtime/sdk/src/sim-config-staging.js
|
||||||
|
wasm-port/tools/source-manifest.txt
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/*.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc
|
||||||
|
Boundary:
|
||||||
|
machineFileStagingBoundary=linuxcnc_sim_config_file_staging_plan_plus_opfs_text_persistence
|
||||||
|
stagedFiles=ini,hal,pyvcp,tool_table,remap_ngc,demo_gcode
|
||||||
|
nativeHalTaskSync=false
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
machine_file_staging_smoke=ok
|
||||||
|
linuxcnc_kinematics_runtime_smoke=ok
|
||||||
|
linuxcnc_interpreter_runtime_smoke=ok
|
||||||
|
five_axis_session_smoke=ok
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
profile_boundary_smoke=ok
|
||||||
|
gmoccapy_static_build=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
Machine files are staged and persisted as browser/host text assets. This does
|
||||||
|
not yet execute a machine-file backed LinuxCNC task session, native HAL sync,
|
||||||
|
Python/NGC remap runtime, full planner, tool DB, or external user-M process.
|
||||||
|
Next:
|
||||||
|
remap_planner_boundary_or_machine_file_backed_run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 19. M15 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M15-machine-file-backed-fiveaxis-remap-run
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/src/runtime/linuxcnc-interpreter-runtime.js
|
||||||
|
app/src/runtime/linuxcnc-interpreter-worker.js
|
||||||
|
app/src/runtime/linuxcnc-interpreter-worker-client.js
|
||||||
|
app/src/runtime/linuxcnc-machine-file-staging.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
tests/node/verify_machine_file_staging.mjs
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
Feature:
|
||||||
|
Adds machine-file backed five-axis remap execution. The simulator now stages
|
||||||
|
TRT machine files, forwards the staged INI/tool table/remap/demo files into
|
||||||
|
the LinuxCNC interpreter WASM filesystem, and calls
|
||||||
|
`runSimConfigProgram({ executionMode: "fiveAxisRemap" })`. Node and browser
|
||||||
|
smokes verify LinuxCNC reports INI open, remap readiness, and program exit.
|
||||||
|
LinuxCNC references:
|
||||||
|
wasm-port/runtime/sdk/src/linuxcnc-interp.js
|
||||||
|
wasm-port/runtime/sdk/src/sim-config-staging.js
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc
|
||||||
|
Boundary:
|
||||||
|
machineFileBackedRunBoundary=linuxcnc_fiveaxis_remap_wasm_machine_file_execution
|
||||||
|
sourceMode=linuxcnc-machine-file-remap-wasm
|
||||||
|
remapRuntimeReady=vendored_linuxcnc_fiveaxis_remap_c_abi_for_staged_files
|
||||||
|
nativeTaskHalSync=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
machine_file_staging_smoke=ok
|
||||||
|
linuxcnc_interpreter_runtime_smoke=ok
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
The run uses vendored LinuxCNC five-axis remap C ABI for staged files, but
|
||||||
|
still does not promote full native task/HAL synchronization, trajectory
|
||||||
|
planner runtime, tool DB process, or external user-M process.
|
||||||
|
Next:
|
||||||
|
planner_boundary_or_full_task_hal_gap_closure
|
||||||
|
```
|
||||||
|
|
||||||
|
## 20. M16 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M16-planner-task-hal-boundary-audit
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/package.json
|
||||||
|
app/src/runtime/full-execution-boundary.js
|
||||||
|
app/src/runtime/linuxcnc-interpreter-runtime.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
tests/node/verify_full_execution_boundary.mjs
|
||||||
|
tests/node/verify_linuxcnc_interpreter_runtime.mjs
|
||||||
|
tests/node/verify_machine_file_staging.mjs
|
||||||
|
tests/node/verify_rtcp_store.mjs
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
Feature:
|
||||||
|
Adds a full LinuxCNC execution boundary audit layer. The Web simulator now
|
||||||
|
summarizes kinematics WASM, interpreter canonical motion, machine-file
|
||||||
|
staging, five-axis remap C ABI run, and switchkins HAL evidence in one
|
||||||
|
explicit diagnostics contract. It also adds an Audit control that can trigger
|
||||||
|
staging plus machine-file backed remap execution from the gmoccapy shell.
|
||||||
|
LinuxCNC references:
|
||||||
|
wasm-port/runtime/sdk/src/linuxcnc-interp.js
|
||||||
|
wasm-port/runtime/sdk/src/linuxcnc-hal.js
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc
|
||||||
|
Boundary:
|
||||||
|
fullExecutionBoundary=linuxcnc_machine_file_remap_ready_planner_task_hal_blocked
|
||||||
|
remapRuntimeReady=true for staged fiveAxisRemap C ABI run
|
||||||
|
halSwitchkinsEvidenceReady=true for fiveaxis_hal_switchkins evidence
|
||||||
|
nativeTaskReady=false
|
||||||
|
nativeHalSyncReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
promotionAllowed=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
full_execution_boundary_smoke=ok
|
||||||
|
machine_file_staging_smoke=ok
|
||||||
|
linuxcnc_interpreter_runtime_smoke=ok
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
Full native LinuxCNC task/NML process, realtime HAL thread synchronization,
|
||||||
|
trajectory planner queue runtime, external user-M process, and full tool DB
|
||||||
|
process are still not ported or promoted.
|
||||||
|
Next:
|
||||||
|
optional_native_task_hal_planner_port_or_release_handoff
|
||||||
|
```
|
||||||
|
|
||||||
|
## 21. M17 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M17-linuxcnc-5axis-gcode-source-ingest
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/src/runtime/linuxcnc-machine-file-staging.js
|
||||||
|
app/src/runtime/linuxcnc-interpreter-runtime.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
tests/node/verify_machine_file_staging.mjs
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
Feature:
|
||||||
|
Adds real vendored LinuxCNC five-axis G-code source ingestion. TRT demo
|
||||||
|
programs from `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc`
|
||||||
|
are staged, listed in the gmoccapy G-code panel, loadable into the program
|
||||||
|
viewer, and used as the selected `wasmProgramPath` for machine-file backed
|
||||||
|
fiveAxisRemap execution.
|
||||||
|
LinuxCNC references:
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc
|
||||||
|
Boundary:
|
||||||
|
gcodeSourceBoundary=linuxcnc_vendored_5axis_gcode_source_file
|
||||||
|
sourceMode=linuxcnc-vendored-5axis-gcode
|
||||||
|
machineFileBackedRunProgram=selected_staged_linuxcnc_ngc_source
|
||||||
|
sourceDirectoryGuard=configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc only
|
||||||
|
remapRuntimeReady=true for staged fiveAxisRemap C ABI run
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
nativeTaskReady=false
|
||||||
|
nativeHalSyncReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
machine_file_staging_smoke=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
The selected G-code source is real LinuxCNC vendored source and the machine-file
|
||||||
|
run uses that selected file path, but full native task/NML, realtime HAL sync,
|
||||||
|
and trajectory planner queue runtime are still not promoted.
|
||||||
|
Next:
|
||||||
|
trajectory_planner_wasm_boundary_or_native_task_hal_port
|
||||||
|
```
|
||||||
|
|
||||||
|
## 16. M12 追溯记录
|
||||||
|
|
||||||
|
```text
|
||||||
|
Batch: M12-switchkins-rtcp-program-execution
|
||||||
|
Date: 2026-06-21 CST
|
||||||
|
Files changed:
|
||||||
|
app/src/runtime/linuxcnc-interpreter-runtime.js
|
||||||
|
app/src/runtime/linuxcnc-kinematics-runtime.js
|
||||||
|
app/src/runtime/linuxcnc-kinematics-worker.js
|
||||||
|
app/src/runtime/linuxcnc-kinematics-worker-client.js
|
||||||
|
app/src/state/store.js
|
||||||
|
app/src/ui/gmoccapy-shell.js
|
||||||
|
tests/node/verify_linuxcnc_interpreter_runtime.mjs
|
||||||
|
tests/node/verify_rtcp_store.mjs
|
||||||
|
tests/browser/gmoccapy_shell_smoke.html
|
||||||
|
Feature:
|
||||||
|
LinuxCNC TRT `M428/M429/M430` switchkins remap M-code is preserved as
|
||||||
|
program-level runtime events. The interpreter runtime strips those high M-codes
|
||||||
|
only from the WASM input program so LinuxCNC interpreter canonical motion can
|
||||||
|
continue, then annotates canonical motion with active switchkins state.
|
||||||
|
RUN/STEP/RUN_FRAME now applies program switchkins state to RTCP state and calls
|
||||||
|
LinuxCNC kinematics WASM `lckins_switch()` through direct and Worker runtimes.
|
||||||
|
LinuxCNC references:
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc
|
||||||
|
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc
|
||||||
|
src/emc/kinematics/switchkins.c
|
||||||
|
wasm-port/runtime/sdk/src/linuxcnc-kinematics.js
|
||||||
|
Boundary:
|
||||||
|
switchkinsProgramBoundary=linuxcnc_remap_mcode_preserved_and_web_runtime_applied
|
||||||
|
interpreter sourceMode=linuxcnc-interpreter-wasm
|
||||||
|
interpreter semanticBoundary=linuxcnc_interpreter_wasm_canonical_events
|
||||||
|
kinematics semanticBoundary=linuxcnc_kinematics_wasm_c_abi
|
||||||
|
remapRuntimeReady=false
|
||||||
|
plannerRuntimeReady=false
|
||||||
|
fullLinuxCncProgramExecutionReady=false
|
||||||
|
Tests:
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||||
|
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||||
|
Result:
|
||||||
|
linuxcnc_kinematics_runtime_smoke=ok
|
||||||
|
linuxcnc_interpreter_runtime_smoke=ok
|
||||||
|
five_axis_session_smoke=ok
|
||||||
|
rtcp_store_smoke=ok
|
||||||
|
profile_boundary_smoke=ok
|
||||||
|
gmoccapy_static_build=ok
|
||||||
|
gmoccapy_shell_smoke=ok
|
||||||
|
gmoccapy_dist_smoke=ok
|
||||||
|
Remaining risk:
|
||||||
|
This preserves and applies switchkins remap M-codes in Web runtime, but does not
|
||||||
|
execute LinuxCNC Python/NGC remap, native HAL/task synchronization, full planner,
|
||||||
|
tool DB, or external user-M process.
|
||||||
|
Next:
|
||||||
|
interpreter_worker_or_opfs_machine_file_staging_or_remap_planner_boundary
|
||||||
|
```
|
||||||
|
|||||||
36
web-rtcp-5axis-sim-plan/open-local.sh
Executable file
36
web-rtcp-5axis-sim-plan/open-local.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_DIR="/home/cnc/桌面/cnc_wams/web-rtcp-5axis-sim-plan/app/dist"
|
||||||
|
PORT="8092"
|
||||||
|
URL="http://127.0.0.1:${PORT}/"
|
||||||
|
LOG_DIR="${HOME}/.cache/web-rtcp-5axis-sim"
|
||||||
|
LOG_FILE="${LOG_DIR}/local-server.log"
|
||||||
|
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
is_serving() {
|
||||||
|
curl -fsS --max-time 1 "$URL" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! is_serving; then
|
||||||
|
nohup python3 -m http.server "$PORT" --bind 127.0.0.1 --directory "$APP_DIR" >"$LOG_FILE" 2>&1 &
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
if is_serving; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v google-chrome-stable >/dev/null 2>&1; then
|
||||||
|
exec google-chrome-stable --new-window "$URL"
|
||||||
|
elif command -v google-chrome >/dev/null 2>&1; then
|
||||||
|
exec google-chrome --new-window "$URL"
|
||||||
|
elif command -v chromium >/dev/null 2>&1; then
|
||||||
|
exec chromium --new-window "$URL"
|
||||||
|
elif command -v chromium-browser >/dev/null 2>&1; then
|
||||||
|
exec chromium-browser --new-window "$URL"
|
||||||
|
else
|
||||||
|
exec xdg-open "$URL"
|
||||||
|
fi
|
||||||
@@ -6,12 +6,34 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<pre id="result">gmoccapy_shell_smoke=pending</pre>
|
<pre id="result">gmoccapy_shell_smoke=pending</pre>
|
||||||
<iframe id="app-frame" src="../../app/index.html" title="gmoccapy shell"></iframe>
|
<iframe id="app-frame" title="gmoccapy shell"></iframe>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const result = document.querySelector("#result");
|
const result = document.querySelector("#result");
|
||||||
const frame = document.querySelector("#app-frame");
|
const frame = document.querySelector("#app-frame");
|
||||||
|
const appSrc = new URLSearchParams(window.location.search).get("app") || "../../app/index.html";
|
||||||
|
frame.src = appSrc;
|
||||||
|
|
||||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
async function waitForInterpreterExecution(win) {
|
||||||
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||||
|
const state = win.webRtcp5AxisSimulation.getState();
|
||||||
|
if (!state.interpreterExecutionPending && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm") {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
await wait(25);
|
||||||
|
}
|
||||||
|
return win.webRtcp5AxisSimulation.getState();
|
||||||
|
}
|
||||||
|
async function waitForMachineFileExecution(win) {
|
||||||
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
||||||
|
const state = win.webRtcp5AxisSimulation.getState();
|
||||||
|
if (!state.interpreterExecutionPending && state.machineFileExecution) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
await wait(25);
|
||||||
|
}
|
||||||
|
return win.webRtcp5AxisSimulation.getState();
|
||||||
|
}
|
||||||
|
|
||||||
async function runSmoke() {
|
async function runSmoke() {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
@@ -89,25 +111,84 @@
|
|||||||
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
|
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runtimeReadiness = await win.webRtcp5AxisSimulation.kinematicsRuntimeReady;
|
||||||
|
if (runtimeReadiness.loaded !== true || runtimeReadiness.moduleId !== "xyzac-trt") {
|
||||||
|
throw new Error(`LinuxCNC kinematics runtime did not load in browser: ${JSON.stringify(runtimeReadiness)}`);
|
||||||
|
}
|
||||||
|
if (runtimeReadiness.executionContext !== "worker") {
|
||||||
|
throw new Error(`LinuxCNC kinematics runtime should run in worker: ${JSON.stringify(runtimeReadiness)}`);
|
||||||
|
}
|
||||||
|
const interpreterReadiness = await win.webRtcp5AxisSimulation.interpreterRuntimeReady;
|
||||||
|
if (interpreterReadiness.loaded !== true || interpreterReadiness.runProgramReady !== true) {
|
||||||
|
throw new Error(`LinuxCNC interpreter runtime did not load in browser: ${JSON.stringify(interpreterReadiness)}`);
|
||||||
|
}
|
||||||
|
if (interpreterReadiness.executionContext !== "worker") {
|
||||||
|
throw new Error(`LinuxCNC interpreter runtime should run in worker: ${JSON.stringify(interpreterReadiness)}`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
interpreterReadiness.plannerRuntimeReady !== true ||
|
||||||
|
interpreterReadiness.plannerSemanticBoundary !== "linuxcnc_tp_queue_runtime_timing_from_canonical_motion"
|
||||||
|
) {
|
||||||
|
throw new Error(`LinuxCNC TP planner runtime did not load in browser worker: ${JSON.stringify(interpreterReadiness)}`);
|
||||||
|
}
|
||||||
const state = win.webRtcp5AxisSimulation.getState();
|
const state = win.webRtcp5AxisSimulation.getState();
|
||||||
if (state.sourceMode !== "fixture-ui-only") {
|
if (state.sourceMode !== "source-derived-kinematics-wasm") {
|
||||||
throw new Error(`unexpected source mode: ${state.sourceMode}`);
|
throw new Error(`unexpected source mode: ${state.sourceMode}`);
|
||||||
}
|
}
|
||||||
if (state.machineProfile !== "xyzac-trt") {
|
if (state.kinematicsExecutionContext !== "worker") {
|
||||||
throw new Error(`unexpected profile: ${state.machineProfile}`);
|
throw new Error(`unexpected kinematics execution context: ${state.kinematicsExecutionContext}`);
|
||||||
|
}
|
||||||
|
const profileSelector = doc.querySelector('[data-action="select-profile"]');
|
||||||
|
if (!profileSelector || profileSelector.options.length < 2) {
|
||||||
|
throw new Error("missing five-axis profile selector");
|
||||||
|
}
|
||||||
|
profileSelector.value = "xyzbc-trt";
|
||||||
|
profileSelector.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
await wait(500);
|
||||||
|
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke refreshed xyzbc frame" });
|
||||||
|
const xyzbcState = win.webRtcp5AxisSimulation.getState();
|
||||||
|
if (xyzbcState.machineProfile !== "xyzbc-trt" || xyzbcState.profile.traj.coordinates !== "XYZBC") {
|
||||||
|
throw new Error("profile selector did not switch to XYZBC");
|
||||||
|
}
|
||||||
|
if (xyzbcState.kinematicsRuntimeReadiness?.moduleId !== "xyzbc-trt") {
|
||||||
|
throw new Error(`XYZBC kinematics runtime did not load: ${JSON.stringify(xyzbcState.kinematicsRuntimeReadiness)}`);
|
||||||
|
}
|
||||||
|
const restoredProfileSelector = doc.querySelector('[data-action="select-profile"]');
|
||||||
|
restoredProfileSelector.value = "xyzac-trt";
|
||||||
|
restoredProfileSelector.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
await wait(500);
|
||||||
|
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke restored xyzac frame" });
|
||||||
|
const restoredProfileState = win.webRtcp5AxisSimulation.getState();
|
||||||
|
if (restoredProfileState.machineProfile !== "xyzac-trt") {
|
||||||
|
throw new Error(`unexpected profile: ${restoredProfileState.machineProfile}`);
|
||||||
}
|
}
|
||||||
if (state.rtcpFrame?.apiName !== "web-rtcp-5axis-motion-frame") {
|
if (state.rtcpFrame?.apiName !== "web-rtcp-5axis-motion-frame") {
|
||||||
throw new Error("missing RTCP frame state");
|
throw new Error("missing RTCP frame state");
|
||||||
}
|
}
|
||||||
if (state.rtcpFrame?.readiness?.linuxCncKinematicsReady !== false) {
|
if (state.rtcpFrame?.readiness?.linuxCncKinematicsReady !== true) {
|
||||||
throw new Error("fixture RTCP frame must not claim LinuxCNC kinematics readiness");
|
throw new Error("browser RTCP frame must use LinuxCNC kinematics readiness");
|
||||||
}
|
}
|
||||||
if (!doc.querySelector('[data-rtcp-value="tcp"]')?.textContent.includes("TCP")) {
|
if (!doc.querySelector('[data-rtcp-value="tcp"]')?.textContent.includes("TCP")) {
|
||||||
throw new Error("missing TCP DRO strip");
|
throw new Error("missing TCP DRO strip");
|
||||||
}
|
}
|
||||||
if (!doc.querySelector('[data-rtcp-diagnostic="boundary"]')?.textContent.includes("fixture_frame_ui_plumbing")) {
|
if (!doc.querySelector('[data-rtcp-diagnostic="boundary"]')?.textContent.includes("linuxcnc_kinematics_wasm_c_abi")) {
|
||||||
throw new Error("missing RTCP boundary diagnostic");
|
throw new Error("missing RTCP boundary diagnostic");
|
||||||
}
|
}
|
||||||
|
if (!doc.querySelector('[data-linuxcnc-ini="status"]')?.textContent.includes("xyzac-trt.ini")) {
|
||||||
|
throw new Error("missing LinuxCNC INI status diagnostic");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-linuxcnc-ini="kins"]')?.textContent.includes("xyzac-trt-kins")) {
|
||||||
|
throw new Error("missing LinuxCNC INI kinematics diagnostic");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-linuxcnc-ini="limits"]')?.textContent.includes("X[-200,200]")) {
|
||||||
|
throw new Error("missing LinuxCNC INI axis limits diagnostic");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-linuxcnc-task-policy="boundary"]')?.textContent.includes("linuxcnc_task_state_mode_command_gate")) {
|
||||||
|
throw new Error("missing LinuxCNC task policy boundary diagnostic");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-linuxcnc-task-policy="source"]')?.textContent.includes("linuxcnc/src/emc/task/emctaskmain.cc")) {
|
||||||
|
throw new Error("missing LinuxCNC emctaskmain source diagnostic");
|
||||||
|
}
|
||||||
if (!doc.querySelector('[data-tool-preview="summary"]')?.textContent.includes("T1")) {
|
if (!doc.querySelector('[data-tool-preview="summary"]')?.textContent.includes("T1")) {
|
||||||
throw new Error("missing tool preview summary");
|
throw new Error("missing tool preview summary");
|
||||||
}
|
}
|
||||||
@@ -131,11 +212,19 @@
|
|||||||
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power on")) {
|
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power on")) {
|
||||||
throw new Error("machine state did not render power on");
|
throw new Error("machine state did not render power on");
|
||||||
}
|
}
|
||||||
|
doc.querySelector('[data-action="HOME"]').click();
|
||||||
|
await wait(50);
|
||||||
|
doc.querySelector('[data-action="mode-auto"]').click();
|
||||||
|
await wait(50);
|
||||||
|
if (!doc.querySelector('[data-linuxcnc-task-policy="gates"]')?.textContent.includes("auto")) {
|
||||||
|
throw new Error("LinuxCNC task policy did not expose auto run gate");
|
||||||
|
}
|
||||||
|
|
||||||
win.webRtcp5AxisSimulation.dispatch({
|
win.webRtcp5AxisSimulation.dispatch({
|
||||||
type: "LOAD_PROGRAM",
|
type: "LOAD_PROGRAM",
|
||||||
filename: "operator-demo.ngc",
|
filename: "operator-demo.ngc",
|
||||||
content: [
|
content: [
|
||||||
|
"G90 G17",
|
||||||
"G0 X0 Y0 Z0",
|
"G0 X0 Y0 Z0",
|
||||||
"G1 X10 F100",
|
"G1 X10 F100",
|
||||||
"G1 Y10",
|
"G1 Y10",
|
||||||
@@ -143,18 +232,154 @@
|
|||||||
"G1 Y0",
|
"G1 Y0",
|
||||||
"G0 Z5",
|
"G0 Z5",
|
||||||
"M5",
|
"M5",
|
||||||
"M30",
|
"M2",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
});
|
});
|
||||||
await wait(50);
|
await waitForInterpreterExecution(win);
|
||||||
if (win.webRtcp5AxisSimulation.getState().activeProgram !== "operator-demo.ngc") {
|
if (win.webRtcp5AxisSimulation.getState().activeProgram !== "operator-demo.ngc") {
|
||||||
throw new Error("LOAD_PROGRAM did not update active program");
|
throw new Error("LOAD_PROGRAM did not update active program");
|
||||||
}
|
}
|
||||||
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 1") {
|
if (win.webRtcp5AxisSimulation.getState().programExecutionSourceMode !== "linuxcnc-interpreter-wasm") {
|
||||||
throw new Error("loaded program did not render current line 1");
|
throw new Error("LOAD_PROGRAM did not execute through LinuxCNC interpreter WASM");
|
||||||
}
|
}
|
||||||
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "1") {
|
if (win.webRtcp5AxisSimulation.getState().programExecution?.summary?.motionEventCount < 5) {
|
||||||
throw new Error("loaded program active row should be line 1");
|
throw new Error("LinuxCNC interpreter execution did not produce motion events");
|
||||||
|
}
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().programExecution?.summary?.plannerRuntimeReady !== true) {
|
||||||
|
throw new Error("LinuxCNC TP planner timing was not ready for operator program");
|
||||||
|
}
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().programExecutionTiming?.semanticBoundary !== "linuxcnc_tp_queue_runtime_timing_from_canonical_motion") {
|
||||||
|
throw new Error(`program timing did not use LinuxCNC TP queue runtime: ${JSON.stringify(win.webRtcp5AxisSimulation.getState().programExecutionTiming)}`);
|
||||||
|
}
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().programExecutionTiming?.sampleCount < 2) {
|
||||||
|
throw new Error("LinuxCNC TP runtime feedback samples were not exposed");
|
||||||
|
}
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.semanticBoundary !== "linuxcnc_tp_run_cycle_feedback_without_hardware") {
|
||||||
|
throw new Error(`initial runtime feedback did not use LinuxCNC TP sample: ${JSON.stringify(win.webRtcp5AxisSimulation.getState().programRuntimeFeedback)}`);
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-program-execution-source]')?.textContent.includes("linuxcnc-interpreter-wasm")) {
|
||||||
|
throw new Error("program execution source DOM did not render interpreter source");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-program-execution-summary]')?.textContent.includes("motion")) {
|
||||||
|
throw new Error("program execution summary DOM did not render motion count");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-program-timing="summary"]')?.textContent.match(/\\d+:\\d/)) {
|
||||||
|
throw new Error("program timing summary DOM did not render estimated execution time");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-program-timing="segment"]')?.textContent.includes("mm/min")) {
|
||||||
|
throw new Error("program timing segment DOM did not render segment velocity");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-program-runtime-feedback="source"]')?.textContent.includes("linuxcnc-tp-runtime-sample")) {
|
||||||
|
throw new Error("program runtime feedback DOM did not render LinuxCNC TP sample source");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-program-runtime-feedback="dtg"]')?.textContent.includes("DTG")) {
|
||||||
|
throw new Error("program runtime feedback DOM did not render DTG");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-program-switchkins-summary]')?.textContent.includes("0 events")) {
|
||||||
|
throw new Error("program switchkins summary DOM did not render zero-event state");
|
||||||
|
}
|
||||||
|
win.webRtcp5AxisSimulation.dispatch({
|
||||||
|
type: "LOAD_PROGRAM",
|
||||||
|
filename: "operator-arc-demo.ngc",
|
||||||
|
content: [
|
||||||
|
"G90 G17",
|
||||||
|
"G0 X1 Y0 Z0",
|
||||||
|
"G2 X0 Y1 I-1 J0 F60",
|
||||||
|
"M2",
|
||||||
|
].join("\n"),
|
||||||
|
});
|
||||||
|
await waitForInterpreterExecution(win);
|
||||||
|
const arcState = win.webRtcp5AxisSimulation.getState();
|
||||||
|
if (!arcState.programExecution?.summary?.motionTypes?.includes("ARC_FEED")) {
|
||||||
|
throw new Error("LinuxCNC interpreter did not produce ARC_FEED for browser arc program");
|
||||||
|
}
|
||||||
|
if (arcState.programExecution?.summary?.plannerRuntimeReady !== true) {
|
||||||
|
throw new Error("LinuxCNC TP planner timing was not ready for browser arc program");
|
||||||
|
}
|
||||||
|
const arcSegment = arcState.programExecutionTiming?.segments?.find((segment) => segment.type === "ARC_FEED");
|
||||||
|
if (!arcSegment || arcSegment.durationSeconds <= 1.4) {
|
||||||
|
throw new Error(`LinuxCNC TP arc timing segment missing or too short: ${JSON.stringify(arcState.programExecutionTiming)}`);
|
||||||
|
}
|
||||||
|
const stagedMachineFiles = await win.webRtcp5AxisSimulation.stageMachineFiles();
|
||||||
|
await wait(50);
|
||||||
|
if (stagedMachineFiles.save.status !== "saved" || stagedMachineFiles.save.fileCount < 5) {
|
||||||
|
throw new Error(`machine file staging did not save files: ${JSON.stringify(stagedMachineFiles.save)}`);
|
||||||
|
}
|
||||||
|
if (!stagedMachineFiles.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc"))) {
|
||||||
|
throw new Error("machine file staging did not include M428 remap");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
|
||||||
|
throw new Error("machine file staging status did not render");
|
||||||
|
}
|
||||||
|
const sourceSelect = doc.querySelector('[data-action="select-linuxcnc-gcode-source"]');
|
||||||
|
if (!sourceSelect || sourceSelect.options.length < 4) {
|
||||||
|
throw new Error("LinuxCNC 5-axis G-code source selector did not render staged demos");
|
||||||
|
}
|
||||||
|
const impellerSource = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc";
|
||||||
|
sourceSelect.value = impellerSource;
|
||||||
|
sourceSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
await waitForInterpreterExecution(win);
|
||||||
|
const linuxCncSourceState = win.webRtcp5AxisSimulation.getState();
|
||||||
|
if (linuxCncSourceState.programSource !== "linuxcnc-vendored-5axis-gcode") {
|
||||||
|
throw new Error(`LinuxCNC source program was not loaded: ${linuxCncSourceState.programSource}`);
|
||||||
|
}
|
||||||
|
if (!linuxCncSourceState.activeProgram.endsWith("impeller-7bl-xyzac.ngc")) {
|
||||||
|
throw new Error(`unexpected LinuxCNC source program: ${linuxCncSourceState.activeProgram}`);
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-linuxcnc-gcode-source="selected"]')?.textContent.includes("impeller-7bl-xyzac.ngc")) {
|
||||||
|
throw new Error("selected LinuxCNC G-code source did not render");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-program-source]')?.textContent.includes("linuxcnc-vendored-5axis-gcode")) {
|
||||||
|
throw new Error("program source DOM did not show LinuxCNC vendored source");
|
||||||
|
}
|
||||||
|
win.webRtcp5AxisSimulation.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
||||||
|
const machineFileRunState = await waitForMachineFileExecution(win);
|
||||||
|
if (machineFileRunState.machineFileExecution?.summary?.machineFileExecutionReady !== true) {
|
||||||
|
throw new Error(`machine-file backed remap run did not complete: ${JSON.stringify(machineFileRunState.machineFileExecution?.summary)}`);
|
||||||
|
}
|
||||||
|
if (!machineFileRunState.machineFileExecution.resultText.includes("fiveaxis_file_reached_exit=1")) {
|
||||||
|
throw new Error("machine-file backed remap run did not reach exit");
|
||||||
|
}
|
||||||
|
if (!machineFileRunState.machineFileExecution.machineFilePlan?.selectedProgramFilename?.includes("impeller-7bl-xyzac.ngc")) {
|
||||||
|
throw new Error("machine-file backed remap run did not use selected LinuxCNC G-code source");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-machine-file-execution="status"]')?.textContent.includes("ready")) {
|
||||||
|
throw new Error("machine-file execution status did not render");
|
||||||
|
}
|
||||||
|
if (machineFileRunState.fullExecutionBoundary?.machineFileBackedRemapReady !== true) {
|
||||||
|
throw new Error(`full execution boundary did not record remap readiness: ${JSON.stringify(machineFileRunState.fullExecutionBoundary)}`);
|
||||||
|
}
|
||||||
|
if (machineFileRunState.fullExecutionBoundary?.fullLinuxCncProgramExecutionReady !== false) {
|
||||||
|
throw new Error("full LinuxCNC program execution must remain blocked without native task/planner/HAL");
|
||||||
|
}
|
||||||
|
if (!machineFileRunState.fullExecutionBoundary.blockers.some((blocker) => blocker.includes("trajectory planner"))) {
|
||||||
|
throw new Error("full execution boundary did not expose planner blocker");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("remap ready")) {
|
||||||
|
throw new Error("full execution boundary status did not render remap readiness");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("full blocked")) {
|
||||||
|
throw new Error("full execution boundary status did not render full blocked state");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-full-execution-boundary="blockers"]')?.textContent.includes("native LinuxCNC task")) {
|
||||||
|
throw new Error("full execution blocker diagnostic did not render native task blocker");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-full-execution-boundary="evidence"]')?.textContent.includes("linuxcnc_machine_file_remap_ready_planner_task_hal_blocked")) {
|
||||||
|
throw new Error("full execution evidence diagnostic did not render boundary semantic");
|
||||||
|
}
|
||||||
|
const savedSession = await win.webRtcp5AxisSimulation.saveSession();
|
||||||
|
await wait(50);
|
||||||
|
if (savedSession.snapshot.format !== "web-rtcp-5axis-session-snapshot") {
|
||||||
|
throw new Error("saveSession did not write a five-axis session snapshot");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("saved")) {
|
||||||
|
throw new Error("session save status did not render");
|
||||||
|
}
|
||||||
|
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 2") {
|
||||||
|
throw new Error("loaded program did not render first LinuxCNC motion line");
|
||||||
|
}
|
||||||
|
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "2") {
|
||||||
|
throw new Error("loaded program active row should be first LinuxCNC motion line");
|
||||||
}
|
}
|
||||||
|
|
||||||
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
|
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
|
||||||
@@ -162,8 +387,29 @@
|
|||||||
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
||||||
throw new Error("RUN action did not update state after power on");
|
throw new Error("RUN action did not update state after power on");
|
||||||
}
|
}
|
||||||
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "6") {
|
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.sourceMode !== "linuxcnc-tp-runtime-sample") {
|
||||||
throw new Error("RUN did not highlight the executing current line");
|
throw new Error("RUN did not advance with LinuxCNC TP runtime feedback");
|
||||||
|
}
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().feed.currentVelocity <= 0) {
|
||||||
|
throw new Error("RUN did not expose LinuxCNC TP current velocity");
|
||||||
|
}
|
||||||
|
if (Number(doc.querySelector(".gcode-row.active")?.dataset.programLine || 0) < 2) {
|
||||||
|
throw new Error("RUN did not highlight a LinuxCNC canonical motion line");
|
||||||
|
}
|
||||||
|
doc.querySelector('[data-action="PAUSE"]').click();
|
||||||
|
await wait(50);
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().runState !== "paused") {
|
||||||
|
throw new Error("PAUSE action did not update state");
|
||||||
|
}
|
||||||
|
doc.querySelector('[data-action="RUN"]').click();
|
||||||
|
await wait(50);
|
||||||
|
if (!win.webRtcp5AxisSimulation.getState().operatorMessage.includes("resume paused program")) {
|
||||||
|
throw new Error("RUN should be blocked while program is paused");
|
||||||
|
}
|
||||||
|
doc.querySelector('[data-action="RESUME"]').click();
|
||||||
|
await wait(50);
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
||||||
|
throw new Error("RESUME action did not restore running state");
|
||||||
}
|
}
|
||||||
const tcpButton = doc.querySelector('[data-action="kins-tcp"]');
|
const tcpButton = doc.querySelector('[data-action="kins-tcp"]');
|
||||||
tcpButton.click();
|
tcpButton.click();
|
||||||
@@ -181,8 +427,11 @@
|
|||||||
if (canvas.dataset.threeRtcpState !== "on") {
|
if (canvas.dataset.threeRtcpState !== "on") {
|
||||||
throw new Error("Three.js preview did not consume RTCP enabled frame");
|
throw new Error("Three.js preview did not consume RTCP enabled frame");
|
||||||
}
|
}
|
||||||
if (doc.querySelector('[data-rtcp-diagnostic="kinematics-ready"]')?.textContent !== "pending") {
|
if (doc.querySelector('[data-rtcp-diagnostic="kinematics-ready"]')?.textContent !== "ready") {
|
||||||
throw new Error("RTCP diagnostics must keep LinuxCNC kinematics pending for fixture mode");
|
throw new Error("RTCP diagnostics must show LinuxCNC kinematics ready");
|
||||||
|
}
|
||||||
|
if (doc.querySelector('[data-rtcp-diagnostic="execution-context"]')?.textContent !== "worker") {
|
||||||
|
throw new Error("RTCP diagnostics must show worker kinematics context");
|
||||||
}
|
}
|
||||||
if (!doc.querySelector('[data-linuxcnc-boundary="adapter"]')?.textContent.includes("linuxcnc-boundary-adapter")) {
|
if (!doc.querySelector('[data-linuxcnc-boundary="adapter"]')?.textContent.includes("linuxcnc-boundary-adapter")) {
|
||||||
throw new Error("missing LinuxCNC boundary adapter diagnostic");
|
throw new Error("missing LinuxCNC boundary adapter diagnostic");
|
||||||
@@ -193,12 +442,19 @@
|
|||||||
if (!doc.querySelector('[data-linuxcnc-boundary="profile-summary"]')?.textContent.includes("XYZAC / 5 joints / 10 tools")) {
|
if (!doc.querySelector('[data-linuxcnc-boundary="profile-summary"]')?.textContent.includes("XYZAC / 5 joints / 10 tools")) {
|
||||||
throw new Error("missing LinuxCNC profile summary diagnostic");
|
throw new Error("missing LinuxCNC profile summary diagnostic");
|
||||||
}
|
}
|
||||||
if (!doc.querySelector('[data-linuxcnc-boundary="readiness"]')?.textContent.includes("blocked")) {
|
if (!doc.querySelector('[data-linuxcnc-boundary="readiness"]')?.textContent.includes("ready")) {
|
||||||
throw new Error("LinuxCNC boundary readiness should remain blocked");
|
throw new Error("LinuxCNC boundary readiness should show kinematics ready");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-linuxcnc-boundary="interpreter"]')?.textContent.includes("linuxcnc_interpreter_wasm_canonical_events")) {
|
||||||
|
throw new Error("missing LinuxCNC interpreter boundary diagnostic");
|
||||||
|
}
|
||||||
|
if (doc.querySelector('[data-linuxcnc-boundary="interpreter-context"]')?.textContent !== "worker") {
|
||||||
|
throw new Error("LinuxCNC interpreter diagnostics must show worker context");
|
||||||
}
|
}
|
||||||
canvas = doc.querySelector("[data-five-axis-canvas]");
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||||
const tcpPoseBeforeStep = canvas.dataset.threeTcpPose;
|
const tcpPoseBeforeStep = canvas.dataset.threeTcpPose;
|
||||||
win.webRtcp5AxisSimulation.dispatch({ type: "STEP" });
|
win.webRtcp5AxisSimulation.dispatch({ type: "STEP" });
|
||||||
|
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke refreshed worker frame" });
|
||||||
await wait(50);
|
await wait(50);
|
||||||
if (win.webRtcp5AxisSimulation.getState().activeLine <= rtcpState.activeLine) {
|
if (win.webRtcp5AxisSimulation.getState().activeLine <= rtcpState.activeLine) {
|
||||||
throw new Error("STEP did not advance RTCP frame line");
|
throw new Error("STEP did not advance RTCP frame line");
|
||||||
@@ -208,6 +464,13 @@
|
|||||||
throw new Error("Three.js preview did not update TCP pose after STEP");
|
throw new Error("Three.js preview did not update TCP pose after STEP");
|
||||||
}
|
}
|
||||||
assertCanvasNonblank(canvas, "updated Three.js preview");
|
assertCanvasNonblank(canvas, "updated Three.js preview");
|
||||||
|
doc.querySelector('[data-action="STOP"]').click();
|
||||||
|
await wait(50);
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().machine.interpState !== "idle") {
|
||||||
|
throw new Error("STOP did not clear interpreter state");
|
||||||
|
}
|
||||||
|
doc.querySelector('[data-action="mode-manual"]').click();
|
||||||
|
await wait(50);
|
||||||
doc.querySelector('[data-action="mode-jog"]').click();
|
doc.querySelector('[data-action="mode-jog"]').click();
|
||||||
await wait(50);
|
await wait(50);
|
||||||
const xBeforeJog = win.webRtcp5AxisSimulation.getState().axisPose.x;
|
const xBeforeJog = win.webRtcp5AxisSimulation.getState().axisPose.x;
|
||||||
@@ -216,11 +479,47 @@
|
|||||||
if (win.webRtcp5AxisSimulation.getState().axisPose.x <= xBeforeJog) {
|
if (win.webRtcp5AxisSimulation.getState().axisPose.x <= xBeforeJog) {
|
||||||
throw new Error("JOG X+ did not move the axis");
|
throw new Error("JOG X+ did not move the axis");
|
||||||
}
|
}
|
||||||
|
await win.webRtcp5AxisSimulation.restoreSession();
|
||||||
|
await wait(50);
|
||||||
|
const restoredState = win.webRtcp5AxisSimulation.getState();
|
||||||
|
if (restoredState.axisPose.x !== xBeforeJog) {
|
||||||
|
throw new Error("restoreSession did not restore saved axis pose");
|
||||||
|
}
|
||||||
|
if (restoredState.programExecutionSourceMode !== "linuxcnc-interpreter-wasm") {
|
||||||
|
throw new Error("restoreSession did not restore LinuxCNC interpreter execution source");
|
||||||
|
}
|
||||||
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("restored")) {
|
||||||
|
throw new Error("session restore status did not render");
|
||||||
|
}
|
||||||
|
if (win.webRtcp5AxisSimulation.getState().machine.allHomed !== true) {
|
||||||
|
doc.querySelector('[data-action="mode-manual"]').click();
|
||||||
|
await wait(50);
|
||||||
|
doc.querySelector('[data-action="HOME"]').click();
|
||||||
|
await wait(50);
|
||||||
|
}
|
||||||
doc.querySelector('[data-action="mode-mdi"]').click();
|
doc.querySelector('[data-action="mode-mdi"]').click();
|
||||||
|
const mdiInput = doc.querySelector('[data-action="mdi-command"]');
|
||||||
|
if (!mdiInput) {
|
||||||
|
throw new Error("missing MDI command input");
|
||||||
|
}
|
||||||
|
mdiInput.value = "G90 X12.5 Y-4 Z1.25 F900";
|
||||||
|
doc.querySelector('[data-action="mdi-submit"]').click();
|
||||||
|
await wait(50);
|
||||||
|
let mdiState = win.webRtcp5AxisSimulation.getState();
|
||||||
|
if (mdiState.runState !== "mdi") {
|
||||||
|
throw new Error("MDI action did not update run state");
|
||||||
|
}
|
||||||
|
if (mdiState.axisPose.x !== 12.5 || mdiState.axisPose.y !== -4 || mdiState.axisPose.z !== 1.25) {
|
||||||
|
throw new Error(`MDI input did not move axes: ${JSON.stringify(mdiState.axisPose)}`);
|
||||||
|
}
|
||||||
|
if (!doc.querySelector(".mdi-history")?.textContent.includes("G90 X12.5")) {
|
||||||
|
throw new Error("MDI history did not render executed command");
|
||||||
|
}
|
||||||
doc.querySelector('[data-action="MDI_RUN"]').click();
|
doc.querySelector('[data-action="MDI_RUN"]').click();
|
||||||
await wait(50);
|
await wait(50);
|
||||||
if (win.webRtcp5AxisSimulation.getState().runState !== "mdi") {
|
mdiState = win.webRtcp5AxisSimulation.getState();
|
||||||
throw new Error("MDI action did not update run state");
|
if (mdiState.runState !== "mdi" || mdiState.machine.mdiCommand !== "G90 X12.5 Y-4 Z1.25 F900") {
|
||||||
|
throw new Error("bottom MDI button did not execute staged command");
|
||||||
}
|
}
|
||||||
doc.querySelector('[data-action="reset"]').click();
|
doc.querySelector('[data-action="reset"]').click();
|
||||||
await wait(50);
|
await wait(50);
|
||||||
@@ -272,6 +571,8 @@
|
|||||||
if (win.webRtcp5AxisSimulation.getState().preview.fullscreen !== true) {
|
if (win.webRtcp5AxisSimulation.getState().preview.fullscreen !== true) {
|
||||||
throw new Error("fullscreen button did not update state");
|
throw new Error("fullscreen button did not update state");
|
||||||
}
|
}
|
||||||
|
doc.querySelector('[data-action="mode-manual"]').click();
|
||||||
|
await wait(50);
|
||||||
doc.querySelector('[data-action="HOME"]').click();
|
doc.querySelector('[data-action="HOME"]').click();
|
||||||
await wait(50);
|
await wait(50);
|
||||||
const homedState = win.webRtcp5AxisSimulation.getState();
|
const homedState = win.webRtcp5AxisSimulation.getState();
|
||||||
|
|||||||
77
web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh
Executable file
77
web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||||
|
CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}"
|
||||||
|
|
||||||
|
if [[ -z "$CHROMIUM" ]]; then
|
||||||
|
echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
npm --prefix "$ROOT_DIR/web-rtcp-5axis-sim-plan/app" run build >/dev/null
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
PORT_FILE="$TMP_DIR/port"
|
||||||
|
SERVER_LOG="$TMP_DIR/server.log"
|
||||||
|
CHROME_PROFILE="$TMP_DIR/chrome-profile"
|
||||||
|
mkdir -p "$CHROME_PROFILE"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${SERVER_PID:-}" ]]; then
|
||||||
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 &
|
||||||
|
import functools
|
||||||
|
import http.server
|
||||||
|
import pathlib
|
||||||
|
import socketserver
|
||||||
|
import sys
|
||||||
|
|
||||||
|
root = pathlib.Path(sys.argv[1])
|
||||||
|
port_file = pathlib.Path(sys.argv[2])
|
||||||
|
|
||||||
|
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
|
||||||
|
with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd:
|
||||||
|
port_file.write_text(str(httpd.server_address[1]), encoding="ascii")
|
||||||
|
httpd.serve_forever()
|
||||||
|
PY
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
for _ in $(seq 1 100); do
|
||||||
|
[[ -s "$PORT_FILE" ]] && break
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -s "$PORT_FILE" ]]; then
|
||||||
|
echo "gmoccapy dist browser smoke HTTP server did not start" >&2
|
||||||
|
cat "$SERVER_LOG" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PORT="$(cat "$PORT_FILE")"
|
||||||
|
APP_PATH="../../app/dist/index.html"
|
||||||
|
URL="http://127.0.0.1:$PORT/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html?app=$APP_PATH"
|
||||||
|
OUT="$TMP_DIR/chromium-gmoccapy-dist.out"
|
||||||
|
|
||||||
|
"$CHROMIUM" \
|
||||||
|
--headless=new \
|
||||||
|
--disable-gpu \
|
||||||
|
--no-sandbox \
|
||||||
|
--user-data-dir="$CHROME_PROFILE" \
|
||||||
|
--virtual-time-budget=10000 \
|
||||||
|
--dump-dom \
|
||||||
|
"$URL" >"$OUT" 2>&1
|
||||||
|
|
||||||
|
if ! grep -Fq "gmoccapy_shell_smoke=ok" "$OUT"; then
|
||||||
|
echo "gmoccapy dist browser smoke failed" >&2
|
||||||
|
sed -n '1,260p' "$OUT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "gmoccapy_dist_smoke=ok"
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { createFiveAxisSessionPayload, createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||||
|
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||||
|
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||||
|
|
||||||
|
async function waitForInterpreterExecution(store) {
|
||||||
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||||
|
const state = store.getState();
|
||||||
|
if (!state.interpreterExecutionPending && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm") {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
return store.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = createSimulationStore();
|
||||||
|
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
|
||||||
|
|
||||||
|
store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime: interpreterRuntime });
|
||||||
|
store.dispatch({ type: "TOGGLE_POWER" });
|
||||||
|
store.dispatch({ type: "HOME" });
|
||||||
|
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||||
|
store.dispatch({ type: "SET_RTCP", enabled: true });
|
||||||
|
store.dispatch({
|
||||||
|
type: "LOAD_PROGRAM",
|
||||||
|
filename: "session-demo.ngc",
|
||||||
|
content: [
|
||||||
|
"G90 G17",
|
||||||
|
"G0 X0 Y0 Z0",
|
||||||
|
"G1 X4 Y5 A6 C7 F100",
|
||||||
|
"M2",
|
||||||
|
].join("\n"),
|
||||||
|
});
|
||||||
|
await waitForInterpreterExecution(store);
|
||||||
|
store.dispatch({ type: "STEP" });
|
||||||
|
|
||||||
|
const beforeSave = store.getState();
|
||||||
|
const payload = createFiveAxisSessionPayload(beforeSave);
|
||||||
|
assert.equal(payload.apiName, "web-rtcp-5axis-session-payload");
|
||||||
|
assert.equal(payload.machineProfile, "xyzac-trt");
|
||||||
|
assert.equal(payload.programExecution.sourceMode, "linuxcnc-interpreter-wasm");
|
||||||
|
assert.equal(payload.rtcpState, "on");
|
||||||
|
assert.equal(payload.programRuntimeFeedback.sourceMode, "linuxcnc-tp-runtime-sample");
|
||||||
|
assert.equal(payload.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_cycle_feedback_without_hardware");
|
||||||
|
|
||||||
|
const storage = createMemorySessionStorage();
|
||||||
|
const saved = await store.saveSession({ storage });
|
||||||
|
assert.equal(saved.snapshot.format, "web-rtcp-5axis-session-snapshot");
|
||||||
|
assert.equal(saved.path, "web-rtcp-5axis-sim-plan/sessions/gmoccapy-web-session/web-rtcp-5axis-session.json");
|
||||||
|
|
||||||
|
store.dispatch({ type: "SET_RTCP", enabled: false });
|
||||||
|
store.dispatch({ type: "STOP" });
|
||||||
|
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||||
|
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 25 });
|
||||||
|
assert.notEqual(store.getState().rtcpState, beforeSave.rtcpState);
|
||||||
|
assert.notEqual(store.getState().axisPose.x, beforeSave.axisPose.x);
|
||||||
|
|
||||||
|
const restored = await store.restoreSession({ storage });
|
||||||
|
const afterRestore = store.getState();
|
||||||
|
assert.equal(restored.path, saved.path);
|
||||||
|
assert.equal(afterRestore.sessionPersistence.status, "restored");
|
||||||
|
assert.equal(afterRestore.rtcpState, beforeSave.rtcpState);
|
||||||
|
assert.equal(afterRestore.kinsType, beforeSave.kinsType);
|
||||||
|
assert.equal(afterRestore.axisPose.x, beforeSave.axisPose.x);
|
||||||
|
assert.equal(afterRestore.activeProgram, "session-demo.ngc");
|
||||||
|
assert.equal(afterRestore.programExecution.sourceMode, "linuxcnc-interpreter-wasm");
|
||||||
|
assert.equal(afterRestore.programExecution.summary.motionEventCount, beforeSave.programExecution.summary.motionEventCount);
|
||||||
|
assert.equal(afterRestore.programRuntimeFeedback.semanticBoundary, beforeSave.programRuntimeFeedback.semanticBoundary);
|
||||||
|
assert.equal(afterRestore.programRuntimeFeedback.axisPose.x, beforeSave.programRuntimeFeedback.axisPose.x);
|
||||||
|
|
||||||
|
console.log("five_axis_session_smoke=ok");
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { createFullLinuxCncExecutionBoundary } from "../../app/src/runtime/full-execution-boundary.js";
|
||||||
|
|
||||||
|
const blocked = createFullLinuxCncExecutionBoundary({});
|
||||||
|
assert.equal(blocked.apiName, "web-rtcp-5axis-full-linuxcnc-execution-boundary");
|
||||||
|
assert.equal(blocked.phase, "blocked");
|
||||||
|
assert.equal(blocked.promotionAllowed, false);
|
||||||
|
assert.equal(blocked.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
assert.equal(blocked.missing.includes("linuxcnc kinematics WASM frame"), true);
|
||||||
|
assert.equal(blocked.blockers.some((blocker) => blocker.includes("native LinuxCNC task")), true);
|
||||||
|
|
||||||
|
const canonicalState = {
|
||||||
|
machineProfile: "xyzac-trt",
|
||||||
|
linuxCncBoundaryAdapter: {
|
||||||
|
linuxCncKinematicsReady: true,
|
||||||
|
linuxCncInterpreterReady: true,
|
||||||
|
},
|
||||||
|
rtcpFrame: {
|
||||||
|
semanticBoundary: "linuxcnc_kinematics_wasm_c_abi",
|
||||||
|
readiness: { linuxCncKinematicsReady: true },
|
||||||
|
},
|
||||||
|
interpreterRuntimeReadiness: {
|
||||||
|
loaded: true,
|
||||||
|
semanticBoundary: "linuxcnc_interpreter_wasm_canonical_events",
|
||||||
|
},
|
||||||
|
programExecutionSourceMode: "linuxcnc-interpreter-wasm",
|
||||||
|
programExecution: {
|
||||||
|
sourceMode: "linuxcnc-interpreter-wasm",
|
||||||
|
summary: {
|
||||||
|
motionEventCount: 3,
|
||||||
|
canonicalEventCount: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const canonical = createFullLinuxCncExecutionBoundary(canonicalState);
|
||||||
|
|
||||||
|
assert.equal(canonical.readyForUiSimulation, true);
|
||||||
|
assert.equal(canonical.machineFileBackedRemapReady, false);
|
||||||
|
assert.equal(canonical.semanticBoundary, "linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked");
|
||||||
|
assert.equal(canonical.satisfied.includes("canonical-motion-events"), true);
|
||||||
|
assert.equal(canonical.plannerRuntimeReady, false);
|
||||||
|
assert.equal(canonical.missing.includes("machine-file backed five-axis remap run"), true);
|
||||||
|
|
||||||
|
const canonicalWithPlanner = createFullLinuxCncExecutionBoundary({
|
||||||
|
...canonicalState,
|
||||||
|
programExecution: {
|
||||||
|
...canonicalState.programExecution,
|
||||||
|
plannerTiming: {
|
||||||
|
plannerRuntimeReady: true,
|
||||||
|
semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
...canonicalState.programExecution.summary,
|
||||||
|
plannerRuntimeReady: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(canonicalWithPlanner.plannerRuntimeReady, true);
|
||||||
|
assert.equal(canonicalWithPlanner.satisfied.includes("linuxcnc-tp-queue-runtime-timing"), true);
|
||||||
|
assert.equal(canonicalWithPlanner.nativeTaskReady, false);
|
||||||
|
assert.equal(canonicalWithPlanner.nativeHalSyncReady, false);
|
||||||
|
|
||||||
|
const remap = createFullLinuxCncExecutionBoundary({
|
||||||
|
...canonicalState,
|
||||||
|
programExecution: {
|
||||||
|
...canonicalState.programExecution,
|
||||||
|
plannerTiming: {
|
||||||
|
plannerRuntimeReady: true,
|
||||||
|
semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
...canonicalState.programExecution.summary,
|
||||||
|
plannerRuntimeReady: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
machineFileStaging: {
|
||||||
|
status: "staged",
|
||||||
|
fileCount: 12,
|
||||||
|
},
|
||||||
|
machineFileExecution: {
|
||||||
|
sourceMode: "linuxcnc-machine-file-remap-wasm",
|
||||||
|
summary: {
|
||||||
|
machineFileExecutionReady: true,
|
||||||
|
},
|
||||||
|
resultText: [
|
||||||
|
"fiveaxis_ini_open=1",
|
||||||
|
"fiveaxis_remaps_ready=1",
|
||||||
|
"fiveaxis_file_reached_exit=1",
|
||||||
|
"fiveaxis_hal_switchkins: rc=0 found=1 value=0",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(remap.machineFileBackedRemapReady, true);
|
||||||
|
assert.equal(remap.remapRuntimeReady, true);
|
||||||
|
assert.equal(remap.halSwitchkinsEvidenceReady, true);
|
||||||
|
assert.equal(remap.plannerRuntimeReady, true);
|
||||||
|
assert.equal(remap.nativeTaskReady, false);
|
||||||
|
assert.equal(remap.nativeHalSyncReady, false);
|
||||||
|
assert.equal(remap.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
assert.equal(remap.promotionAllowed, false);
|
||||||
|
assert.equal(remap.semanticBoundary, "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked");
|
||||||
|
assert.equal(remap.satisfied.includes("fiveaxis-remap-machine-file-run"), true);
|
||||||
|
assert.equal(remap.evidence.machineFileFlags.length, 3);
|
||||||
|
|
||||||
|
console.log("full_execution_boundary_smoke=ok");
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createLinuxCncInterpSdk,
|
||||||
|
createLinuxCncKinematicsSdk,
|
||||||
|
planSimConfigStaging,
|
||||||
|
} from "../../../wasm-port/runtime/sdk/src/index.js";
|
||||||
|
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const workspaceRoot = resolve(__dirname, "../../..");
|
||||||
|
const wasmPortRoot = resolve(workspaceRoot, "wasm-port");
|
||||||
|
const vendorRoot = resolve(wasmPortRoot, "vendor/linuxcnc");
|
||||||
|
const trtConfigRel = "configs/sim/axis/vismach/5axis/table-rotary-tilting";
|
||||||
|
|
||||||
|
function near(actual, expected, tolerance = 1e-7) {
|
||||||
|
return Math.abs(actual - expected) <= tolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPoseComponent(pose, key, expected, label) {
|
||||||
|
assert.equal(near(Number(pose[key] ?? 0), expected), true, `${label}.${key}: ${pose[key]} != ${expected}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyTrtKinematics(moduleId, joints, expectedAxis) {
|
||||||
|
const wasmFile = `linuxcnc_${moduleId.replaceAll("-", "_")}_kinematics.wasm`;
|
||||||
|
const kins = await createLinuxCncKinematicsSdk({
|
||||||
|
moduleId,
|
||||||
|
moduleOptions: {
|
||||||
|
wasmBinary: readFileSync(resolve(wasmPortRoot, "build/wasm/kinematics", wasmFile)),
|
||||||
|
print() {},
|
||||||
|
printErr() {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(kins.apiName, "linuxcnc-kinematics-wasm-sdk");
|
||||||
|
assert.equal(kins.switchable(), 1);
|
||||||
|
assert.equal(kins.switchKinematics(0), 0);
|
||||||
|
|
||||||
|
const forward = kins.forward(joints);
|
||||||
|
assert.equal(forward.rc, 0, `${moduleId} forward`);
|
||||||
|
|
||||||
|
const inverse = kins.inverse(forward.pose, 5, { seedJoints: joints });
|
||||||
|
assert.equal(inverse.rc, 0, `${moduleId} inverse`);
|
||||||
|
inverse.joints.slice(0, 5).forEach((joint, index) => {
|
||||||
|
assert.equal(near(joint, joints[index]), true, `${moduleId} inverse joint ${index}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(kins.switchKinematics(1), 0);
|
||||||
|
const identity = kins.forward(joints);
|
||||||
|
assert.equal(identity.rc, 0, `${moduleId} identity forward`);
|
||||||
|
assertPoseComponent(identity.pose, "x", joints[0], `${moduleId} identity`);
|
||||||
|
assertPoseComponent(identity.pose, "y", joints[1], `${moduleId} identity`);
|
||||||
|
assertPoseComponent(identity.pose, "z", joints[2], `${moduleId} identity`);
|
||||||
|
assertPoseComponent(identity.pose, expectedAxis, joints[3], `${moduleId} identity`);
|
||||||
|
assertPoseComponent(identity.pose, "c", joints[4], `${moduleId} identity`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await verifyTrtKinematics("xyzac-trt", [10, 20, 30, 25, 40], "a");
|
||||||
|
await verifyTrtKinematics("xyzbc-trt", [10, 20, 30, 35, 40], "b");
|
||||||
|
|
||||||
|
const manifestText = readFileSync(resolve(wasmPortRoot, "tools/source-manifest.txt"), "utf8");
|
||||||
|
for (const iniFile of ["xyzac-trt.ini", "xyzbc-trt.ini"]) {
|
||||||
|
const iniText = readFileSync(resolve(vendorRoot, trtConfigRel, iniFile), "utf8");
|
||||||
|
const iniConfig = parseLinuxCncIni(iniText, {
|
||||||
|
path: `${trtConfigRel}/${iniFile}`,
|
||||||
|
profileId: iniFile.startsWith("xyzbc") ? "xyzbc-trt" : "xyzac-trt",
|
||||||
|
});
|
||||||
|
assert.equal(iniConfig.validation.ready, true, `${iniFile} INI ready`);
|
||||||
|
assert.equal(iniConfig.kinematics.name.endsWith("-trt-kins"), true, `${iniFile} kins`);
|
||||||
|
assert.deepEqual(iniConfig.halui.mdiCommands, ["M429", "M428", "M430"], `${iniFile} HALUI MDI`);
|
||||||
|
|
||||||
|
const plan = planSimConfigStaging({
|
||||||
|
manifestText,
|
||||||
|
machineRel: `axis/vismach/5axis/table-rotary-tilting`,
|
||||||
|
iniFile,
|
||||||
|
iniText,
|
||||||
|
});
|
||||||
|
const staged = plan.files.map((file) => file.sourceRel);
|
||||||
|
assert.equal(staged.includes(`${trtConfigRel}/${iniFile}`), true, `${iniFile} staged`);
|
||||||
|
assert.equal(staged.some((file) => file.endsWith("/remap_subs/428remap.ngc")), true, `${iniFile} M428 remap staged`);
|
||||||
|
assert.equal(staged.some((file) => file.endsWith("/remap_subs/429remap.ngc")), true, `${iniFile} M429 remap staged`);
|
||||||
|
assert.equal(staged.some((file) => file.endsWith("/remap_subs/430remap.ngc")), true, `${iniFile} M430 remap staged`);
|
||||||
|
assert.equal(staged.some((file) => file.endsWith(".tbl")), true, `${iniFile} tool table staged`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const interp = await createLinuxCncInterpSdk({
|
||||||
|
wasmBinary: readFileSync(resolve(wasmPortRoot, "build/wasm/core/linuxcnc_interp.wasm")),
|
||||||
|
print() {},
|
||||||
|
printErr() {},
|
||||||
|
});
|
||||||
|
const directFiveAxisProgram = [
|
||||||
|
"G90 G17",
|
||||||
|
"G0 X0 Y0 Z0 A0 C0",
|
||||||
|
"G1 X10 Y2 Z-1 A15 C30 F120",
|
||||||
|
"G1 X0 Y0 Z0 A0 C0 F120",
|
||||||
|
"M2",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
const output = interp.runProgram(directFiveAxisProgram);
|
||||||
|
assert.equal(output.includes("canon_event=STRAIGHT_TRAVERSE"), true);
|
||||||
|
assert.equal(output.includes("canon_event=STRAIGHT_FEED"), true);
|
||||||
|
assert.equal(output.includes("a=15"), true);
|
||||||
|
assert.equal(output.includes("c=30"), true);
|
||||||
|
|
||||||
|
console.log("full_linuxcnc_5axis_source_node_smoke=ok");
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyIniConfigToProfile,
|
||||||
|
parseLinuxCncIni,
|
||||||
|
} from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||||
|
import { xyzacTrtProfile } from "../../app/src/profiles/xyzac-trt.js";
|
||||||
|
import { xyzbcTrtProfile } from "../../app/src/profiles/xyzbc-trt.js";
|
||||||
|
|
||||||
|
const xyzacIniText = await readFile(
|
||||||
|
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const xyzbcIniText = await readFile(
|
||||||
|
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const xyzacIni = parseLinuxCncIni(xyzacIniText, {
|
||||||
|
path: xyzacTrtProfile.iniPath,
|
||||||
|
profileId: xyzacTrtProfile.id,
|
||||||
|
});
|
||||||
|
assert.equal(xyzacIni.apiName, "web-rtcp-5axis-linuxcnc-ini-config");
|
||||||
|
assert.equal(xyzacIni.validation.ready, true);
|
||||||
|
assert.equal(xyzacIni.machineName, "sim-xyzac-trt-kins (switchkins)");
|
||||||
|
assert.equal(xyzacIni.kinematics.name, "xyzac-trt-kins");
|
||||||
|
assert.equal(xyzacIni.kinematicsParameters.sparm, "identityfirst");
|
||||||
|
assert.equal(xyzacIni.kinematicsParameters.joints, 5);
|
||||||
|
assert.equal(xyzacIni.traj.coordinates, "XYZAC");
|
||||||
|
assert.equal(xyzacIni.axisLimits.A.max, 50);
|
||||||
|
assert.equal(xyzacIni.jointConfig[3].axis, "A");
|
||||||
|
assert.equal(xyzacIni.jointConfig[4].max, 36000);
|
||||||
|
assert.deepEqual(xyzacIni.halui.mdiCommands, ["M429", "M428", "M430"]);
|
||||||
|
assert.equal(xyzacIni.rs274ngc.remaps.map((remap) => remap.code).join(","), "M428,M429,M430");
|
||||||
|
assert.equal(xyzacIni.hal.initialSets.some((set) => set.pin === "y-offset" && set.value === 20), true);
|
||||||
|
|
||||||
|
const derivedXyzac = applyIniConfigToProfile(xyzacTrtProfile, xyzacIni);
|
||||||
|
assert.equal(derivedXyzac.machineName, xyzacIni.machineName);
|
||||||
|
assert.equal(derivedXyzac.traj.coordinates, "XYZAC");
|
||||||
|
assert.equal(derivedXyzac.axisLimits.X.min, -200);
|
||||||
|
assert.equal(derivedXyzac.jointConfig.length, 5);
|
||||||
|
assert.equal(derivedXyzac.linuxCncIniConfig.path.endsWith("xyzac-trt.ini"), true);
|
||||||
|
|
||||||
|
const xyzbcIni = parseLinuxCncIni(xyzbcIniText, {
|
||||||
|
path: xyzbcTrtProfile.iniPath,
|
||||||
|
profileId: xyzbcTrtProfile.id,
|
||||||
|
});
|
||||||
|
assert.equal(xyzbcIni.validation.ready, true);
|
||||||
|
assert.equal(xyzbcIni.kinematics.name, "xyzbc-trt-kins");
|
||||||
|
assert.equal(xyzbcIni.kinematicsModuleId, "xyzbc-trt");
|
||||||
|
assert.equal(xyzbcIni.traj.coordinates, "XYZBC");
|
||||||
|
assert.equal(xyzbcIni.axisLimits.B.max, 36000);
|
||||||
|
assert.equal(xyzbcIni.jointConfig[3].axis, "B");
|
||||||
|
assert.equal(xyzbcIni.kinematicsParameters.switchkinsTypes[1].webKinsType, "tcp-xyzbc");
|
||||||
|
|
||||||
|
console.log("linuxcnc_ini_runtime_smoke=ok");
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||||
|
|
||||||
|
const runtime = await createLinuxCncInterpreterRuntime();
|
||||||
|
const readiness = runtime.readiness();
|
||||||
|
|
||||||
|
assert.equal(runtime.apiName, "web-rtcp-5axis-linuxcnc-interpreter-runtime");
|
||||||
|
assert.equal(runtime.loaded, true);
|
||||||
|
assert.equal(runtime.sourceMode, "linuxcnc-interpreter-wasm");
|
||||||
|
assert.equal(runtime.semanticBoundary, "linuxcnc_interpreter_wasm_canonical_events");
|
||||||
|
assert.equal(readiness.loaded, true);
|
||||||
|
assert.equal(readiness.runProgramReady, true);
|
||||||
|
assert.equal(readiness.remapRuntimeReady, false);
|
||||||
|
assert.equal(readiness.plannerRuntimeReady, true);
|
||||||
|
assert.equal(readiness.plannerSemanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
|
||||||
|
|
||||||
|
const programText = [
|
||||||
|
"G90 G17",
|
||||||
|
"G0 X0 Y0 Z0",
|
||||||
|
"G1 X10 Y2 F100",
|
||||||
|
"G1 X12 Y4 A5 C7",
|
||||||
|
"M2",
|
||||||
|
].join("\n");
|
||||||
|
const execution = runtime.runProgram(programText);
|
||||||
|
|
||||||
|
assert.equal(execution.apiName, "web-rtcp-5axis-linuxcnc-interpreter-program-execution");
|
||||||
|
assert.equal(execution.sourceMode, "linuxcnc-interpreter-wasm");
|
||||||
|
assert.equal(execution.semanticBoundary, "linuxcnc_interpreter_wasm_canonical_events");
|
||||||
|
assert.equal(execution.summary.ready, true);
|
||||||
|
assert.equal(execution.summary.motionEventCount >= 3, true);
|
||||||
|
assert.equal(execution.summary.canonicalEventCount > execution.summary.motionEventCount, true);
|
||||||
|
assert.equal(execution.summary.motionTypes.includes("STRAIGHT_TRAVERSE"), true);
|
||||||
|
assert.equal(execution.summary.motionTypes.includes("STRAIGHT_FEED"), true);
|
||||||
|
assert.equal(execution.summary.finalAxes.x, 12);
|
||||||
|
assert.equal(execution.summary.finalAxes.y, 4);
|
||||||
|
assert.equal(execution.summary.finalAxes.a, 5);
|
||||||
|
assert.equal(execution.summary.finalAxes.c, 7);
|
||||||
|
assert.equal(execution.motion[2].feedRate, 100);
|
||||||
|
assert.equal(execution.summary.remapRuntimeReady, false);
|
||||||
|
assert.equal(execution.summary.plannerRuntimeReady, true);
|
||||||
|
assert.equal(execution.plannerTiming.semanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
|
||||||
|
assert.equal(execution.plannerTiming.totalSeconds > 0, true);
|
||||||
|
assert.equal(execution.plannerTiming.motionCount, execution.motion.length);
|
||||||
|
assert.equal(execution.summary.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
|
||||||
|
const switchkinsProgramText = [
|
||||||
|
"G90 G17",
|
||||||
|
"M428",
|
||||||
|
"G0 X0 Y0 Z0 A0 C0",
|
||||||
|
"G1 X10 Y2 Z-1 A15 C30 F120",
|
||||||
|
"M429",
|
||||||
|
"G1 X0 Y0 Z0 A0 C0 F120",
|
||||||
|
"M2",
|
||||||
|
].join("\n");
|
||||||
|
const switchkinsExecution = runtime.runProgram(switchkinsProgramText);
|
||||||
|
|
||||||
|
assert.equal(switchkinsExecution.summary.ready, true);
|
||||||
|
assert.equal(switchkinsExecution.summary.motionEventCount, 3);
|
||||||
|
assert.equal(switchkinsExecution.summary.switchkinsEventCount, 2);
|
||||||
|
assert.deepEqual(switchkinsExecution.summary.switchkinsCodes, ["M428", "M429"]);
|
||||||
|
assert.equal(
|
||||||
|
switchkinsExecution.summary.switchkinsRemapBoundary,
|
||||||
|
"linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
|
||||||
|
);
|
||||||
|
assert.equal(switchkinsExecution.switchkinsEvents[0].switchkinsType, 1);
|
||||||
|
assert.equal(switchkinsExecution.switchkinsEvents[1].switchkinsType, 0);
|
||||||
|
assert.equal(switchkinsExecution.motion[0].kinsType, "tcp");
|
||||||
|
assert.equal(switchkinsExecution.motion[1].axes.a, 15);
|
||||||
|
assert.equal(switchkinsExecution.motion[1].axes.c, 30);
|
||||||
|
assert.equal(switchkinsExecution.motion[1].switchkinsCode, "M428");
|
||||||
|
assert.equal(switchkinsExecution.motion[2].kinsType, "identity");
|
||||||
|
assert.equal(switchkinsExecution.motion[2].switchkinsCode, "M429");
|
||||||
|
|
||||||
|
assert.equal(switchkinsExecution.summary.remapRuntimeReady, false);
|
||||||
|
assert.equal(switchkinsExecution.summary.plannerRuntimeReady, true);
|
||||||
|
assert.equal(switchkinsExecution.plannerTiming.totalSeconds > 0, true);
|
||||||
|
assert.equal(switchkinsExecution.summary.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
|
||||||
|
console.log("linuxcnc_interpreter_runtime_smoke=ok");
|
||||||
@@ -11,8 +11,10 @@ assert.equal(runtime.moduleId, "xyzac-trt");
|
|||||||
assert.equal(runtime.loaded, true);
|
assert.equal(runtime.loaded, true);
|
||||||
assert.equal(runtime.sourceMode, "source-derived-kinematics-wasm");
|
assert.equal(runtime.sourceMode, "source-derived-kinematics-wasm");
|
||||||
assert.equal(runtime.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
assert.equal(runtime.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||||
|
assert.equal(runtime.executionContext, "direct");
|
||||||
assert.equal(runtime.wasmFile, "linuxcnc_xyzac_trt_kinematics.wasm");
|
assert.equal(runtime.wasmFile, "linuxcnc_xyzac_trt_kinematics.wasm");
|
||||||
assert.equal(readiness.loaded, true);
|
assert.equal(readiness.loaded, true);
|
||||||
|
assert.equal(readiness.executionContext, "direct");
|
||||||
assert.equal(readiness.supportedModules.includes("xyzac-trt"), true);
|
assert.equal(readiness.supportedModules.includes("xyzac-trt"), true);
|
||||||
assert.equal(runtime.switchRc, 0);
|
assert.equal(runtime.switchRc, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||||
|
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||||
|
import {
|
||||||
|
createMachineFileStagingPlan,
|
||||||
|
selectMachineFileProgram,
|
||||||
|
stageProfileMachineFiles,
|
||||||
|
} from "../../app/src/runtime/linuxcnc-machine-file-staging.js";
|
||||||
|
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||||
|
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||||
|
|
||||||
|
async function waitForMachineFileExecution(store) {
|
||||||
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||||
|
const state = store.getState();
|
||||||
|
if (!state.interpreterExecutionPending && state.machineFileExecution) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
return store.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = getFiveAxisProfile("xyzac-trt");
|
||||||
|
const storage = createMemorySessionStorage();
|
||||||
|
const plan = await createMachineFileStagingPlan({ profile });
|
||||||
|
|
||||||
|
assert.equal(plan.apiName, "web-rtcp-5axis-machine-file-staging-plan");
|
||||||
|
assert.equal(plan.profileId, "xyzac-trt");
|
||||||
|
assert.equal(plan.semanticBoundary, "linuxcnc_sim_config_file_staging_plan_only");
|
||||||
|
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("xyzac-trt.ini")), true);
|
||||||
|
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("xyzac-trt.tbl")), true);
|
||||||
|
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc")), true);
|
||||||
|
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("remap_subs/429remap.ngc")), true);
|
||||||
|
assert.equal(plan.files.some((file) => file.sourceRel.endsWith("demos/xyzac_switchkins.ngc")), true);
|
||||||
|
assert.equal(plan.summary.remapFileCount >= 3, true);
|
||||||
|
assert.equal(plan.summary.demoFileCount >= 1, true);
|
||||||
|
|
||||||
|
const staged = await stageProfileMachineFiles(profile, { storage });
|
||||||
|
assert.equal(staged.save.apiName, "web-rtcp-5axis-machine-file-staging-save");
|
||||||
|
assert.equal(staged.save.status, "saved");
|
||||||
|
assert.equal(staged.save.fileCount, staged.plan.files.length);
|
||||||
|
assert.equal(staged.save.semanticBoundary, "opfs_machine_file_text_staging_only");
|
||||||
|
assert.equal(staged.save.summary.kinds.remap >= 3, true);
|
||||||
|
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "impeller-7bl-xyzac.ngc"), true);
|
||||||
|
assert.equal(staged.save.gcodeSources.some((source) => source.filename === "boat-xyzac.ngc"), true);
|
||||||
|
assert.equal(staged.save.files.every((file) => file.opfsPath.startsWith("web-rtcp-5axis-sim-plan/machines/xyzac-trt/")), true);
|
||||||
|
assert.throws(
|
||||||
|
() => selectMachineFileProgram(staged.plan, staged.save, "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc"),
|
||||||
|
/must come from LinuxCNC source demos/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const iniOpfsPath = staged.save.files.find((file) => file.sourceRel.endsWith("xyzac-trt.ini")).opfsPath;
|
||||||
|
assert.equal(storage.files.get(iniOpfsPath).includes("KINEMATICS = xyzac-trt-kins"), true);
|
||||||
|
|
||||||
|
const store = createSimulationStore();
|
||||||
|
store.dispatch({
|
||||||
|
type: "ATTACH_INTERPRETER_RUNTIME",
|
||||||
|
runtime: await createLinuxCncInterpreterRuntime(),
|
||||||
|
});
|
||||||
|
const storeStage = await store.stageMachineFiles({ storage });
|
||||||
|
const state = store.getState();
|
||||||
|
assert.equal(storeStage.save.fileCount, staged.save.fileCount);
|
||||||
|
assert.equal(state.machineFileStaging.status, "staged");
|
||||||
|
assert.equal(state.machineFileStaging.profileId, "xyzac-trt");
|
||||||
|
assert.equal(state.machineFileStaging.fileCount, staged.save.fileCount);
|
||||||
|
assert.equal(state.machineFileStaging.save.summary.kinds.remap >= 3, true);
|
||||||
|
assert.equal(state.machineFileStaging.gcodeSources.length >= 4, true);
|
||||||
|
assert.equal(state.machineFileStaging.selectedGcodeSourceRel, null);
|
||||||
|
|
||||||
|
store.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
||||||
|
assert.equal(
|
||||||
|
store.getState().operatorMessage,
|
||||||
|
"machine-file run blocked: select a LinuxCNC source-directory 5-axis G-code program",
|
||||||
|
);
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||||
|
sourceRel: "operator-demo.ngc",
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
store.getState().operatorMessage,
|
||||||
|
"LinuxCNC G-code source not staged: operator-demo.ngc",
|
||||||
|
);
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||||
|
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
const sourceState = store.getState();
|
||||||
|
assert.equal(sourceState.programSource, "linuxcnc-vendored-5axis-gcode");
|
||||||
|
assert.equal(sourceState.activeProgram.endsWith("impeller-7bl-xyzac.ngc"), true);
|
||||||
|
assert.equal(sourceState.programLines[0].includes("Impeller 5-axis"), true);
|
||||||
|
assert.equal(sourceState.machineFileStaging.selectedGcodeSourceRel.endsWith("impeller-7bl-xyzac.ngc"), true);
|
||||||
|
assert.equal(sourceState.machineFileStaging.plan.wasmProgramPath.endsWith("/demos/impeller-7bl-xyzac.ngc"), true);
|
||||||
|
|
||||||
|
store.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
||||||
|
const runState = await waitForMachineFileExecution(store);
|
||||||
|
assert.equal(runState.machineFileExecution.sourceMode, "linuxcnc-machine-file-remap-wasm");
|
||||||
|
assert.equal(runState.machineFileExecution.semanticBoundary, "linuxcnc_fiveaxis_remap_wasm_machine_file_execution");
|
||||||
|
assert.equal(runState.machineFileExecution.summary.machineFileExecutionReady, true);
|
||||||
|
assert.equal(runState.machineFileExecution.summary.remapRuntimeReady, true);
|
||||||
|
assert.equal(runState.machineFileExecution.summary.plannerRuntimeReady, false);
|
||||||
|
assert.equal(runState.machineFileExecution.summary.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
assert.equal(runState.machineFileExecution.resultText.includes("fiveaxis_ini_open=1"), true);
|
||||||
|
assert.equal(runState.machineFileExecution.resultText.includes("fiveaxis_remaps_ready=1"), true);
|
||||||
|
assert.equal(runState.machineFileExecution.resultText.includes("fiveaxis_file_reached_exit=1"), true);
|
||||||
|
assert.equal(runState.machineFileExecution.machineFilePlan.selectedProgramFilename, "impeller-7bl-xyzac.ngc");
|
||||||
|
assert.equal(runState.machineFileExecution.machineFilePlan.wasmProgramPath.endsWith("/demos/impeller-7bl-xyzac.ngc"), true);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.apiName, "web-rtcp-5axis-full-linuxcnc-execution-boundary");
|
||||||
|
assert.equal(runState.fullExecutionBoundary.machineFileBackedRemapReady, true);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.remapRuntimeReady, true);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.halSwitchkinsEvidenceReady, true);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.plannerRuntimeReady, false);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.nativeTaskReady, false);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.nativeHalSyncReady, false);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.promotionAllowed, false);
|
||||||
|
assert.equal(
|
||||||
|
runState.fullExecutionBoundary.semanticBoundary,
|
||||||
|
"linuxcnc_machine_file_remap_ready_planner_task_hal_blocked",
|
||||||
|
);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.satisfied.includes("fiveaxis-remap-machine-file-run"), true);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.satisfied.includes("switchkins-hal-bridge-evidence"), true);
|
||||||
|
assert.equal(runState.fullExecutionBoundary.blockers.some((blocker) => blocker.includes("trajectory planner")), true);
|
||||||
|
|
||||||
|
console.log("machine_file_staging_smoke=ok");
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
import { xyzacTrtProfile } from "../../app/src/profiles/xyzac-trt.js";
|
import { xyzacTrtProfile } from "../../app/src/profiles/xyzac-trt.js";
|
||||||
|
import { getFiveAxisProfile, fiveAxisProfiles } from "../../app/src/profiles/index.js";
|
||||||
import { createPyvcpHalBindingSummary, xyzacTrtPyvcpPanelSchema } from "../../app/src/panel-schema/xyzac-trt-pyvcp.js";
|
import { createPyvcpHalBindingSummary, xyzacTrtPyvcpPanelSchema } from "../../app/src/panel-schema/xyzac-trt-pyvcp.js";
|
||||||
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../../app/src/runtime/linuxcnc-boundary-adapter.js";
|
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../../app/src/runtime/linuxcnc-boundary-adapter.js";
|
||||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||||
@@ -29,6 +30,20 @@ assert.equal(xyzacTrtProfile.halPins.includes("motion.switchkins-type"), true);
|
|||||||
assert.equal(xyzacTrtProfile.halPins.includes("xyzac-trt-kins.tool-offset"), true);
|
assert.equal(xyzacTrtProfile.halPins.includes("xyzac-trt-kins.tool-offset"), true);
|
||||||
assert.equal(xyzacTrtProfile.promotionAllowed, false);
|
assert.equal(xyzacTrtProfile.promotionAllowed, false);
|
||||||
assert.equal(xyzacTrtProfile.linuxCncKinematicsReady, false);
|
assert.equal(xyzacTrtProfile.linuxCncKinematicsReady, false);
|
||||||
|
assert.deepEqual(fiveAxisProfiles.map(({ id }) => id), ["xyzac-trt", "xyzbc-trt"]);
|
||||||
|
|
||||||
|
const xyzbcTrtProfile = getFiveAxisProfile("xyzbc-trt");
|
||||||
|
assert.equal(xyzbcTrtProfile.id, "xyzbc-trt");
|
||||||
|
assert.equal(xyzbcTrtProfile.iniPath.endsWith("xyzbc-trt.ini"), true);
|
||||||
|
assert.deepEqual(xyzbcTrtProfile.coordinates, ["X", "Y", "Z", "B", "C"]);
|
||||||
|
assert.equal(xyzbcTrtProfile.kinematics, "xyzbc-trt-kins");
|
||||||
|
assert.equal(xyzbcTrtProfile.kinematicsModuleId, "xyzbc-trt");
|
||||||
|
assert.equal(xyzbcTrtProfile.machineName, "sim-xyzbc-trt-kins (switchkins)");
|
||||||
|
assert.equal(xyzbcTrtProfile.traj.coordinates, "XYZBC");
|
||||||
|
assert.equal(xyzbcTrtProfile.axisLimits.B.max, 36000);
|
||||||
|
assert.equal(xyzbcTrtProfile.hal.halcmd.feedbackNets.some((net) => net.target === "xyzbc-trt-gui.tilt-b"), true);
|
||||||
|
assert.equal(xyzbcTrtProfile.halPins.includes("xyzbc-trt-kins.x-offset"), true);
|
||||||
|
assert.equal(xyzbcTrtProfile.panelSchema.id, "xyzbc-trt-switchkins-pyvcp");
|
||||||
|
|
||||||
const sourceSummary = createProfileSourceReferenceSummary("xyzac-trt");
|
const sourceSummary = createProfileSourceReferenceSummary("xyzac-trt");
|
||||||
assert.equal(sourceSummary.referenceCount >= 8, true);
|
assert.equal(sourceSummary.referenceCount >= 8, true);
|
||||||
@@ -39,6 +54,11 @@ assert.equal(sourceSummary.semanticBoundary, "profile_source_map_only_not_runtim
|
|||||||
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("xyzac-trt.ini")));
|
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("xyzac-trt.ini")));
|
||||||
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("trtfuncs.c")));
|
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("trtfuncs.c")));
|
||||||
|
|
||||||
|
const xyzbcSourceSummary = createProfileSourceReferenceSummary("xyzbc-trt");
|
||||||
|
assert.equal(xyzbcSourceSummary.referenceCount >= 7, true);
|
||||||
|
assert.ok(xyzbcSourceSummary.references.some((reference) => reference.path.endsWith("xyzbc-trt.ini")));
|
||||||
|
assert.ok(xyzbcSourceSummary.references.some((reference) => reference.path.endsWith("xyzbc-trt-kins.c")));
|
||||||
|
|
||||||
const panelSummary = createPyvcpHalBindingSummary(xyzacTrtPyvcpPanelSchema);
|
const panelSummary = createPyvcpHalBindingSummary(xyzacTrtPyvcpPanelSchema);
|
||||||
assert.equal(panelSummary.schemaId, "xyzac-trt-switchkins-pyvcp");
|
assert.equal(panelSummary.schemaId, "xyzac-trt-switchkins-pyvcp");
|
||||||
assert.equal(panelSummary.controlCount, 5);
|
assert.equal(panelSummary.controlCount, 5);
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||||
|
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||||
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
||||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
async function waitForInterpreterExecution(store) {
|
||||||
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||||
|
const state = store.getState();
|
||||||
|
if (!state.interpreterExecutionPending && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm") {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
return store.getState();
|
||||||
|
}
|
||||||
|
|
||||||
const identityFrame = buildRtcpFrame({
|
const identityFrame = buildRtcpFrame({
|
||||||
axisPose: { x: 43, y: -32.15, z: -11.306, a: 0, b: 0, c: 0 },
|
axisPose: { x: 43, y: -32.15, z: -11.306, a: 0, b: 0, c: 0 },
|
||||||
@@ -34,6 +48,7 @@ assert.ok(Number.isFinite(tcpFrame.toolAxisVector.z));
|
|||||||
assert.equal(tcpFrame.jointPose.length, 5);
|
assert.equal(tcpFrame.jointPose.length, 5);
|
||||||
|
|
||||||
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
|
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
|
||||||
|
const interpreterRuntime = await createLinuxCncInterpreterRuntime();
|
||||||
const linuxCncKinematicsResult = runtime.frameForJoints([10, 20, 30, 25, 40]);
|
const linuxCncKinematicsResult = runtime.frameForJoints([10, 20, 30, 25, 40]);
|
||||||
const linuxCncFrame = buildRtcpFrame({
|
const linuxCncFrame = buildRtcpFrame({
|
||||||
axisPose: { x: 10, y: 20, z: 30, a: 25, b: 0, c: 40 },
|
axisPose: { x: 10, y: 20, z: 30, a: 25, b: 0, c: 40 },
|
||||||
@@ -65,6 +80,37 @@ assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSumma
|
|||||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSummary.coordinates, "XYZAC");
|
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSummary.coordinates, "XYZAC");
|
||||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.ready, false);
|
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.ready, false);
|
||||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.promotionAllowed, false);
|
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.promotionAllowed, false);
|
||||||
|
assert.equal(fixtureInitialStore.getState().fullExecutionBoundary.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
assert.equal(fixtureInitialStore.getState().fullExecutionBoundary.promotionAllowed, false);
|
||||||
|
assert.equal(fixtureInitialStore.getState().fullExecutionBoundary.phase, "blocked");
|
||||||
|
assert.equal(fixtureInitialStore.getState().linuxCncTaskPolicy.semanticBoundary, "linuxcnc_task_state_mode_command_gate");
|
||||||
|
assert.equal(
|
||||||
|
fixtureInitialStore.getState().linuxCncTaskPolicy.sourceReferences.some((reference) => reference.path === "linuxcnc/src/emc/task/emctaskmain.cc"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(fixtureInitialStore.getState().linuxCncTaskPolicy.canRunAuto, false);
|
||||||
|
|
||||||
|
const xyzacIniText = await readFile(
|
||||||
|
new URL("../../../wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const xyzacIniConfig = parseLinuxCncIni(xyzacIniText, {
|
||||||
|
path: fixtureInitialStore.getState().profile.iniPath,
|
||||||
|
profileId: fixtureInitialStore.getState().machineProfile,
|
||||||
|
});
|
||||||
|
fixtureInitialStore.dispatch({ type: "ATTACH_INI_CONFIG", iniConfig: xyzacIniConfig });
|
||||||
|
assert.equal(fixtureInitialStore.getState().iniConfigReadiness.loaded, true);
|
||||||
|
assert.equal(fixtureInitialStore.getState().iniConfigReadiness.ready, true);
|
||||||
|
assert.equal(fixtureInitialStore.getState().profile.axisLimits.X.max, 200);
|
||||||
|
fixtureInitialStore.dispatch({ type: "TOGGLE_POWER" });
|
||||||
|
fixtureInitialStore.dispatch({ type: "HOME" });
|
||||||
|
fixtureInitialStore.dispatch({ type: "SET_MODE", mode: "mdi" });
|
||||||
|
fixtureInitialStore.dispatch({ type: "RUN_MDI", command: "G90 X999 Y-999 Z999 A999 C99999" });
|
||||||
|
assert.equal(fixtureInitialStore.getState().axisPose.x, 200);
|
||||||
|
assert.equal(fixtureInitialStore.getState().axisPose.y, -100);
|
||||||
|
assert.equal(fixtureInitialStore.getState().axisPose.z, 120);
|
||||||
|
assert.equal(fixtureInitialStore.getState().axisPose.a, 50);
|
||||||
|
assert.equal(fixtureInitialStore.getState().axisPose.c, 36000);
|
||||||
|
|
||||||
const kinematicsStore = createSimulationStore();
|
const kinematicsStore = createSimulationStore();
|
||||||
kinematicsStore.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime });
|
kinematicsStore.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime });
|
||||||
@@ -74,6 +120,9 @@ assert.equal(kinematicsState.linuxCncBoundaryAdapter.interpreterRuntimeReady, fa
|
|||||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.ready, true);
|
assert.equal(kinematicsState.linuxCncBoundaryReadiness.ready, true);
|
||||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.fullLinuxCncProgramExecutionReady, false);
|
assert.equal(kinematicsState.linuxCncBoundaryReadiness.fullLinuxCncProgramExecutionReady, false);
|
||||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.missing.includes("interpreter/remap runtime"), true);
|
assert.equal(kinematicsState.linuxCncBoundaryReadiness.missing.includes("interpreter/remap runtime"), true);
|
||||||
|
assert.equal(kinematicsState.fullExecutionBoundary.satisfied.includes("linuxcnc-kinematics-wasm"), true);
|
||||||
|
assert.equal(kinematicsState.fullExecutionBoundary.missing.includes("linuxcnc interpreter WASM runtime"), true);
|
||||||
|
assert.equal(kinematicsState.kinematicsExecutionContext, "direct");
|
||||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||||
assert.equal(kinematicsState.rtcpFrame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
assert.equal(kinematicsState.rtcpFrame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||||
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, true);
|
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, true);
|
||||||
@@ -86,27 +135,159 @@ assert.equal(kinematicsState.lastKinematicsResult.moduleId, "xyzac-trt");
|
|||||||
assert.equal(kinematicsState.dro.tcpX, kinematicsState.rtcpFrame.tcpPose.x);
|
assert.equal(kinematicsState.dro.tcpX, kinematicsState.rtcpFrame.tcpPose.x);
|
||||||
|
|
||||||
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
|
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
|
||||||
|
kinematicsStore.dispatch({ type: "HOME" });
|
||||||
|
kinematicsStore.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||||
kinematicsStore.dispatch({ type: "STEP" });
|
kinematicsStore.dispatch({ type: "STEP" });
|
||||||
kinematicsState = kinematicsStore.getState();
|
kinematicsState = kinematicsStore.getState();
|
||||||
assert.equal(kinematicsState.runState, "stepping");
|
assert.equal(kinematicsState.runState, "stepping");
|
||||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||||
assert.equal(kinematicsState.rtcpFrame.readiness.fullLinuxCncProgramExecutionReady, false);
|
assert.equal(kinematicsState.rtcpFrame.readiness.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime: interpreterRuntime });
|
||||||
|
kinematicsStore.dispatch({
|
||||||
|
type: "LOAD_PROGRAM",
|
||||||
|
filename: "linuxcnc-canonical-demo.ngc",
|
||||||
|
content: [
|
||||||
|
"G90 G17",
|
||||||
|
"G0 X0 Y0 Z0",
|
||||||
|
"G1 X10 Y2 F100",
|
||||||
|
"G1 X12 Y4 A5 C7",
|
||||||
|
"M2",
|
||||||
|
].join("\n"),
|
||||||
|
});
|
||||||
|
kinematicsState = await waitForInterpreterExecution(kinematicsStore);
|
||||||
|
assert.equal(kinematicsState.linuxCncBoundaryAdapter.linuxCncInterpreterReady, true);
|
||||||
|
assert.equal(kinematicsState.linuxCncBoundaryReadiness.linuxCncInterpreterReady, true);
|
||||||
|
assert.equal(kinematicsState.programExecutionSourceMode, "linuxcnc-interpreter-wasm");
|
||||||
|
assert.equal(kinematicsState.programExecution.summary.motionEventCount >= 3, true);
|
||||||
|
assert.equal(kinematicsState.programExecutionTiming.motionCount >= 3, true);
|
||||||
|
assert.equal(kinematicsState.programExecutionTiming.sampleCount >= 3, true);
|
||||||
|
assert.equal(kinematicsState.programExecutionTiming.totalSeconds > 0, true);
|
||||||
|
assert.equal(kinematicsState.programExecutionTiming.semanticBoundary, "linuxcnc_tp_queue_runtime_timing_from_canonical_motion");
|
||||||
|
assert.equal(kinematicsState.programExecution.summary.plannerRuntimeReady, true);
|
||||||
|
assert.equal(kinematicsState.fullExecutionBoundary.plannerRuntimeReady, true);
|
||||||
|
assert.equal(kinematicsState.programExecution.summary.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
assert.equal(kinematicsState.fullExecutionBoundary.readyForUiSimulation, true);
|
||||||
|
assert.equal(kinematicsState.fullExecutionBoundary.machineFileBackedRemapReady, false);
|
||||||
|
assert.equal(kinematicsState.fullExecutionBoundary.fullLinuxCncProgramExecutionReady, false);
|
||||||
|
assert.equal(
|
||||||
|
kinematicsState.fullExecutionBoundary.semanticBoundary,
|
||||||
|
"linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked",
|
||||||
|
);
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({ type: "STEP" });
|
||||||
|
kinematicsState = kinematicsStore.getState();
|
||||||
|
assert.equal(kinematicsState.programExecutionMotionIndex, 1);
|
||||||
|
assert.equal(kinematicsState.activeLine, kinematicsState.programExecution.motion[1].line);
|
||||||
|
assert.equal(kinematicsState.programRuntimeFeedback.sourceMode, "linuxcnc-tp-runtime-sample");
|
||||||
|
assert.equal(kinematicsState.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_cycle_feedback_without_hardware");
|
||||||
|
assert.equal(kinematicsState.axisPose.x, kinematicsState.programRuntimeFeedback.axisPose.x);
|
||||||
|
assert.notEqual(kinematicsState.axisPose.x, kinematicsState.programExecution.motion[1].axes.x);
|
||||||
|
assert.equal(kinematicsState.programRuntimeFeedback.queueDepth >= 0, true);
|
||||||
|
assert.equal(kinematicsState.programElapsedSeconds > 0, true);
|
||||||
|
assert.equal(kinematicsState.programRemainingSeconds >= 0, true);
|
||||||
|
assert.equal(kinematicsState.feed.currentVelocity > 0, true);
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({
|
||||||
|
type: "LOAD_PROGRAM",
|
||||||
|
filename: "linuxcnc-switchkins-rtcp-demo.ngc",
|
||||||
|
content: [
|
||||||
|
"G90 G17",
|
||||||
|
"M428",
|
||||||
|
"G0 X0 Y0 Z0 A0 C0",
|
||||||
|
"G1 X10 Y2 Z-1 A15 C30 F120",
|
||||||
|
"M429",
|
||||||
|
"G1 X0 Y0 Z0 A0 C0 F120",
|
||||||
|
"M2",
|
||||||
|
].join("\n"),
|
||||||
|
});
|
||||||
|
kinematicsState = await waitForInterpreterExecution(kinematicsStore);
|
||||||
|
assert.equal(kinematicsState.programExecutionSourceMode, "linuxcnc-interpreter-wasm");
|
||||||
|
assert.equal(kinematicsState.programExecution.summary.switchkinsEventCount, 2);
|
||||||
|
assert.equal(kinematicsState.programExecution.motion[0].switchkinsType, 1);
|
||||||
|
assert.equal(kinematicsState.kinsType, "tcp-xyzac");
|
||||||
|
assert.equal(kinematicsState.rtcpState, "on");
|
||||||
|
assert.equal(kinematicsState.rtcpFrame.kinematicsSwitchkinsType, 1);
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({ type: "STEP" });
|
||||||
|
kinematicsState = kinematicsStore.getState();
|
||||||
|
assert.equal(kinematicsState.programExecutionMotionIndex, 1);
|
||||||
|
assert.equal(kinematicsState.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_cycle_feedback_without_hardware");
|
||||||
|
assert.equal(kinematicsState.axisPose.a > 0 && kinematicsState.axisPose.a < 15, true);
|
||||||
|
assert.equal(kinematicsState.axisPose.c > 0 && kinematicsState.axisPose.c < 30, true);
|
||||||
|
assert.equal(kinematicsState.programRuntimeFeedback.distanceToGo > 0, true);
|
||||||
|
assert.equal(kinematicsState.kinsType, "tcp-xyzac");
|
||||||
|
assert.equal(kinematicsState.rtcpState, "on");
|
||||||
|
assert.equal(kinematicsState.rtcpFrame.kinematicsSwitchkinsType, 1);
|
||||||
|
const switchkinsStepSampleIndex = kinematicsState.programExecutionSampleIndex;
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({ type: "RESUME" });
|
||||||
|
kinematicsStore.dispatch({ type: "RUN" });
|
||||||
|
kinematicsState = kinematicsStore.getState();
|
||||||
|
assert.equal(kinematicsState.programExecutionSampleIndex > switchkinsStepSampleIndex, true);
|
||||||
|
assert.equal(kinematicsState.programExecutionMotionIndex, 1);
|
||||||
|
assert.equal(kinematicsState.programRuntimeFeedback.semanticBoundary, "linuxcnc_tp_run_cycle_feedback_without_hardware");
|
||||||
|
assert.equal(kinematicsState.programRuntimeFeedback.distanceToGo > 0, true);
|
||||||
|
assert.equal(kinematicsState.kinsType, "tcp-xyzac");
|
||||||
|
assert.equal(kinematicsState.rtcpState, "on");
|
||||||
|
assert.equal(kinematicsState.rtcpFrame.kinematicsSwitchkinsType, 1);
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({ type: "PAUSE" });
|
||||||
|
kinematicsState = kinematicsStore.getState();
|
||||||
|
assert.equal(kinematicsState.runState, "paused");
|
||||||
|
assert.equal(kinematicsState.machine.interpState, "paused");
|
||||||
|
assert.equal(kinematicsState.machine.taskPaused, true);
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({ type: "RUN" });
|
||||||
|
kinematicsState = kinematicsStore.getState();
|
||||||
|
assert.equal(kinematicsState.operatorMessage, "run blocked: resume paused program first");
|
||||||
|
assert.equal(kinematicsState.runState, "paused");
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({ type: "RESUME" });
|
||||||
|
kinematicsState = kinematicsStore.getState();
|
||||||
|
assert.equal(kinematicsState.runState, "running");
|
||||||
|
assert.equal(kinematicsState.machine.interpState, "reading");
|
||||||
|
assert.equal(kinematicsState.machine.taskPaused, false);
|
||||||
|
|
||||||
|
kinematicsStore.dispatch({ type: "STOP" });
|
||||||
|
kinematicsState = kinematicsStore.getState();
|
||||||
|
assert.equal(kinematicsState.runState, "stopped");
|
||||||
|
assert.equal(kinematicsState.machine.interpState, "idle");
|
||||||
|
|
||||||
kinematicsStore.dispatch({ type: "SET_FRAME_SOURCE", sourceMode: "fixture-ui-only" });
|
kinematicsStore.dispatch({ type: "SET_FRAME_SOURCE", sourceMode: "fixture-ui-only" });
|
||||||
kinematicsState = kinematicsStore.getState();
|
kinematicsState = kinematicsStore.getState();
|
||||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "fixture-ui-only");
|
assert.equal(kinematicsState.rtcpFrame.sourceMode, "fixture-ui-only");
|
||||||
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, false);
|
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, false);
|
||||||
|
|
||||||
const store = createSimulationStore();
|
const store = createSimulationStore();
|
||||||
|
let state;
|
||||||
|
assert.equal(store.getState().availableProfiles.length, 2);
|
||||||
|
store.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
|
||||||
|
assert.equal(store.getState().machineProfile, "xyzbc-trt");
|
||||||
|
assert.equal(store.getState().profile.traj.coordinates, "XYZBC");
|
||||||
|
const xyzbcRuntime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzbc-trt" });
|
||||||
|
store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime: xyzbcRuntime });
|
||||||
|
store.dispatch({ type: "SET_RTCP", enabled: true });
|
||||||
|
state = store.getState();
|
||||||
|
assert.equal(state.kinsType, "tcp-xyzbc");
|
||||||
|
assert.equal(state.rtcpFrame.kinematicsModuleId, "xyzbc-trt");
|
||||||
|
assert.equal(state.rtcpFrame.jointPose[3].axis, "B");
|
||||||
|
|
||||||
|
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
|
||||||
store.dispatch({ type: "RUN" });
|
store.dispatch({ type: "RUN" });
|
||||||
assert.equal(store.getState().activeLine, 501);
|
assert.equal(store.getState().activeLine, 501);
|
||||||
assert.equal(store.getState().operatorMessage, "run blocked: power or estop state");
|
assert.equal(store.getState().operatorMessage, "run blocked: machine must be on");
|
||||||
|
|
||||||
store.dispatch({ type: "TOGGLE_POWER" });
|
store.dispatch({ type: "TOGGLE_POWER" });
|
||||||
assert.equal(store.getState().machine.powerOn, true);
|
assert.equal(store.getState().machine.powerOn, true);
|
||||||
|
store.dispatch({ type: "HOME" });
|
||||||
|
assert.equal(store.getState().machine.allHomed, true);
|
||||||
|
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||||
|
assert.equal(store.getState().linuxCncTaskPolicy.canRunAuto, true);
|
||||||
|
assert.equal(store.getState().linuxCncTaskPolicy.canExecuteMdi, false);
|
||||||
|
|
||||||
store.dispatch({ type: "SET_RTCP", enabled: true });
|
store.dispatch({ type: "SET_RTCP", enabled: true });
|
||||||
let state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.rtcpState, "on");
|
assert.equal(state.rtcpState, "on");
|
||||||
assert.equal(state.kinsType, "tcp-xyzac");
|
assert.equal(state.kinsType, "tcp-xyzac");
|
||||||
assert.equal(state.rtcpFrame.readiness.linuxCncKinematicsReady, false);
|
assert.equal(state.rtcpFrame.readiness.linuxCncKinematicsReady, false);
|
||||||
@@ -120,24 +301,55 @@ assert.equal(state.activeLine, previousLine + 1);
|
|||||||
assert.equal(state.runState, "stepping");
|
assert.equal(state.runState, "stepping");
|
||||||
assert.notEqual(state.tcpPose.x, previousTcpX);
|
assert.notEqual(state.tcpPose.x, previousTcpX);
|
||||||
|
|
||||||
|
store.dispatch({ type: "STOP" });
|
||||||
|
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||||
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 2 });
|
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 2 });
|
||||||
state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.machine.mode, "jog");
|
assert.equal(state.machine.mode, "manual");
|
||||||
assert.equal(state.runState, "jogging");
|
assert.equal(state.runState, "jogging");
|
||||||
assert.equal(Math.round(state.axisPose.x), Math.round(state.rtcpFrame.axisPose.x));
|
assert.equal(Math.round(state.axisPose.x), Math.round(state.rtcpFrame.axisPose.x));
|
||||||
|
|
||||||
|
store.dispatch({ type: "SET_MODE", mode: "mdi" });
|
||||||
store.dispatch({ type: "RUN_MDI", command: "G0 X1" });
|
store.dispatch({ type: "RUN_MDI", command: "G0 X1" });
|
||||||
state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.machine.mode, "mdi");
|
assert.equal(state.machine.mode, "mdi");
|
||||||
assert.equal(state.machine.mdiCommand, "G0 X1");
|
assert.equal(state.machine.mdiCommand, "G0 X1");
|
||||||
assert.equal(state.runState, "mdi");
|
assert.equal(state.runState, "mdi");
|
||||||
|
assert.equal(state.axisPose.x, 1);
|
||||||
|
assert.equal(state.programSource, "operator-mdi");
|
||||||
|
assert.equal(state.programLines[0], "G0 X1");
|
||||||
|
assert.equal(state.mdiHistory[0], "G0 X1");
|
||||||
|
|
||||||
|
const mdiRelativeBase = store.getState().axisPose;
|
||||||
|
store.dispatch({ type: "RUN_MDI", command: "G91 X2 Y-3 F1200 M3 S2400 M8" });
|
||||||
|
state = store.getState();
|
||||||
|
assert.equal(state.machine.mdiDistanceMode, "relative");
|
||||||
|
assert.equal(state.axisPose.x, mdiRelativeBase.x + 2);
|
||||||
|
assert.equal(state.axisPose.y, mdiRelativeBase.y - 3);
|
||||||
|
assert.equal(state.feed.feedRate, 1200);
|
||||||
|
assert.equal(state.spindle.enabled, true);
|
||||||
|
assert.equal(state.spindle.rpm, 2400);
|
||||||
|
assert.equal(state.coolant.flood, true);
|
||||||
|
|
||||||
|
store.dispatch({ type: "RUN_MDI", command: "M428" });
|
||||||
|
state = store.getState();
|
||||||
|
assert.equal(state.kinsType, "tcp-xyzac");
|
||||||
|
assert.equal(state.rtcpState, "on");
|
||||||
|
|
||||||
|
store.dispatch({ type: "RUN_MDI", command: "M429 M9 M5" });
|
||||||
|
state = store.getState();
|
||||||
|
assert.equal(state.kinsType, "identity");
|
||||||
|
assert.equal(state.rtcpState, "off");
|
||||||
|
assert.equal(state.coolant.flood, false);
|
||||||
|
assert.equal(state.coolant.mist, false);
|
||||||
|
assert.equal(state.spindle.enabled, false);
|
||||||
|
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: "LOAD_PROGRAM",
|
type: "LOAD_PROGRAM",
|
||||||
filename: "operator-demo.ngc",
|
filename: "operator-demo.ngc",
|
||||||
content: "G0 X0 Y0\nG1 X10 F100\nM30\n",
|
content: "G0 X0 Y0\nG1 X10 F100\nM30\n",
|
||||||
});
|
});
|
||||||
state = store.getState();
|
state = await waitForInterpreterExecution(store);
|
||||||
assert.equal(state.activeProgram, "operator-demo.ngc");
|
assert.equal(state.activeProgram, "operator-demo.ngc");
|
||||||
assert.equal(state.programSource, "operator-file");
|
assert.equal(state.programSource, "operator-file");
|
||||||
assert.equal(state.programStartLine, 1);
|
assert.equal(state.programStartLine, 1);
|
||||||
@@ -169,13 +381,15 @@ store.dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: 10 });
|
|||||||
state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.spindle.override, 110);
|
assert.equal(state.spindle.override, 110);
|
||||||
|
|
||||||
|
const floodBeforeToggle = store.getState().coolant.flood;
|
||||||
store.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
|
store.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
|
||||||
state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.coolant.flood, false);
|
assert.equal(state.coolant.flood, !floodBeforeToggle);
|
||||||
|
|
||||||
|
const mistBeforeToggle = store.getState().coolant.mist;
|
||||||
store.dispatch({ type: "TOGGLE_COOLANT", kind: "mist" });
|
store.dispatch({ type: "TOGGLE_COOLANT", kind: "mist" });
|
||||||
state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.coolant.mist, true);
|
assert.equal(state.coolant.mist, !mistBeforeToggle);
|
||||||
|
|
||||||
store.dispatch({ type: "SET_VIEW", view: "x" });
|
store.dispatch({ type: "SET_VIEW", view: "x" });
|
||||||
state = store.getState();
|
state = store.getState();
|
||||||
@@ -195,6 +409,7 @@ store.dispatch({ type: "TOGGLE_FULLSCREEN" });
|
|||||||
state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.preview.fullscreen, true);
|
assert.equal(state.preview.fullscreen, true);
|
||||||
|
|
||||||
|
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||||
store.dispatch({ type: "HOME" });
|
store.dispatch({ type: "HOME" });
|
||||||
state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.axisPose.x, 43);
|
assert.equal(state.axisPose.x, 43);
|
||||||
@@ -209,6 +424,8 @@ assert.equal(state.runState, "estopped");
|
|||||||
store.dispatch({ type: "RESET" });
|
store.dispatch({ type: "RESET" });
|
||||||
state = store.getState();
|
state = store.getState();
|
||||||
assert.equal(state.machine.estopActive, false);
|
assert.equal(state.machine.estopActive, false);
|
||||||
assert.equal(state.operatorMessage, "machine reset complete");
|
assert.equal(state.machine.powerOn, false);
|
||||||
|
assert.equal(state.machine.taskState, "estop-reset");
|
||||||
|
assert.equal(state.operatorMessage, "estop reset; machine off");
|
||||||
|
|
||||||
console.log("rtcp_store_smoke=ok");
|
console.log("rtcp_store_smoke=ok");
|
||||||
|
|||||||
Reference in New Issue
Block a user