| /* |
| * Copyright (c) 2026 Philipp Steiner <philipp.steiner1987@gmail.com> |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #undef _POSIX_C_SOURCE |
| #define _POSIX_C_SOURCE 200809L |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_DECLARE(net_shell); |
| |
| #include <inttypes.h> |
| #include <stdlib.h> |
| |
| #if defined(CONFIG_PTP) |
| #include <zephyr/net/net_if.h> |
| #include <zephyr/net/ethernet.h> |
| #include <zephyr/drivers/ptp_clock.h> |
| #include "ptp/clock.h" |
| #include "ptp/port.h" |
| |
| #include <time.h> |
| #endif |
| |
| #include "net_shell_private.h" |
| |
| #if defined(CONFIG_PTP) |
| static int64_t ptp_timeinterval_to_ns(ptp_timeinterval value) |
| { |
| return value >> 16; |
| } |
| |
| static const char *ptp_port_state2str(enum ptp_port_state state) |
| { |
| switch (state) { |
| case PTP_PS_INITIALIZING: |
| return "INITIALIZING"; |
| case PTP_PS_FAULTY: |
| return "FAULTY"; |
| case PTP_PS_DISABLED: |
| return "DISABLED"; |
| case PTP_PS_LISTENING: |
| return "LISTENING"; |
| case PTP_PS_PRE_TIME_TRANSMITTER: |
| return "PRE_TIME_TRANSMITTER"; |
| case PTP_PS_TIME_TRANSMITTER: |
| return "TIME_TRANSMITTER"; |
| case PTP_PS_GRAND_MASTER: |
| return "GRAND_MASTER"; |
| case PTP_PS_PASSIVE: |
| return "PASSIVE"; |
| case PTP_PS_UNCALIBRATED: |
| return "UNCALIBRATED"; |
| case PTP_PS_TIME_RECEIVER: |
| return "TIME_RECEIVER"; |
| default: |
| return "<unknown>"; |
| } |
| } |
| |
| static const char *ptp_clock_type2str(enum ptp_clock_type type) |
| { |
| switch (type) { |
| case PTP_CLOCK_TYPE_ORDINARY: |
| return "ORDINARY"; |
| case PTP_CLOCK_TYPE_BOUNDARY: |
| return "BOUNDARY"; |
| case PTP_CLOCK_TYPE_P2P: |
| return "TRANSPARENT_P2P"; |
| case PTP_CLOCK_TYPE_E2E: |
| return "TRANSPARENT_E2E"; |
| case PTP_CLOCK_TYPE_MANAGEMENT: |
| return "MANAGEMENT"; |
| default: |
| return "<unknown>"; |
| } |
| } |
| |
| static const char *ptp_delay_mechanism2str(enum ptp_delay_mechanism mechanism) |
| { |
| switch (mechanism) { |
| case PTP_DM_E2E: |
| return "E2E"; |
| case PTP_DM_P2P: |
| return "P2P"; |
| case PTP_DM_COMMON_P2P: |
| return "COMMON_P2P"; |
| case PTP_DM_SPECIAL: |
| return "SPECIAL"; |
| case PTP_DM_NO_MECHANISM: |
| return "NO_MECHANISM"; |
| default: |
| return "<unknown>"; |
| } |
| } |
| |
| static const char *ptp_net_protocol2str(void) |
| { |
| if (IS_ENABLED(CONFIG_PTP_UDP_IPV4_PROTOCOL)) { |
| return "UDP/IPv4"; |
| } else if (IS_ENABLED(CONFIG_PTP_UDP_IPV6_PROTOCOL)) { |
| return "UDP/IPv6"; |
| } else if (IS_ENABLED(CONFIG_PTP_IEEE_802_3_PROTOCOL)) { |
| return "IEEE 802.3"; |
| } |
| |
| return "<unknown>"; |
| } |
| |
| static const char *ptp_time_src2str(uint8_t time_src) |
| { |
| switch (time_src) { |
| case PTP_TIME_SRC_ATOMIC_CLK: |
| return "ATOMIC_CLOCK"; |
| case PTP_TIME_SRC_GNSS: |
| return "GNSS"; |
| case PTP_TIME_SRC_TERRESTRIAL_RADIO: |
| return "TERRESTRIAL_RADIO"; |
| case PTP_TIME_SRC_SERIAL_TIME_CODE: |
| return "SERIAL_TIME_CODE"; |
| case PTP_TIME_SRC_PTP: |
| return "PTP"; |
| case PTP_TIME_SRC_NTP: |
| return "NTP"; |
| case PTP_TIME_SRC_HAND_SET: |
| return "HAND_SET"; |
| case PTP_TIME_SRC_OTHER: |
| return "OTHER"; |
| case PTP_TIME_SRC_INTERNAL_OSC: |
| return "INTERNAL_OSCILLATOR"; |
| default: |
| return "<unknown>"; |
| } |
| } |
| |
| static const char *ptp_sync_status2str(const struct ptp_current_ds *cds) |
| { |
| return cds->sync_uncertain ? "uncertain" : "stable"; |
| } |
| |
| static void ptp_sprint_clock_id(const ptp_clk_id *clock_id, char *buf, size_t len) |
| { |
| snprintk(buf, len, "%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X", clock_id->id[0], |
| clock_id->id[1], clock_id->id[2], clock_id->id[3], clock_id->id[4], |
| clock_id->id[5], clock_id->id[6], clock_id->id[7]); |
| } |
| |
| static struct ptp_port *ptp_find_port(uint16_t port_id) |
| { |
| struct ptp_port *port; |
| |
| SYS_SLIST_FOR_EACH_CONTAINER(ptp_clock_ports_list(), port, node) { |
| if (port->port_ds.id.port_number == port_id) { |
| return port; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static bool ptp_read_hw_timestamp(struct net_if *iface, struct net_ptp_time *ts) |
| { |
| const struct device *phc = net_eth_get_ptp_clock(iface); |
| |
| if (!phc) { |
| return false; |
| } |
| |
| return ptp_clock_get(phc, ts) == 0; |
| } |
| |
| static void ptp_print_hw_timestamp_formated(const struct shell *sh, struct net_ptp_time ts, |
| const char *name, const char *suffix) |
| { |
| uint32_t ms = ts.nanosecond / NSEC_PER_MSEC; |
| uint32_t us = (ts.nanosecond / NSEC_PER_USEC) % USEC_PER_MSEC; |
| uint32_t ns = ts.nanosecond % NSEC_PER_USEC; |
| struct tm tm_timestamp = {0}; |
| time_t time_seconds = ts.second; |
| |
| gmtime_r(&time_seconds, &tm_timestamp); |
| |
| #if defined(CONFIG_REQUIRES_FULL_LIBC) |
| char time_str[sizeof("1970-01-01 00:00:00")]; |
| |
| strftime(time_str, sizeof(time_str), "%F %T", &tm_timestamp); |
| |
| PR("%s: %s.%03u,%03u,%03u %s\n", name, time_str, ms, us, ns, suffix); |
| #else /* CONFIG_REQUIRES_FULL_LIBC */ |
| PR("%s: %04u-%02u-%02u %02u:%02u:%02u.%03u,%03u,%03u %s\n", name, |
| tm_timestamp.tm_year + 1900, tm_timestamp.tm_mon + 1, tm_timestamp.tm_mday, |
| tm_timestamp.tm_hour, tm_timestamp.tm_min, tm_timestamp.tm_sec, ms, us, ns, suffix); |
| #endif /* CONFIG_REQUIRES_FULL_LIBC */ |
| } |
| |
| static void ptp_print_hw_timestamp(const struct shell *sh, struct net_if *iface, const char *name) |
| { |
| struct net_ptp_time ts; |
| |
| if (ptp_read_hw_timestamp(iface, &ts)) { |
| PR("%s: %" PRIu64 ".%09u\n", name, ts.second, ts.nanosecond); |
| |
| ptp_print_hw_timestamp_formated(sh, ts, name, "(TAI)"); |
| |
| ts.second -= ptp_clock_time_prop_ds()->current_utc_offset; /* TAI-UTC offset */ |
| |
| ptp_print_hw_timestamp_formated(sh, ts, name, "(UTC)"); |
| } else { |
| PR("%s: unavailable\n", name); |
| } |
| } |
| |
| static void ptp_print_best_foreign(const struct shell *sh) |
| { |
| const struct ptp_foreign_tt_clock *best = ptp_clock_best_time_transmitter(); |
| char sender_clock_id[sizeof("FF:FF:FF:FF:FF:FF:FF:FF")]; |
| char receiver_clock_id[sizeof("FF:FF:FF:FF:FF:FF:FF:FF")]; |
| |
| if (!best) { |
| PR("Best foreign transmitter : none\n"); |
| return; |
| } |
| |
| ptp_sprint_clock_id(&best->dataset.sender.clk_id, sender_clock_id, sizeof(sender_clock_id)); |
| ptp_sprint_clock_id(&best->dataset.receiver.clk_id, receiver_clock_id, |
| sizeof(receiver_clock_id)); |
| |
| PR("Best foreign transmitter : %s-%u (records=%u)\n", sender_clock_id, |
| best->dataset.sender.port_number, best->messages_count); |
| PR(" Dataset : p1=%u p2=%u steps_rm=%u receiver=%s-%u\n", |
| best->dataset.priority1, best->dataset.priority2, best->dataset.steps_rm, |
| receiver_clock_id, best->dataset.receiver.port_number); |
| } |
| |
| static void ptp_print_port_overview(const struct shell *sh) |
| { |
| struct ptp_port *port; |
| |
| PR("Port Interface State\n"); |
| SYS_SLIST_FOR_EACH_CONTAINER(ptp_clock_ports_list(), port, node) { |
| PR("%4u %9d %s\n", port->port_ds.id.port_number, net_if_get_by_iface(port->iface), |
| ptp_port_state2str(ptp_port_state(port))); |
| } |
| } |
| |
| static void ptp_print_instance_info(const struct shell *sh) |
| { |
| const struct ptp_default_ds *dds = ptp_clock_default_ds(); |
| const struct ptp_current_ds *cds = ptp_clock_current_ds(); |
| const struct ptp_parent_ds *pds = ptp_clock_parent_ds(); |
| const struct ptp_time_prop_ds *tpds = ptp_clock_time_prop_ds(); |
| char clock_id[sizeof("FF:FF:FF:FF:FF:FF:FF:FF")]; |
| char gm_id[sizeof("FF:FF:FF:FF:FF:FF:FF:FF")]; |
| char parent_port_clock_id[sizeof("FF:FF:FF:FF:FF:FF:FF:FF")]; |
| struct ptp_port *first_port = NULL; |
| |
| ptp_sprint_clock_id(&dds->clk_id, clock_id, sizeof(clock_id)); |
| ptp_sprint_clock_id(&pds->gm_id, gm_id, sizeof(gm_id)); |
| ptp_sprint_clock_id(&pds->port_id.clk_id, parent_port_clock_id, |
| sizeof(parent_port_clock_id)); |
| |
| PR("PTP Instance:\n"); |
| PR("Clock ID : %s\n", clock_id); |
| PR("Clock Type : %s\n", ptp_clock_type2str(ptp_clock_type())); |
| PR("Protocol : %s\n", ptp_net_protocol2str()); |
| PR("Domain : %u\n", dds->domain); |
| PR("Priorities : %u / %u\n", dds->priority1, dds->priority2); |
| PR("Time source : %s (0x%02x)\n", ptp_time_src2str(tpds->time_src), tpds->time_src); |
| PR("Ports : %u\n", dds->n_ports); |
| PR("TR only : %s\n", dds->time_receiver_only ? "yes" : "no"); |
| |
| SYS_SLIST_FOR_EACH_CONTAINER(ptp_clock_ports_list(), first_port, node) { |
| break; |
| } |
| |
| if (first_port) { |
| ptp_print_hw_timestamp(sh, first_port->iface, "PHC now "); |
| } else { |
| PR("PHC now : unavailable\n"); |
| } |
| |
| PR("\nCurrent dataset:\n"); |
| PR("steps_rm : %u\n", cds->steps_rm); |
| PR("offset_from_tt: %" PRId64 " ns\n", ptp_timeinterval_to_ns(cds->offset_from_tt)); |
| PR("mean_delay : %" PRId64 " ns\n", ptp_timeinterval_to_ns(cds->mean_delay)); |
| PR("sync status : %s\n", ptp_sync_status2str(cds)); |
| |
| PR("\nParent/GM dataset:\n"); |
| PR("grandmaster : %s (p1=%u p2=%u class=%u acc=0x%02x)\n", gm_id, pds->gm_priority1, |
| pds->gm_priority2, pds->gm_clk_quality.cls, pds->gm_clk_quality.accuracy); |
| PR("parent port : %s-%u\n", parent_port_clock_id, pds->port_id.port_number); |
| PR("UTC offset : %d\n", tpds->current_utc_offset); |
| |
| PR("\n"); |
| ptp_print_best_foreign(sh); |
| PR("\n"); |
| ptp_print_port_overview(sh); |
| } |
| |
| static void ptp_print_port_info(const struct shell *sh, uint16_t port_id) |
| { |
| struct ptp_port *port = ptp_find_port(port_id); |
| struct ptp_dataset *best = NULL; |
| char port_clock_id[sizeof("FF:FF:FF:FF:FF:FF:FF:FF")]; |
| char best_sender_id[sizeof("FF:FF:FF:FF:FF:FF:FF:FF")]; |
| |
| if (!port) { |
| PR_WARNING("PTP port %u was not found.\n", port_id); |
| return; |
| } |
| |
| ptp_sprint_clock_id(&port->port_ds.id.clk_id, port_clock_id, sizeof(port_clock_id)); |
| |
| PR("PTP Port %u:\n", port->port_ds.id.port_number); |
| PR("interface : %d\n", net_if_get_by_iface(port->iface)); |
| PR("port identity : %s-%u\n", port_clock_id, port->port_ds.id.port_number); |
| |
| PR("\nConfiguration:\n"); |
| PR("state : %s\n", ptp_port_state2str(ptp_port_state(port))); |
| PR("enabled : %s\n", port->port_ds.enable ? "yes" : "no"); |
| PR("protocol : %s\n", ptp_net_protocol2str()); |
| PR("time transmitter only : %s\n", port->port_ds.time_transmitter_only ? "yes" : "no"); |
| PR("announce log itv : %d\n", port->port_ds.log_announce_interval); |
| PR("announce timeout : %u\n", port->port_ds.announce_receipt_timeout); |
| PR("sync log itv : %d\n", port->port_ds.log_sync_interval); |
| PR("min delay_req log itv : %d\n", port->port_ds.log_min_delay_req_interval); |
| PR("min pdelay_req log itv: %d\n", port->port_ds.log_min_pdelay_req_interval); |
| PR("delay mechanism : %s\n", ptp_delay_mechanism2str(port->port_ds.delay_mechanism)); |
| PR("delay asymmetry : %" PRId64 " ns\n", |
| ptp_timeinterval_to_ns(port->port_ds.delay_asymmetry)); |
| PR("mean link delay : %" PRId64 " ns\n", |
| ptp_timeinterval_to_ns(port->port_ds.mean_link_delay)); |
| PR("neighbor rate ratio : %s (%" PRId64 " ppb)\n", |
| port->neighbor_rate_ratio_valid ? "valid" : "nominal", |
| (int64_t)((port->neighbor_rate_ratio - 1.0) * 1000000000.0)); |
| |
| PR("\nRuntime:\n"); |
| PR("seq announce/delay/signaling/sync/pdelay: %u / %u / %u / %u / %u\n", |
| port->seq_id.announce, port->seq_id.delay, port->seq_id.signaling, port->seq_id.sync, |
| port->seq_id.pdelay); |
| PR("pdelay pending seq : %u\n", port->pdelay_req_sequence_id); |
| PR("timeouts bits : announce=%u delay=%u sync=%u qualification=%u pdelay=%u\n", |
| atomic_test_bit(&port->timeouts, PTP_PORT_TIMER_ANNOUNCE_TO), |
| atomic_test_bit(&port->timeouts, PTP_PORT_TIMER_DELAY_TO), |
| atomic_test_bit(&port->timeouts, PTP_PORT_TIMER_SYNC_TO), |
| atomic_test_bit(&port->timeouts, PTP_PORT_TIMER_QUALIFICATION_TO), |
| atomic_test_bit(&port->timeouts, PTP_PORT_TIMER_PDELAY_TO)); |
| PR("timer remaining ms : announce=%d delay=%d sync=%d qualification=%d pdelay=%d\n", |
| k_timer_remaining_get(&port->timers.announce), |
| k_timer_remaining_get(&port->timers.delay), k_timer_remaining_get(&port->timers.sync), |
| k_timer_remaining_get(&port->timers.qualification), |
| k_timer_remaining_get(&port->timers.pdelay)); |
| ptp_print_hw_timestamp(sh, port->iface, "PHC now "); |
| |
| best = ptp_port_best_foreign_ds(port); |
| if (!best) { |
| PR("best foreign : none\n"); |
| return; |
| } |
| |
| ptp_sprint_clock_id(&best->sender.clk_id, best_sender_id, sizeof(best_sender_id)); |
| PR("best foreign : %s-%u (p1=%u p2=%u steps_rm=%u)\n", best_sender_id, |
| best->sender.port_number, best->priority1, best->priority2, best->steps_rm); |
| } |
| #endif /* CONFIG_PTP */ |
| |
| static int cmd_net_ptp_port(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| #if defined(CONFIG_PTP) |
| char *endptr = NULL; |
| long port_id; |
| |
| ARG_UNUSED(argc); |
| |
| if (argv[1] == NULL) { |
| PR_WARNING("Port number must be given.\n"); |
| return -ENOEXEC; |
| } |
| |
| port_id = strtol(argv[1], &endptr, 10); |
| if (*endptr != '\0' || port_id <= 0 || port_id > UINT16_MAX) { |
| PR_WARNING("Invalid PTP port number: %s\n", argv[1]); |
| return -EINVAL; |
| } |
| |
| ptp_print_port_info(sh, (uint16_t)port_id); |
| #else |
| ARG_UNUSED(argc); |
| ARG_UNUSED(argv); |
| |
| PR_INFO("Set %s to enable %s support.\n", "CONFIG_PTP", "PTP"); |
| #endif |
| |
| return 0; |
| } |
| |
| static int cmd_net_ptp(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| #if defined(CONFIG_PTP) |
| ARG_UNUSED(argc); |
| |
| if (argv[1] != NULL) { |
| return cmd_net_ptp_port(sh, argc, argv); |
| } |
| |
| ptp_print_instance_info(sh); |
| #else |
| ARG_UNUSED(argc); |
| ARG_UNUSED(argv); |
| |
| PR_INFO("Set %s to enable %s support.\n", "CONFIG_PTP", "PTP"); |
| #endif |
| |
| return 0; |
| } |
| |
| SHELL_STATIC_SUBCMD_SET_CREATE( |
| net_cmd_ptp, |
| SHELL_CMD(port, NULL, SHELL_HELP("Print detailed information about PTP port", "<port>"), |
| cmd_net_ptp_port), |
| SHELL_SUBCMD_SET_END); |
| |
| SHELL_SUBCMD_ADD((net), ptp, &net_cmd_ptp, "Print information about PTP support.", cmd_net_ptp, 1, |
| 1); |