Files
can-utils/slcanpty.c
T
Oliver Hartkopp 1663fc32b3 slcanpty: use the SLCAN message length when parsing commands
pty2can() detected complete SLCAN messages by looking for the '\r'
termination but did not use this message length for parsing. The
content was read at fixed positions and the start of the following
message was derived from the parsed content.

Too short messages (e.g. 'T1\r') read the DLC and data from stale
buffer content and sent a phantom CAN frame. Following commands got
lost after short messages, unknown commands or frames with timestamp.

Terminate the current message at its '\r' position, check that the
CAN ID fits into the message and always continue with the next message
behind the '\r'.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Oliver Hartkopp <socketcan@hartkopp.net>
2026-10-14 11:15:54 +02:00

522 lines
11 KiB
C

/* SPDX-License-Identifier: GPL-2.0-only */
/*
* slcanpty: adapter for applications using the slcan ASCII protocol
*
* slcanpty.c - creates a pty for applications using the slcan ASCII protocol
* and converts the ASCII data to a CAN network interface (and vice versa)
*
* Copyright (c)2009 Oliver Hartkopp
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the version 2 of the GNU General Public License
* as published by the Free Software Foundation
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Send feedback to <linux-can@vger.kernel.org>
*
*/
#include <fcntl.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <linux/sockios.h>
#include "lib.h"
/* maximum rx buffer len: extended CAN frame with timestamp */
#define SLC_MTU (sizeof("T1111222281122334455667788EA5F\r") + 1)
#define DEVICE_NAME_PTMX "/dev/ptmx"
/* read data from pty, send CAN frames to CAN socket and answer commands */
static int pty2can(int pty, int socket, struct can_filter *fi,
int *is_open, int *tstamp)
{
unsigned int nbytes, tmp;
char cmd;
static char buf[200];
char replybuf[10]; /* for answers to received commands */
unsigned int ptr;
unsigned int msglen; /* length of the current SLCAN message w/o '\r' */
struct can_frame frame;
int ret, i;
static unsigned int rxoffset = 0; /* points to the end of an received incomplete SLCAN message */
ret = read(pty, &buf[rxoffset], sizeof(buf) - rxoffset - 1);
if (ret <= 0) {
/* ret == 0 : no error but pty descriptor has been closed */
if (ret < 0)
perror("read pty");
return 1;
}
nbytes = ret;
/* reset incomplete message offset */
nbytes += rxoffset;
rxoffset = 0;
rx_restart:
/* remove trailing '\r' characters to be robust against some apps */
while (buf[0] == '\r' && nbytes > 0) {
for (tmp = 0; tmp < nbytes; tmp++)
buf[tmp] = buf[tmp + 1];
nbytes--;
}
if (!nbytes)
return 0;
/* check if we can detect a complete SLCAN message including '\r' */
for (tmp = 0; tmp < nbytes; tmp++) {
if (buf[tmp] == '\r')
break;
}
/* no '\r' found in the message buffer? */
if (tmp == nbytes) {
/* save incomplete message */
rxoffset = nbytes;
/* leave here and read from pty again */
return 0;
}
msglen = tmp;
cmd = buf[0];
buf[nbytes] = 0;
for (tmp = 0; tmp < nbytes; tmp++)
if (buf[tmp] == '\r')
pr_debug("@");
else
pr_debug("%c", buf[tmp]);
pr_debug("\n");
/* terminate the current SLCAN message to not read the following content */
buf[msglen] = 0;
/* check for filter configuration commands */
if (cmd == 'm' || cmd == 'M') {
#if 0
/* the filter is no SocketCAN filter :-( */
/* TODO: behave like a SJA1000 controller specific filter */
if (cmd == 'm') {
fi->can_id = strtoul(buf+1,NULL,16);
fi->can_id &= CAN_EFF_MASK;
} else {
fi->can_mask = strtoul(buf+1,NULL,16);
fi->can_mask &= CAN_EFF_MASK;
}
if (*is_open)
setsockopt(socket, SOL_CAN_RAW,
CAN_RAW_FILTER, fi,
sizeof(struct can_filter));
#endif
goto rx_out_ack;
}
/* check for timestamp on/off command */
if (cmd == 'Z') {
*tstamp = buf[1] & 0x01;
goto rx_out_ack;
}
/* check for 'O'pen command */
if (cmd == 'O') {
setsockopt(socket, SOL_CAN_RAW, CAN_RAW_FILTER, fi, sizeof(struct can_filter));
*is_open = 1;
goto rx_out_ack;
}
/* check for 'C'lose command */
if (cmd == 'C') {
setsockopt(socket, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
*is_open = 0;
goto rx_out_ack;
}
/* check for 'V'ersion command */
if (cmd == 'V') {
sprintf(replybuf, "V1013\r");
tmp = strlen(replybuf);
goto rx_out;
}
/* check for 'v'ersion command */
if (cmd == 'v') {
sprintf(replybuf, "v1014\r");
tmp = strlen(replybuf);
goto rx_out;
}
/* check for serial 'N'umber command */
if (cmd == 'N') {
sprintf(replybuf, "N4242\r");
tmp = strlen(replybuf);
goto rx_out;
}
/* check for read status 'F'lags */
if (cmd == 'F') {
sprintf(replybuf, "F00\r");
tmp = strlen(replybuf);
goto rx_out;
}
/* correctly answer unsupported commands */
if (cmd == 'U') {
goto rx_out_ack;
}
if (cmd == 'S') {
goto rx_out_ack;
}
if (cmd == 's') {
goto rx_out_ack;
}
if (cmd == 'P' || cmd == 'A') {
goto rx_out_nack;
}
if (cmd == 'X') {
if (buf[1] & 0x01)
goto rx_out_ack;
else
goto rx_out_nack;
}
/* catch unknown commands */
if ((cmd != 't') && (cmd != 'T') && (cmd != 'r') && (cmd != 'R'))
goto rx_out_nack;
if (cmd & 0x20) /* tiny chars 'r' 't' => SFF */
ptr = 4; /* dlc position tiiid */
else
ptr = 9; /* dlc position Tiiiiiiiid */
/* the CAN ID has to fit into the current SLCAN message */
if (ptr > msglen)
goto rx_out_nack;
memset(&frame.data, 0, 8); /* clear data[] */
if ((cmd | 0x20) == 'r' && buf[ptr] != '0') {
/*
* RTR frame without dlc information!
* This is against the SLCAN spec but sent
* by a commercial CAN tool ... so we are
* robust against this protocol violation.
*/
frame.can_dlc = buf[ptr]; /* save following byte */
buf[ptr] = 0; /* terminate can_id string */
frame.can_id = strtoul(buf + 1, NULL, 16);
frame.can_id |= CAN_RTR_FLAG;
if (!(cmd & 0x20)) /* NO tiny chars => EFF */
frame.can_id |= CAN_EFF_FLAG;
buf[ptr] = frame.can_dlc; /* restore following byte */
frame.can_dlc = 0;
} else {
if (!(buf[ptr] >= '0' && buf[ptr] < '9'))
goto rx_out_nack;
frame.can_dlc = buf[ptr] - '0'; /* get dlc from ASCII val */
buf[ptr] = 0; /* terminate can_id string */
frame.can_id = strtoul(buf + 1, NULL, 16);
if (!(cmd & 0x20)) /* NO tiny chars => EFF */
frame.can_id |= CAN_EFF_FLAG;
if ((cmd | 0x20) == 'r') /* RTR frame */
frame.can_id |= CAN_RTR_FLAG;
for (i = 0, ptr++; i < frame.can_dlc; i++) {
tmp = asc2nibble(buf[ptr++]);
if (tmp > 0x0F)
goto rx_out_nack;
frame.data[i] = (tmp << 4);
tmp = asc2nibble(buf[ptr++]);
if (tmp > 0x0F)
goto rx_out_nack;
frame.data[i] |= tmp;
}
}
ret = write(socket, &frame, sizeof(frame));
if (ret != sizeof(frame)) {
perror("write socket");
return 1;
}
rx_out_ack:
replybuf[0] = '\r';
tmp = 1;
goto rx_out;
rx_out_nack:
replybuf[0] = '\a';
tmp = 1;
rx_out:
ret = write(pty, replybuf, tmp);
if (ret < 0) {
perror("write pty replybuf");
return 1;
}
/* check if there is another command in this buffer */
if (nbytes > msglen + 1) {
for (tmp = 0, ptr = msglen + 1; ptr + tmp < nbytes; tmp++)
buf[tmp] = buf[ptr + tmp];
nbytes = tmp;
goto rx_restart;
}
return 0;
}
/* read CAN frames from CAN interface and write it to the pty */
static int can2pty(int pty, int socket, int *tstamp)
{
int nbytes;
char cmd;
char buf[SLC_MTU];
int ptr;
struct can_frame frame;
int i;
nbytes = read(socket, &frame, sizeof(frame));
if (nbytes != sizeof(frame)) {
perror("read socket");
return 1;
}
/* convert to slcan ASCII frame */
if (frame.can_id & CAN_RTR_FLAG)
cmd = 'R'; /* becomes 'r' in SFF format */
else
cmd = 'T'; /* becomes 't' in SFF format */
if (frame.can_id & CAN_EFF_FLAG)
sprintf(buf, "%c%08X%d", cmd, frame.can_id & CAN_EFF_MASK, frame.can_dlc);
else
sprintf(buf, "%c%03X%d", cmd | 0x20, frame.can_id & CAN_SFF_MASK, frame.can_dlc);
ptr = strlen(buf);
for (i = 0; i < frame.can_dlc; i++)
sprintf(&buf[ptr + 2 * i], "%02X", frame.data[i]);
if (*tstamp) {
struct timeval tv;
if (ioctl(socket, SIOCGSTAMP, &tv) < 0)
perror("SIOCGSTAMP");
sprintf(&buf[ptr + 2*frame.can_dlc], "%04llX",
(unsigned long long)(tv.tv_sec%60)*1000 + tv.tv_usec/1000);
}
strcat(buf, "\r"); /* add terminating character */
nbytes = write(pty, buf, strlen(buf));
if (nbytes < 0) {
perror("write pty");
return 1;
}
fflush(NULL);
return 0;
}
static int check_select_stdin(void)
{
fd_set rdfs;
struct timeval timeout;
int ret;
FD_ZERO(&rdfs);
FD_SET(0, &rdfs);
timeout.tv_sec = 0;
timeout.tv_usec = 0;
ret = select(1, &rdfs, NULL, NULL, &timeout);
if (ret < 0)
return 0; /* not selectable */
if (ret > 0 && getchar() == EOF)
return 0; /* EOF, eg. /dev/null */
return 1;
}
static void print_usage(const char *prg)
{
fprintf(stderr,
"%s: adapter for applications using the slcan ASCII protocol.\n"
"\n"
"%s creates a pty for applications using the slcan ASCII protocol and\n"
"converts the ASCII data to a CAN network interface (and vice versa)\n"
"\n"
"Usage: %s <pty> <can interface>\n"
"\n"
"Examples:\n"
"%s /dev/ptyc0 can0 - creates /dev/ttyc0 for the slcan application\n"
"\n"
"e.g. for pseudo-terminal '%s %s can0' creates /dev/pts/N\n"
"\n",
prg, prg, prg, prg, prg, DEVICE_NAME_PTMX);
}
int main(int argc, char **argv)
{
fd_set rdfs;
int p; /* pty master file */
int s; /* can raw socket */
struct sockaddr_can addr;
struct termios topts;
int select_stdin = 0;
int running = 1;
int tstamp = 0;
int is_open = 0;
struct can_filter fi;
/* check command line options */
if (argc != 3) {
print_usage(basename(argv[0]));
return 1;
}
select_stdin = check_select_stdin();
/* open pty */
p = open(argv[1], O_RDWR);
if (p < 0) {
perror("open pty");
return 1;
}
if (tcgetattr(p, &topts)) {
perror("tcgetattr");
return 1;
}
/* disable local echo which would cause double frames */
topts.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL | ECHOPRT | ECHOKE);
topts.c_iflag &= ~(ICRNL);
topts.c_iflag |= INLCR;
tcsetattr(p, TCSANOW, &topts);
/* Support for the Unix 98 pseudo-terminal interface /dev/ptmx /dev/pts/N */
if (strcmp(argv[1], DEVICE_NAME_PTMX) == 0) {
char *name_pts = NULL; /* slave pseudo-terminal device name */
if (grantpt(p) < 0) {
perror("grantpt");
return 1;
}
if (unlockpt(p) < 0) {
perror("unlockpt");
return 1;
}
name_pts = ptsname(p);
if (name_pts == NULL) {
perror("ptsname");
return 1;
}
printf("open: %s: slave pseudo-terminal is %s\n", argv[1], name_pts);
}
/* open socket */
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (s < 0) {
perror("socket");
return 1;
}
addr.can_family = AF_CAN;
addr.can_ifindex = if_nametoindex(argv[2]);
if (!addr.can_ifindex) {
perror("if_nametoindex");
return 1;
}
/* disable reception of CAN frames until we are opened by 'O' */
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
return 1;
}
/* open filter by default */
fi.can_id = 0;
fi.can_mask = 0;
while (running) {
FD_ZERO(&rdfs);
if (select_stdin)
FD_SET(0, &rdfs);
FD_SET(p, &rdfs);
FD_SET(s, &rdfs);
if (select(s + 1, &rdfs, NULL, NULL, NULL) < 0) {
perror("select");
return 1;
}
if (FD_ISSET(0, &rdfs)) {
running = 0;
continue;
}
if (FD_ISSET(p, &rdfs))
if (pty2can(p, s, &fi, &is_open, &tstamp)) {
running = 0;
continue;
}
if (FD_ISSET(s, &rdfs))
if (can2pty(p, s, &tstamp)) {
running = 0;
continue;
}
}
close(p);
close(s);
return 0;
}