// SPDX-License-Identifier: LGPL-2.0-only // SPDX-FileCopyrightText: 2023 Oleksij Rempel #include #include #include #include #include #include #include #include #include #include #include #include "isobusfs_cli.h" /* Maximal number of events that can be registered. The number is * based on feeling not on any real data. */ #define ISOBUSFS_CLI_MAX_EVENTS 10 int isobusfs_cli_register_event(struct isobusfs_priv *priv, const struct isobusfs_event *new_event) { if (!priv->events) { priv->events = malloc(sizeof(*new_event) * ISOBUSFS_CLI_MAX_EVENTS); if (!priv->events) return -ENOMEM; priv->max_events = ISOBUSFS_CLI_MAX_EVENTS; } else if (priv->num_events >= priv->max_events) { return -1; } memcpy(&priv->events[priv->num_events], new_event, sizeof(*new_event)); priv->num_events++; return 0; } int isobusfs_cli_remove_event(struct isobusfs_priv *priv, struct isobusfs_event *event_to_remove) { size_t i; for (i = 0; i < priv->num_events; ++i) { if (&priv->events[i] == event_to_remove) { memmove(&priv->events[i], &priv->events[i + 1], (priv->num_events - i - 1) * sizeof(*priv->events)); priv->num_events--; return 0; } } return -ENOENT; } struct timespec ms_to_timespec(unsigned int timeout_ms) { struct timespec ts; ts.tv_sec = timeout_ms / 1000; ts.tv_nsec = (timeout_ms % 1000) * 1000000; return ts; } void isobusfs_cli_prepare_response_event(struct isobusfs_event *event, int sock, uint8_t fs_function) { struct timespec current_time, timeout_timespec; event->fd = sock; event->fs_function = fs_function; /* Calculate the timeout */ clock_gettime(CLOCK_REALTIME, ¤t_time); timeout_timespec = ms_to_timespec(ISOBUSFS_CLI_DEFAULT_WAIT_TIMEOUT_MS); event->timeout.tv_sec = current_time.tv_sec + timeout_timespec.tv_sec; event->timeout.tv_nsec = current_time.tv_nsec + timeout_timespec.tv_nsec; /* Adjust for nanosecond overflow */ if (event->timeout.tv_nsec >= 1000000000) { event->timeout.tv_nsec -= 1000000000; event->timeout.tv_sec++; } event->one_shot = true; } static bool isobusfs_cli_has_event_expired(const struct timespec *timeout) { struct timespec now; clock_gettime(CLOCK_REALTIME, &now); return now.tv_sec > timeout->tv_sec || (now.tv_sec == timeout->tv_sec && now.tv_nsec > timeout->tv_nsec); } static void isobusfs_cli_process_expired_events(struct isobusfs_priv *priv) { for (size_t i = 0; i < priv->num_events; ++i) { struct isobusfs_event *event = &priv->events[i]; if (isobusfs_cli_has_event_expired(&event->timeout)) { event->cb(priv, NULL, event->ctx, -ETIME); isobusfs_cli_remove_event(priv, event); i--; } } } static int isobusfs_cli_rx_event(struct isobusfs_priv *priv, int sock, struct isobusfs_msg *msg, bool *found) { struct isobusfs_event *event = NULL; int ret = 0; *found = false; for (size_t i = 0; i < priv->num_events; ++i) { event = &priv->events[i]; if (event->fd == sock && event->fs_function == msg->buf[0]) { if (event->cb) ret = event->cb(priv, msg, event->ctx, 0); *found = true; break; } } if (*found && event->one_shot) isobusfs_cli_remove_event(priv, event); return ret; } static int isobusfs_cli_rx(struct isobusfs_priv *priv, struct isobusfs_msg *msg) { int cmd = isobusfs_buf_to_cmd(msg->buf); int ret = 0; switch (cmd) { case ISOBUSFS_CG_CONNECTION_MANAGMENT: ret = isobusfs_cli_rx_cg_cm(priv, msg); break; case ISOBUSFS_CG_DIRECTORY_HANDLING: ret = isobusfs_cli_rx_cg_dh(priv, msg); break; case ISOBUSFS_CG_FILE_ACCESS: /* fall through */ ret = isobusfs_cli_rx_cg_fa(priv, msg); break; case ISOBUSFS_CG_FILE_HANDLING: /* fall through */ case ISOBUSFS_CG_VOLUME_HANDLING: /* fall through */ default: isobusfs_send_nack(priv->sock_nack, msg); pr_warn("unsupported command group: %i", cmd); /* Not a critical error */ return 0; } return ret; } static int isobusfs_cli_rx_ack(struct isobusfs_priv *priv, struct isobusfs_msg *msg) { enum isobusfs_ack_ctrl ctrl = msg->buf[0]; switch (ctrl) { case ISOBUS_ACK_CTRL_ACK: /* received ACK unexpectedly an ACK, no idea what to do */ pr_debug("< rx: ACK?????"); break; case ISOBUS_ACK_CTRL_NACK: /* we did something wrong */ pr_debug("< rx: NACK!!!!!!"); /* try to provide some usable information with a trace of * the TX history */ isobusfs_dump_tx_data(&priv->tx_buf_log); priv->state = ISOBUSFS_CLI_STATE_IDLE; break; default: pr_warn("%s: unsupported ACK control: %i", __func__, ctrl); } /* Not a critical error */ return 0; } static int isobusfs_cli_rx_buf(struct isobusfs_priv *priv, struct isobusfs_msg *msg) { pgn_t pgn = msg->peername.can_addr.j1939.pgn; int ret; switch (pgn) { case ISOBUSFS_PGN_FS_TO_CL: ret = isobusfs_cli_rx(priv, msg); break; case ISOBUS_PGN_ACK: ret = isobusfs_cli_rx_ack(priv, msg); break; default: pr_warn("%s: unsupported PGN: %x", __func__, pgn); /* Not a critical error */ ret = 0; break; } return ret; } static int isobusfs_cli_rx_one(struct isobusfs_priv *priv, int sock) { struct isobusfs_msg *msg; int flags = 0; bool found; int ret; msg = malloc(sizeof(*msg)); if (!msg) { pr_err("can't allocate rx msg struct\n"); return -ENOMEM; } msg->buf_size = ISOBUSFS_MAX_TRANSFER_LENGH; msg->peer_addr_len = sizeof(msg->peername); msg->sock = sock; ret = recvfrom(sock, &msg->buf[0], msg->buf_size, flags, (struct sockaddr *)&msg->peername, &msg->peer_addr_len); if (ret < 0) { ret = -errno; pr_warn("recvfrom() failed: %i %s", ret, strerror(-ret)); return ret; } if (ret < ISOBUSFS_MIN_TRANSFER_LENGH) { pr_warn("buf is less then min transfer: %i\n", ret); isobusfs_send_nack(priv->sock_nack, msg); return -EINVAL; } msg->len = ret; ret = isobusfs_cli_rx_event(priv, sock, msg, &found); if (ret < 0) { pr_warn("failed to process rx event: %i (%s)\n", ret, strerror(ret)); return ret; } else if (found) return 0; ret = isobusfs_cli_rx_buf(priv, msg); if (ret < 0) { pr_warn("failed to process rx buf: %i (%s)\n", ret, strerror(ret)); return ret; } return 0; } static int isobusfs_cli_handle_events(struct isobusfs_priv *priv, unsigned int nfds) { int ret; unsigned int n; for (n = 0; n < nfds && n < priv->cmn.epoll_events_size; ++n) { struct epoll_event *ev = &priv->cmn.epoll_events[n]; if (!ev->events) { warn("no events"); continue; } if (ev->data.fd == priv->sock_ccm) { if (ev->events & POLLERR) { struct isobusfs_err_msg emsg = { .stats = &priv->stats, }; ret = isobusfs_recv_err(priv->sock_ccm, &emsg); if (ret) pr_warn("error queue reported error: %i", ret); } } else if (ev->data.fd == STDIN_FILENO) { if (!priv->interactive) { warn("got POLLIN on stdin, but interactive mode is disabled"); continue; } if (ev->events & POLLIN) { ret = isobusfs_cli_interactive(priv); if (ret) return ret; } else warn("got not POLLIN on stdin"); } else if (ev->events & POLLIN) { ret = isobusfs_cli_rx_one(priv, ev->data.fd); if (ret) { warn("recv one"); return ret; } } } return 0; } static int isobusfs_cli_handle_periodic_tasks(struct isobusfs_priv *priv) { /* detect FS timeout */ isobusfs_cli_fs_detect_timeout(priv); isobusfs_cli_run_self_tests(priv); isobusfs_cli_process_expired_events(priv); /* this function will send status only if it is proper time to do so */ return isobusfs_cli_ccm_send(priv); } int isobusfs_cli_process_events_and_tasks(struct isobusfs_priv *priv) { bool dont_wait = false; int nfds = 0; int ret; if (priv->state == ISOBUSFS_CLI_STATE_SELFTEST) dont_wait = true; ret = libj1939_prepare_for_events(&priv->cmn, &nfds, dont_wait); if (ret) return ret; if (nfds > 0) { ret = isobusfs_cli_handle_events(priv, (unsigned int)nfds); if (ret) return ret; } return isobusfs_cli_handle_periodic_tasks(priv); } static int isobusfs_cli_sock_main_prepare(struct isobusfs_priv *priv) { struct sockaddr_can addr = priv->sockname; int ret; ret = libj1939_open_socket(); if (ret < 0) return ret; priv->sock_main = ret; /* TODO: this is TX only socket */ addr.can_addr.j1939.pgn = ISOBUSFS_PGN_FS_TO_CL; ret = libj1939_bind_socket(priv->sock_main, &addr); if (ret < 0) return ret; ret = isobusfs_cmn_set_linger(priv->sock_main); if (ret < 0) return ret; ret = libj1939_socket_prio(priv->sock_main, ISOBUSFS_PRIO_DEFAULT); if (ret < 0) return ret; ret = isobusfs_cmn_connect_socket(priv->sock_main, &priv->peername); if (ret < 0) return ret; return libj1939_add_socket_to_epoll(priv->cmn.epoll_fd, priv->sock_main, EPOLLIN); } /* isobusfs_cli_sock_int_prepare() is used to prepare stdin for interactive * mode. */ static int isobusfs_cli_sock_int_prepare(struct isobusfs_priv *priv) { int ret; if (!priv->interactive) return 0; isobusfs_set_interactive(true); ret = fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); if (ret < 0) return ret; return libj1939_add_socket_to_epoll(priv->cmn.epoll_fd, STDIN_FILENO, EPOLLIN); } static int isobusfs_cli_sock_ccm_prepare(struct isobusfs_priv *priv) { struct sockaddr_can addr = priv->sockname; int ret; ret = libj1939_open_socket(); if (ret < 0) return ret; priv->sock_ccm = ret; ret = isobusfs_cmn_configure_error_queue(priv->sock_ccm); if (ret < 0) return ret; /* TODO: this is TX only socket */ addr.can_addr.j1939.pgn = J1939_NO_PGN; ret = libj1939_bind_socket(priv->sock_ccm, &addr); if (ret < 0) return ret; ret = isobusfs_cmn_set_linger(priv->sock_ccm); if (ret < 0) return ret; ret = libj1939_socket_prio(priv->sock_ccm, ISOBUSFS_PRIO_DEFAULT); if (ret < 0) return ret; ret = isobusfs_cmn_connect_socket(priv->sock_ccm, &priv->peername); if (ret < 0) return ret; /* poll for errors to get confirmation if our packets are send */ return libj1939_add_socket_to_epoll(priv->cmn.epoll_fd, priv->sock_ccm, EPOLLERR); } static int isobusfs_cli_sock_nack_prepare(struct isobusfs_priv *priv) { struct sockaddr_can addr = priv->sockname; int ret; ret = libj1939_open_socket(); if (ret < 0) return ret; priv->sock_nack = ret; addr.can_addr.j1939.pgn = ISOBUS_PGN_ACK; ret = libj1939_bind_socket(priv->sock_nack, &addr); if (ret < 0) return ret; ret = libj1939_socket_prio(priv->sock_nack, ISOBUSFS_PRIO_ACK); if (ret < 0) return ret; /* poll for errors to get confirmation if our packets are send */ return libj1939_add_socket_to_epoll(priv->cmn.epoll_fd, priv->sock_nack, EPOLLIN); } /* rx socket for fss and volume status announcements */ static int isobusfs_cli_sock_bcast_prepare(struct isobusfs_priv *priv) { struct sockaddr_can addr = priv->sockname; int ret; ret = libj1939_open_socket(); if (ret < 0) return ret; priv->sock_bcast_rx = ret; /* keep address and name and overwrite PGN */ addr.can_addr.j1939.name = J1939_NO_NAME; addr.can_addr.j1939.addr = J1939_NO_ADDR; addr.can_addr.j1939.pgn = ISOBUSFS_PGN_FS_TO_CL; ret = libj1939_bind_socket(priv->sock_bcast_rx, &addr); if (ret < 0) return ret; ret = libj1939_set_broadcast(priv->sock_bcast_rx); if (ret < 0) return ret; ret = isobusfs_cmn_connect_socket(priv->sock_bcast_rx, &priv->peername); if (ret < 0) return ret; return libj1939_add_socket_to_epoll(priv->cmn.epoll_fd, priv->sock_bcast_rx, EPOLLIN); } static int isobusfs_cli_sock_prepare(struct isobusfs_priv *priv) { int ret; ret = libj1939_create_epoll(); if (ret < 0) return ret; priv->cmn.epoll_fd = ret; priv->cmn.epoll_events = calloc(ISOBUSFS_CLI_MAX_EPOLL_EVENTS, sizeof(struct epoll_event)); if (!priv->cmn.epoll_events) return -ENOMEM; priv->cmn.epoll_events_size = ISOBUSFS_CLI_MAX_EPOLL_EVENTS; ret = isobusfs_cli_sock_int_prepare(priv); if (ret < 0) return ret; ret = isobusfs_cli_sock_ccm_prepare(priv); if (ret < 0) return ret; ret = isobusfs_cli_sock_bcast_prepare(priv); if (ret < 0) return ret; ret = isobusfs_cli_sock_main_prepare(priv); if (ret < 0) return ret; return isobusfs_cli_sock_nack_prepare(priv); } static void isobusfs_cli_print_help(void) { printf("Usage: isobusfs-cli [options]\n"); printf("Options:\n"); printf(" --interactive or -I (Default)\n"); printf(" --interface or -i \n"); printf(" --local-address or -a \n"); printf(" --local-name or -n \n"); printf(" --log-level or -l (Default %d)\n", LOG_LEVEL_INFO); printf(" --remote-address or -r \n"); printf(" --remote-name or -m \n"); printf("Note: Local address and local name are mutually exclusive\n"); printf("Note: Remote address and remote name are mutually exclusive\n"); } static int isobusfs_cli_parse_args(struct isobusfs_priv *priv, int argc, char *argv[]) { struct sockaddr_can *remote = &priv->peername; struct sockaddr_can *local = &priv->sockname; bool local_address_set = false; bool local_name_set = false; bool remote_address_set = false; bool remote_name_set = false; bool interface_set = false; int long_index = 0; int level; int opt; static struct option long_options[] = { {"interface", required_argument, 0, 'i'}, {"interactive", no_argument, 0, 'I'}, {"local-address", required_argument, 0, 'a'}, {"local-name", required_argument, 0, 'n'}, {"log-level", required_argument, 0, 'l'}, {"remote-address", required_argument, 0, 'r'}, {"remote-name", required_argument, 0, 'm'}, {0, 0, 0, 0} }; /* active by default */ priv->interactive = true; while ((opt = getopt_long(argc, argv, "a:n:r:m:Ii:l:", long_options, &long_index)) != -1) { switch (opt) { case 'a': local->can_addr.j1939.addr = strtoul(optarg, NULL, 16); local_address_set = true; break; case 'n': local->can_addr.j1939.name = strtoull(optarg, NULL, 16); local_name_set = true; break; case 'r': remote->can_addr.j1939.addr = strtoul(optarg, NULL, 16); remote_address_set = true; break; case 'm': remote->can_addr.j1939.name = strtoull(optarg, NULL, 16); remote_name_set = true; break; case 'i': local->can_ifindex = if_nametoindex(optarg); if (!local->can_ifindex) { pr_err("Interface %s not found. Error: %d (%s)\n", optarg, errno, strerror(errno)); return -EINVAL; } remote->can_ifindex = local->can_ifindex; interface_set = true; break; case 'I': priv->interactive = true; break; case 'l': level = strtoul(optarg, NULL, 0); if (level < LOG_LEVEL_ERROR || level > LOG_LEVEL_DEBUG) pr_err("invalid debug level %d", level); isobusfs_log_level_set(level); break; default: isobusfs_cli_print_help(); return -EINVAL; } } if (!interface_set) { pr_err("interface not specified"); isobusfs_cli_print_help(); return -EINVAL; } if ((local_address_set && local_name_set) || (remote_address_set && remote_name_set)) { pr_err("local address and local name or remote address and remote name are mutually exclusive"); isobusfs_cli_print_help(); return -EINVAL; } return 0; } int main(int argc, char *argv[]) { struct isobusfs_priv *priv; struct timespec ts; int ret; priv = malloc(sizeof(*priv)); if (!priv) err(EXIT_FAILURE, "can't allocate priv"); bzero(priv, sizeof(*priv)); libj1939_init_sockaddr_can(&priv->sockname, J1939_NO_PGN); libj1939_init_sockaddr_can(&priv->peername, ISOBUSFS_PGN_CL_TO_FS); ret = isobusfs_cli_parse_args(priv, argc, argv); if (ret) return ret; ret = isobusfs_cli_sock_prepare(priv); if (ret) return ret; isobusfs_cli_ccm_init(priv); /* Init next st_next_send_time value to avoid warnings */ clock_gettime(CLOCK_MONOTONIC, &ts); priv->cmn.next_send_time = ts; if (priv->interactive) isobusfs_cli_int_start(priv); else pr_debug("starting client\n"); while (1) { ret = isobusfs_cli_process_events_and_tasks(priv); if (ret) break; } close(priv->cmn.epoll_fd); free(priv->cmn.epoll_events); close(priv->sock_main); close(priv->sock_nack); close(priv->sock_ccm); close(priv->sock_bcast_rx); return ret; }