|
| 1 | +component pushmsg "Push configurable RT messages to non-RT logging on rising edges"; |
| 2 | + |
| 3 | +description """ |
| 4 | +Each entry in the `msgs` modparam describes a level, pin name, and message |
| 5 | +text using `level|pinname|message` syntax. Level is one letter: |
| 6 | +`e` (error), `w` (warning), `i` (info), `d` (debug). On the rising edge of |
| 7 | +`pushmsg.<pinname>` the corresponding text is emitted via `rtapi_print_msg()` |
| 8 | +at the chosen level. |
| 9 | + |
| 10 | +Pin names use HAL-allowed characters (alphanumeric, dot, dash, underscore). |
| 11 | +Maximum 64 entries per loadrt, 2 substitutions per message, 128 chars per |
| 12 | +message. |
| 13 | + |
| 14 | +Message text may include `{t:refname}` placeholders. Each one creates a |
| 15 | +typed input pin `pushmsg.<refname>` that the user wires to the source |
| 16 | +signal. Type letter selects the pin type: |
| 17 | +`b` => bit, `s` => s32, `u` => u32, `l` => s64, `k` => u64, `f` => float. |
| 18 | +Use `\\{` to embed a literal curly brace. |
| 19 | + |
| 20 | +Example: |
| 21 | +[source,hal] |
| 22 | +---- |
| 23 | +loadrt pushmsg msgs="e|nooil|No oil pressure","w|low|Oil {f:level} L low" |
| 24 | +addf pushmsg servo-thread |
| 25 | +net no-oil classicladder.0.out-21 pushmsg.nooil |
| 26 | +net oil-level oil.level pushmsg.level |
| 27 | +---- |
| 28 | + |
| 29 | +Example INI expansion: |
| 30 | +[source,ini] |
| 31 | +---- |
| 32 | +[PUSHMSG] |
| 33 | +# Note: no surrounding whitespace |
| 34 | +MSGS=\\ |
| 35 | +"e|nooil|No oil pressure",\\ |
| 36 | +"w|low|Oil {f:level} L low" |
| 37 | +---- |
| 38 | +[source,hal] |
| 39 | +---- |
| 40 | +loadrt pushmsg msgs=[PUSHMSG]MSGS |
| 41 | +---- |
| 42 | +"""; |
| 43 | + |
| 44 | +pin in bit enable = TRUE "Enable message generation"; |
| 45 | + |
| 46 | +modparam dummy msgs "Comma separated list of custom messages in format t|pin|message"; |
| 47 | + |
| 48 | +option period no; |
| 49 | +option extra_setup yes; |
| 50 | +option extra_cleanup yes; |
| 51 | +option singleton yes; |
| 52 | + |
| 53 | +function _; |
| 54 | +license "GPL"; // v2+ |
| 55 | +author "LinuxCNC"; |
| 56 | + |
| 57 | +include <rtapi_ctype.h>; |
| 58 | +include <rtapi_gfp.h>; |
| 59 | +include <rtapi_slab.h>; |
| 60 | + |
| 61 | +;; |
| 62 | + |
| 63 | +#define MSG_LEN_MAX 128 // Max size of a message at input and expanded |
| 64 | +#define MSG_N_MAX 64 // Max number of messages supported |
| 65 | +#define MSG_SUBST_MAX 2 // Max number of substitutions within one message |
| 66 | + |
| 67 | +// Command-line config parameter |
| 68 | +static char *msgs[MSG_N_MAX] = {}; |
| 69 | +RTAPI_MP_ARRAY_STRING(msgs, MSG_N_MAX, "Message slots in 't|pin|message' format"); |
| 70 | + |
| 71 | +// The HAL structure lives in HAL memory space |
| 72 | +typedef struct { |
| 73 | + hal_bit_t *trigger; |
| 74 | + hal_data_u *substs[MSG_SUBST_MAX]; |
| 75 | +} msg_hal_t; |
| 76 | + |
| 77 | +// The admin structure lives in normal memory space |
| 78 | +typedef struct { |
| 79 | + bool prev; // Previous pin state (to detect rising edge) |
| 80 | + int msgtype; // Rtapi message type (err, warn, info, debug) |
| 81 | + char msgbuf[MSG_LEN_MAX]; // Complete message with embedded NULs for substitution |
| 82 | + int msglen; // Length so we may find the end easily (stpcpy is not available) |
| 83 | + hal_type_t substtype[MSG_SUBST_MAX]; // Substitution types (hal type enum) |
| 84 | + const char *parts[MSG_SUBST_MAX]; // Text parts index msgbuf for after substitution |
| 85 | +} msg_slot_t; |
| 86 | + |
| 87 | +// |
| 88 | +// The message structure is decomposed from the source: |
| 89 | +// msgs[n] = "e|trigpin|Message {s:pin1} othertext {u:pin2} trailing" |
| 90 | +// |
| 91 | +// The source is copied into the slots->msgbuf and all pin refs are isolated: |
| 92 | +// msgbuf = "Message \0s:pin1\0 othertext \0u:pin2\0 trailing" |
| 93 | +// ^ ^ |
| 94 | +// parts[0] parts[1] |
| 95 | +// The slots->parts[] array is always the trailing part after the substitution. |
| 96 | +// The pins for the substitution variable are referenced in the pins->substs[] |
| 97 | +// array. |
| 98 | +// |
| 99 | + |
| 100 | +static msg_hal_t *pins; // Trigger and substitution pins |
| 101 | +static msg_slot_t *slots; // Admin data for each message |
| 102 | +static int nslots; // Number of active messages |
| 103 | + |
| 104 | +static inline char printable(char c) |
| 105 | +{ |
| 106 | + return isprint(c & 0xff) ? c : '?'; |
| 107 | +} |
| 108 | + |
| 109 | +static int message_type(char c) |
| 110 | +{ |
| 111 | + switch (c) { |
| 112 | + case 'e': case 'E': return RTAPI_MSG_ERR; |
| 113 | + case 'w': case 'W': return RTAPI_MSG_WARN; |
| 114 | + case 'i': case 'I': return RTAPI_MSG_INFO; |
| 115 | + case 'd': case 'D': return RTAPI_MSG_DBG; |
| 116 | + } |
| 117 | + return -1; |
| 118 | +} |
| 119 | + |
| 120 | +static int check_pinname(const char *name) |
| 121 | +{ |
| 122 | + for (; *name; name++) { |
| 123 | + if (!isascii(*name & 0xff) || (!isalnum(*name & 0xff) && NULL == strchr("._-", *name & 0xff))) |
| 124 | + return -EINVAL; |
| 125 | + } |
| 126 | + return 0; |
| 127 | +} |
| 128 | + |
| 129 | +static int setup_message(const char *pfx, int idx, const char *msg, msg_slot_t *slot, msg_hal_t *pin) |
| 130 | +{ |
| 131 | + int pfxlen = strlen(pfx); |
| 132 | + int l = strlen(msg); |
| 133 | + if (l >= MSG_LEN_MAX) { |
| 134 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d too long, more than %d characters\n", pfx, idx, MSG_LEN_MAX-1); |
| 135 | + return -EMSGSIZE; |
| 136 | + } |
| 137 | + // Absolute minimum is "t|p|" |
| 138 | + if (l < 4) { |
| 139 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d too short, less than 4 characters\n", pfx, idx); |
| 140 | + return -EMSGSIZE; |
| 141 | + } |
| 142 | + // Get the message type |
| 143 | + if ((slot->msgtype = message_type(msg[0])) < 0) { |
| 144 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d has invalid type '%c', expected one of {e,w,i,d}\n", |
| 145 | + pfx, idx, printable(msg[0])); |
| 146 | + return -EBADMSG; |
| 147 | + } |
| 148 | + // Message type must be followed by a '|' |
| 149 | + if ('|' != msg[1]) { |
| 150 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d missing first '|' separator\n", pfx, idx); |
| 151 | + return -EBADMSG; |
| 152 | + } |
| 153 | + // Find the second '|' in "t|name|message" |
| 154 | + const char *pinend = strchr(msg+2, '|'); |
| 155 | + if (!pinend) { |
| 156 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d missing pin name, no second '|' separator\n", pfx, idx); |
| 157 | + return -EBADMSG; |
| 158 | + } |
| 159 | + // Make sure we can encompass the full name |
| 160 | + int pinnamelen = pinend - msg - 2; |
| 161 | + if (pinnamelen + pfxlen + 1 >= HAL_NAME_LEN) { // +1 for the extra '.' character |
| 162 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d pin name too long\n", pfx, idx); |
| 163 | + return -EBADMSG; |
| 164 | + } |
| 165 | + |
| 166 | + int rv; |
| 167 | + // Copy the pinname for later pin creation use |
| 168 | + char pinname[HAL_NAME_LEN+1] = {}; // The init ensures termination |
| 169 | + memcpy(pinname, &msg[2], pinnamelen); |
| 170 | + if ((rv = check_pinname(pinname)) < 0) { |
| 171 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d pin name contains bad characters\n", pfx, idx); |
| 172 | + return rv; |
| 173 | + } |
| 174 | + // Create the trigger input pin |
| 175 | + if ((rv = hal_pin_bit_newf(HAL_IN, &pin->trigger, comp_id, "%s.%s", pfx, pinname)) < 0) { |
| 176 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d cannot create pin (duplicate?)\n", pfx, idx); |
| 177 | + return rv; |
| 178 | + } |
| 179 | + |
| 180 | + strcpy(slot->msgbuf, msg + pinnamelen + 3); // Copy message so we may take it apart |
| 181 | + |
| 182 | + // Now see if there are any expansions in the string. |
| 183 | + int subst = 0; // Count number of substitutions |
| 184 | + for (char *cptr = slot->msgbuf; *cptr; cptr++) { |
| 185 | + // Check "\{..." escaped curly open brace |
| 186 | + if ('\\' == *cptr) { |
| 187 | + // The next char is escaped and cannot start a substitution |
| 188 | + if (cptr[1]) // Or this is a backslash at the end |
| 189 | + cptr++; |
| 190 | + continue; |
| 191 | + } |
| 192 | + if ('{' == *cptr) { |
| 193 | + // Need "{t:pinname}" |
| 194 | + *cptr = 0; // Terminate the previous string "foo {t:pin} bar" --> "foo " |
| 195 | + cptr++; // Move to subst internals |
| 196 | + char *end = strchr(cptr, '}'); |
| 197 | + if (!end) { |
| 198 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d missing '}' in substitution\n", pfx, idx); |
| 199 | + return -EBADMSG; |
| 200 | + } |
| 201 | + slot->parts[subst] = end + 1; // This is the continuation position "t:pin} bar" --> " bar" |
| 202 | + *end = 0; // Terminate the pinname "t:pin} bar" --> "t:pin" |
| 203 | + |
| 204 | + // Min content is 3 chars: type + ':' + 1-char-name. end-cptr counts those bytes. |
| 205 | + if (end - cptr < 3) { |
| 206 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d substitution too small, need type and pinname\n", pfx, idx); |
| 207 | + return -EBADMSG; |
| 208 | + } |
| 209 | + if (':' != cptr[1]) { |
| 210 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d substitution missing ':' after type character\n", pfx, idx); |
| 211 | + return -EBADMSG; |
| 212 | + } |
| 213 | + |
| 214 | + char tchar = *cptr; |
| 215 | + cptr += 2; // Move to pinname |
| 216 | + if ((end - cptr) + pfxlen + 1 >= HAL_NAME_LEN) { // +1 for the extra '.' character |
| 217 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d substitution pin name too long\n", pfx, idx); |
| 218 | + return -EBADMSG; |
| 219 | + } |
| 220 | + if ((rv = check_pinname(cptr)) < 0) { |
| 221 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d substitution pin name contains bad characters\n", pfx, idx); |
| 222 | + return rv; |
| 223 | + } |
| 224 | + |
| 225 | + // Create the correct pin based on the type letter |
| 226 | + // Unfortunately, we cannot determine whether a pin has a duplicate |
| 227 | + // name when pin creation fails. The return value is not specific |
| 228 | + // enough. We'd have to track all pin names ourselves if we wanted |
| 229 | + // to merge pin substitution references into one pool. |
| 230 | + switch (tchar) { |
| 231 | + case 'b': case 'B': |
| 232 | + slot->substtype[subst] = HAL_BIT; |
| 233 | + if ((rv = hal_pin_bit_newf(HAL_IN, (hal_bit_t **)&pin->substs[subst], comp_id, "%s.%s", pfx, cptr)) < 0) |
| 234 | + return rv; |
| 235 | + break; |
| 236 | + case 's': case 'S': |
| 237 | + slot->substtype[subst] = HAL_S32; |
| 238 | + if ((rv = hal_pin_s32_newf(HAL_IN, (hal_s32_t **)&pin->substs[subst], comp_id, "%s.%s", pfx, cptr)) < 0) |
| 239 | + return rv; |
| 240 | + break; |
| 241 | + case 'u': case 'U': |
| 242 | + slot->substtype[subst] = HAL_U32; |
| 243 | + if ((rv = hal_pin_u32_newf(HAL_IN, (hal_u32_t **)&pin->substs[subst], comp_id, "%s.%s", pfx, cptr)) < 0) |
| 244 | + return rv; |
| 245 | + break; |
| 246 | + case 'l': case 'L': |
| 247 | + slot->substtype[subst] = HAL_S64; |
| 248 | + if ((rv = hal_pin_s64_newf(HAL_IN, (hal_s64_t **)&pin->substs[subst], comp_id, "%s.%s", pfx, cptr)) < 0) |
| 249 | + return rv; |
| 250 | + break; |
| 251 | + case 'k': case 'K': |
| 252 | + slot->substtype[subst] = HAL_U64; |
| 253 | + if ((rv = hal_pin_u64_newf(HAL_IN, (hal_u64_t **)&pin->substs[subst], comp_id, "%s.%s", pfx, cptr)) < 0) |
| 254 | + return rv; |
| 255 | + break; |
| 256 | + case 'f': case 'F': |
| 257 | + slot->substtype[subst] = HAL_FLOAT; |
| 258 | + if ((rv = hal_pin_float_newf(HAL_IN, (hal_float_t **)&pin->substs[subst], comp_id, "%s.%s", pfx, cptr)) < 0) |
| 259 | + return rv; |
| 260 | + break; |
| 261 | + default: |
| 262 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: Message %d bad substitution type '%c', expected one of {b,f,s,u,k,l}\n", |
| 263 | + pfx, idx, printable(tchar)); |
| 264 | + return -EBADMSG; |
| 265 | + } |
| 266 | + |
| 267 | + // Do not perform more substitutions than allowed |
| 268 | + subst++; |
| 269 | + if (subst >= MSG_SUBST_MAX) |
| 270 | + break; |
| 271 | + |
| 272 | + cptr = end; // Move to end of substitution (loop will increment to next char) |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + // Finally set the first part's length for quick concat in print_slot() |
| 277 | + slot->msglen = strlen(slot->msgbuf); |
| 278 | + return 0; |
| 279 | +} |
| 280 | + |
| 281 | +EXTRA_SETUP() |
| 282 | +{ |
| 283 | + (void)__comp_inst; |
| 284 | + (void)extra_arg; |
| 285 | + |
| 286 | + // See how many messages are set |
| 287 | + for (nslots = 0; nslots < MSG_N_MAX && msgs[nslots]; nslots++) { |
| 288 | + // Just counting. Drops out at the first NULL (unset msgs) |
| 289 | + rtapi_print_msg(RTAPI_MSG_DBG, "%s: msgs[%d]=%s\n", prefix, nslots, msgs[nslots]); |
| 290 | + } |
| 291 | + |
| 292 | + if (nslots < 1) { |
| 293 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: No messages to handle\n", prefix); |
| 294 | + return -EINVAL; |
| 295 | + } |
| 296 | + |
| 297 | + // Get memory for the HAL interface |
| 298 | + pins = (msg_hal_t *)hal_malloc(nslots * sizeof(pins[0])); |
| 299 | + if (!pins) { |
| 300 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: No HAL memory for pins\n", prefix); |
| 301 | + return -ENOMEM; |
| 302 | + } |
| 303 | + memset(pins, 0, nslots * sizeof(pins[0])); // Ensure zeros because we check NULLs |
| 304 | + |
| 305 | + // Get memory for the message slots |
| 306 | + slots = (msg_slot_t *)rtapi_kzalloc(nslots * sizeof(slots[0]), RTAPI_GFP_KERNEL); |
| 307 | + if (!slots) { |
| 308 | + rtapi_print_msg(RTAPI_MSG_ERR, "%s: No kernel memory for slots\n", prefix); |
| 309 | + return -ENOMEM; |
| 310 | + } |
| 311 | + |
| 312 | + // Interpret every message format and allocate pin resources |
| 313 | + for (int i = 0; i < nslots; i++) { |
| 314 | + int rv = setup_message(prefix, i, msgs[i], &slots[i], &pins[i]); |
| 315 | + if (rv < 0) { |
| 316 | + rtapi_kfree(slots); |
| 317 | + slots = NULL; |
| 318 | + return rv; |
| 319 | + } |
| 320 | + } |
| 321 | + return 0; |
| 322 | +} |
| 323 | + |
| 324 | +EXTRA_CLEANUP() |
| 325 | +{ |
| 326 | + if (slots) |
| 327 | + rtapi_kfree(slots); |
| 328 | +} |
| 329 | + |
| 330 | +static void print_slot(int s) |
| 331 | +{ |
| 332 | + char buf[MSG_LEN_MAX]; |
| 333 | + char *end = strcpy(buf, slots[s].msgbuf) + slots[s].msglen; |
| 334 | + for (int i = 0; i < MSG_SUBST_MAX && pins[s].substs[i]; i++) { |
| 335 | + int left = sizeof(buf) - (end - buf); |
| 336 | + int n; |
| 337 | + if (left <= 1) // Need room for terminator |
| 338 | + break; |
| 339 | + switch (slots[s].substtype[i]) { |
| 340 | + case HAL_BIT: |
| 341 | + n = rtapi_snprintf(end, left, "%d%s", (int)!!pins[s].substs[i]->b, slots[s].parts[i]); |
| 342 | + break; |
| 343 | + case HAL_S32: |
| 344 | + n = rtapi_snprintf(end, left, "%d%s", (int)pins[s].substs[i]->s, slots[s].parts[i]); |
| 345 | + break; |
| 346 | + case HAL_U32: |
| 347 | + n = rtapi_snprintf(end, left, "%u%s", (unsigned)pins[s].substs[i]->u, slots[s].parts[i]); |
| 348 | + break; |
| 349 | + case HAL_S64: |
| 350 | + n = rtapi_snprintf(end, left, "%ld%s", (long)pins[s].substs[i]->ls, slots[s].parts[i]); |
| 351 | + break; |
| 352 | + case HAL_U64: |
| 353 | + n = rtapi_snprintf(end, left, "%lu%s", (unsigned long)pins[s].substs[i]->lu, slots[s].parts[i]); |
| 354 | + break; |
| 355 | + case HAL_FLOAT: |
| 356 | + n = rtapi_snprintf(end, left, "%lf%s", (double)pins[s].substs[i]->f, slots[s].parts[i]); |
| 357 | + break; |
| 358 | + default: |
| 359 | + n = 0; |
| 360 | + break; |
| 361 | + } |
| 362 | + if (n < 0) { |
| 363 | + rtapi_print_msg(RTAPI_MSG_ERR, "pushmsg: Printing slot %d failed rtapi_snprintf() call\n", s); |
| 364 | + return; |
| 365 | + } |
| 366 | + end += n; |
| 367 | + } |
| 368 | + rtapi_print_msg(slots[s].msgtype, "%s\n", buf); |
| 369 | +} |
| 370 | + |
| 371 | +// Servo-thread cycle function |
| 372 | +FUNCTION(_) |
| 373 | +{ |
| 374 | + bool en = enable; // Cache enable pin |
| 375 | + // For each slot, test the trigger and save the trigger state |
| 376 | + for (int i = 0; i < nslots; i++) { |
| 377 | + bool trig = *(pins[i].trigger); |
| 378 | + if (en && trig && !slots[i].prev) { |
| 379 | + print_slot(i); // Rising edge -> print message |
| 380 | + } |
| 381 | + slots[i].prev = trig; |
| 382 | + } |
| 383 | +} |
0 commit comments