mirror of
https://mirrors.bfsu.edu.cn/git/linux.git
synced 2024-12-26 12:34:41 +08:00
Merge branch 'ethtool-hw-timestamping-statistics'
Rahul Rameshbabu says: ==================== ethtool HW timestamping statistics The goal of this patch series is to introduce a common set of ethtool statistics for hardware timestamping that a driver implementer can hook into. The statistics counters added are based on what I believe are common patterns/behaviors found across various hardware timestamping implementations seen in the kernel tree today. The mlx5 family of devices is used as the PoC for this patch series. Other vendors are more than welcome to chime in on this series. Link: https://lore.kernel.org/netdev/20240402205223.137565-1-rrameshbabu@nvidia.com/ Link: https://lore.kernel.org/netdev/20240309084440.299358-1-rrameshbabu@nvidia.com/ Link: https://lore.kernel.org/netdev/20240223192658.45893-1-rrameshbabu@nvidia.com/ Signed-off-by: Rahul Rameshbabu <rrameshbabu@nvidia.com> ==================== Link: https://lore.kernel.org/r/20240403212931.128541-1-rrameshbabu@nvidia.com Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
commit
571faefe09
@ -18,7 +18,6 @@ definitions:
|
||||
entries: []
|
||||
-
|
||||
name: header-flags
|
||||
enum-name:
|
||||
type: flags
|
||||
entries: [ compact-bitsets, omit-reply, stats ]
|
||||
|
||||
@ -565,6 +564,18 @@ attribute-sets:
|
||||
-
|
||||
name: tx-lpi-timer
|
||||
type: u32
|
||||
-
|
||||
name: ts-stat
|
||||
attributes:
|
||||
-
|
||||
name: tx-pkts
|
||||
type: uint
|
||||
-
|
||||
name: tx-lost
|
||||
type: uint
|
||||
-
|
||||
name: tx-err
|
||||
type: uint
|
||||
-
|
||||
name: tsinfo
|
||||
attributes:
|
||||
@ -587,6 +598,10 @@ attribute-sets:
|
||||
-
|
||||
name: phc-index
|
||||
type: u32
|
||||
-
|
||||
name: stats
|
||||
type: nest
|
||||
nested-attributes: ts-stat
|
||||
-
|
||||
name: cable-result
|
||||
attributes:
|
||||
@ -1394,6 +1409,7 @@ operations:
|
||||
- tx-types
|
||||
- rx-filters
|
||||
- phc-index
|
||||
- stats
|
||||
dump: *tsinfo-get-op
|
||||
-
|
||||
name: cable-test-act
|
||||
|
@ -300,6 +300,11 @@ the software port.
|
||||
in the beginning of the queue. This is a normal condition.
|
||||
- Informative
|
||||
|
||||
* - `tx[i]_timestamps`
|
||||
- Transmitted packets that were hardware timestamped at the device's DMA
|
||||
layer.
|
||||
- Informative
|
||||
|
||||
* - `tx[i]_added_vlan_packets`
|
||||
- The number of packets sent where vlan tag insertion was offloaded to the
|
||||
hardware.
|
||||
@ -702,6 +707,12 @@ the software port.
|
||||
the device typically ensures not posting the CQE.
|
||||
- Error
|
||||
|
||||
* - `ptp_cq[i]_lost_cqe`
|
||||
- Number of times a CQE is expected to not be delivered on the PTP
|
||||
timestamping CQE by the device due to a time delta elapsing. If such a
|
||||
CQE is somehow delivered, `ptp_cq[i]_late_cqe` is incremented.
|
||||
- Error
|
||||
|
||||
.. [#ring_global] The corresponding ring and global counters do not share the
|
||||
same name (i.e. do not follow the common naming scheme).
|
||||
|
||||
|
@ -1237,12 +1237,21 @@ Kernel response contents:
|
||||
``ETHTOOL_A_TSINFO_TX_TYPES`` bitset supported Tx types
|
||||
``ETHTOOL_A_TSINFO_RX_FILTERS`` bitset supported Rx filters
|
||||
``ETHTOOL_A_TSINFO_PHC_INDEX`` u32 PTP hw clock index
|
||||
``ETHTOOL_A_TSINFO_STATS`` nested HW timestamping statistics
|
||||
===================================== ====== ==========================
|
||||
|
||||
``ETHTOOL_A_TSINFO_PHC_INDEX`` is absent if there is no associated PHC (there
|
||||
is no special value for this case). The bitset attributes are omitted if they
|
||||
would be empty (no bit set).
|
||||
|
||||
Additional hardware timestamping statistics response contents:
|
||||
|
||||
===================================== ====== ===================================
|
||||
``ETHTOOL_A_TS_STAT_TX_PKTS`` u64 Packets with Tx HW timestamps
|
||||
``ETHTOOL_A_TS_STAT_TX_LOST`` u64 Tx HW timestamp not arrived count
|
||||
``ETHTOOL_A_TS_STAT_TX_ERR`` u64 HW error request Tx timestamp count
|
||||
===================================== ====== ===================================
|
||||
|
||||
CABLE_TEST
|
||||
==========
|
||||
|
||||
|
@ -169,6 +169,7 @@ static void mlx5e_ptpsq_mark_ts_cqes_undelivered(struct mlx5e_ptpsq *ptpsq,
|
||||
WARN_ON_ONCE(!pos->inuse);
|
||||
pos->inuse = false;
|
||||
list_del(&pos->entry);
|
||||
ptpsq->cq_stats->lost_cqe++;
|
||||
}
|
||||
spin_unlock_bh(&cqe_list->tracker_list_lock);
|
||||
}
|
||||
|
@ -2387,6 +2387,14 @@ static void mlx5e_get_rmon_stats(struct net_device *netdev,
|
||||
mlx5e_stats_rmon_get(priv, rmon_stats, ranges);
|
||||
}
|
||||
|
||||
static void mlx5e_get_ts_stats(struct net_device *netdev,
|
||||
struct ethtool_ts_stats *ts_stats)
|
||||
{
|
||||
struct mlx5e_priv *priv = netdev_priv(netdev);
|
||||
|
||||
mlx5e_stats_ts_get(priv, ts_stats);
|
||||
}
|
||||
|
||||
const struct ethtool_ops mlx5e_ethtool_ops = {
|
||||
.cap_rss_ctx_supported = true,
|
||||
.supported_coalesce_params = ETHTOOL_COALESCE_USECS |
|
||||
@ -2436,5 +2444,6 @@ const struct ethtool_ops mlx5e_ethtool_ops = {
|
||||
.get_eth_mac_stats = mlx5e_get_eth_mac_stats,
|
||||
.get_eth_ctrl_stats = mlx5e_get_eth_ctrl_stats,
|
||||
.get_rmon_stats = mlx5e_get_rmon_stats,
|
||||
.get_ts_stats = mlx5e_get_ts_stats,
|
||||
.get_link_ext_stats = mlx5e_get_link_ext_stats
|
||||
};
|
||||
|
@ -1172,6 +1172,51 @@ void mlx5e_stats_rmon_get(struct mlx5e_priv *priv,
|
||||
*ranges = mlx5e_rmon_ranges;
|
||||
}
|
||||
|
||||
void mlx5e_stats_ts_get(struct mlx5e_priv *priv,
|
||||
struct ethtool_ts_stats *ts_stats)
|
||||
{
|
||||
int i, j;
|
||||
|
||||
mutex_lock(&priv->state_lock);
|
||||
|
||||
if (priv->tx_ptp_opened) {
|
||||
struct mlx5e_ptp *ptp = priv->channels.ptp;
|
||||
|
||||
ts_stats->pkts = 0;
|
||||
ts_stats->err = 0;
|
||||
ts_stats->lost = 0;
|
||||
|
||||
/* Aggregate stats across all TCs */
|
||||
for (i = 0; i < ptp->num_tc; i++) {
|
||||
struct mlx5e_ptp_cq_stats *stats =
|
||||
ptp->ptpsq[i].cq_stats;
|
||||
|
||||
ts_stats->pkts += stats->cqe;
|
||||
ts_stats->err += stats->abort + stats->err_cqe +
|
||||
stats->late_cqe;
|
||||
ts_stats->lost += stats->lost_cqe;
|
||||
}
|
||||
} else {
|
||||
/* DMA layer will always successfully timestamp packets. Other
|
||||
* counters do not make sense for this layer.
|
||||
*/
|
||||
ts_stats->pkts = 0;
|
||||
|
||||
/* Aggregate stats across all SQs */
|
||||
for (j = 0; j < priv->channels.num; j++) {
|
||||
struct mlx5e_channel *c = priv->channels.c[j];
|
||||
|
||||
for (i = 0; i < c->num_tc; i++) {
|
||||
struct mlx5e_sq_stats *stats = c->sq[i].stats;
|
||||
|
||||
ts_stats->pkts += stats->timestamps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutex_unlock(&priv->state_lock);
|
||||
}
|
||||
|
||||
#define PPORT_PHY_STATISTICAL_OFF(c) \
|
||||
MLX5_BYTE_OFF(ppcnt_reg, \
|
||||
counter_set.phys_layer_statistical_cntrs.c##_high)
|
||||
@ -2066,6 +2111,7 @@ static const struct counter_desc sq_stats_desc[] = {
|
||||
{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, csum_partial_inner) },
|
||||
{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, added_vlan_packets) },
|
||||
{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, nop) },
|
||||
{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, timestamps) },
|
||||
{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, mpwqe_blks) },
|
||||
{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, mpwqe_pkts) },
|
||||
#ifdef CONFIG_MLX5_EN_TLS
|
||||
@ -2178,6 +2224,7 @@ static const struct counter_desc ptp_cq_stats_desc[] = {
|
||||
{ MLX5E_DECLARE_PTP_CQ_STAT(struct mlx5e_ptp_cq_stats, abort) },
|
||||
{ MLX5E_DECLARE_PTP_CQ_STAT(struct mlx5e_ptp_cq_stats, abort_abs_diff_ns) },
|
||||
{ MLX5E_DECLARE_PTP_CQ_STAT(struct mlx5e_ptp_cq_stats, late_cqe) },
|
||||
{ MLX5E_DECLARE_PTP_CQ_STAT(struct mlx5e_ptp_cq_stats, lost_cqe) },
|
||||
};
|
||||
|
||||
static const struct counter_desc ptp_rq_stats_desc[] = {
|
||||
@ -2217,6 +2264,7 @@ static const struct counter_desc qos_sq_stats_desc[] = {
|
||||
{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, csum_partial_inner) },
|
||||
{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, added_vlan_packets) },
|
||||
{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, nop) },
|
||||
{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, timestamps) },
|
||||
{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, mpwqe_blks) },
|
||||
{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, mpwqe_pkts) },
|
||||
#ifdef CONFIG_MLX5_EN_TLS
|
||||
|
@ -128,6 +128,8 @@ void mlx5e_stats_eth_ctrl_get(struct mlx5e_priv *priv,
|
||||
void mlx5e_stats_rmon_get(struct mlx5e_priv *priv,
|
||||
struct ethtool_rmon_stats *rmon,
|
||||
const struct ethtool_rmon_hist_range **ranges);
|
||||
void mlx5e_stats_ts_get(struct mlx5e_priv *priv,
|
||||
struct ethtool_ts_stats *ts_stats);
|
||||
void mlx5e_get_link_ext_stats(struct net_device *dev,
|
||||
struct ethtool_link_ext_stats *stats);
|
||||
|
||||
@ -431,6 +433,7 @@ struct mlx5e_sq_stats {
|
||||
u64 stopped;
|
||||
u64 dropped;
|
||||
u64 recover;
|
||||
u64 timestamps;
|
||||
/* dirtied @completion */
|
||||
u64 cqes ____cacheline_aligned_in_smp;
|
||||
u64 wake;
|
||||
@ -463,6 +466,7 @@ struct mlx5e_ptp_cq_stats {
|
||||
u64 abort;
|
||||
u64 abort_abs_diff_ns;
|
||||
u64 late_cqe;
|
||||
u64 lost_cqe;
|
||||
};
|
||||
|
||||
struct mlx5e_rep_stats {
|
||||
|
@ -750,11 +750,13 @@ static void mlx5e_consume_skb(struct mlx5e_txqsq *sq, struct sk_buff *skb,
|
||||
u64 ts = get_cqe_ts(cqe);
|
||||
|
||||
hwts.hwtstamp = mlx5e_cqe_ts_to_ns(sq->ptp_cyc2time, sq->clock, ts);
|
||||
if (sq->ptpsq)
|
||||
if (sq->ptpsq) {
|
||||
mlx5e_skb_cb_hwtstamp_handler(skb, MLX5E_SKB_CB_CQE_HWTSTAMP,
|
||||
hwts.hwtstamp, sq->ptpsq->cq_stats);
|
||||
else
|
||||
} else {
|
||||
skb_tstamp_tx(skb, &hwts);
|
||||
sq->stats->timestamps++;
|
||||
}
|
||||
}
|
||||
|
||||
napi_consume_skb(skb, napi_budget);
|
||||
|
@ -480,6 +480,26 @@ struct ethtool_rmon_stats {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* struct ethtool_ts_stats - HW timestamping statistics
|
||||
* @pkts: Number of packets successfully timestamped by the hardware.
|
||||
* @lost: Number of hardware timestamping requests where the timestamping
|
||||
* information from the hardware never arrived for submission with
|
||||
* the skb.
|
||||
* @err: Number of arbitrary timestamp generation error events that the
|
||||
* hardware encountered, exclusive of @lost statistics. Cases such
|
||||
* as resource exhaustion, unavailability, firmware errors, and
|
||||
* detected illogical timestamp values not submitted with the skb
|
||||
* are inclusive to this counter.
|
||||
*/
|
||||
struct ethtool_ts_stats {
|
||||
struct_group(tx_stats,
|
||||
u64 pkts;
|
||||
u64 lost;
|
||||
u64 err;
|
||||
);
|
||||
};
|
||||
|
||||
#define ETH_MODULE_EEPROM_PAGE_LEN 128
|
||||
#define ETH_MODULE_MAX_I2C_ADDRESS 0x7f
|
||||
|
||||
@ -755,7 +775,10 @@ struct ethtool_rxfh_param {
|
||||
* @get_ts_info: Get the time stamping and PTP hardware clock capabilities.
|
||||
* It may be called with RCU, or rtnl or reference on the device.
|
||||
* Drivers supporting transmit time stamps in software should set this to
|
||||
* ethtool_op_get_ts_info().
|
||||
* ethtool_op_get_ts_info(). Drivers must not zero statistics which they
|
||||
* don't report. The stats structure is initialized to ETHTOOL_STAT_NOT_SET
|
||||
* indicating driver does not report statistics.
|
||||
* @get_ts_stats: Query the device hardware timestamping statistics.
|
||||
* @get_module_info: Get the size and type of the eeprom contained within
|
||||
* a plug-in module.
|
||||
* @get_module_eeprom: Get the eeprom information from the plug-in module
|
||||
@ -898,6 +921,8 @@ struct ethtool_ops {
|
||||
struct ethtool_dump *, void *);
|
||||
int (*set_dump)(struct net_device *, struct ethtool_dump *);
|
||||
int (*get_ts_info)(struct net_device *, struct ethtool_ts_info *);
|
||||
void (*get_ts_stats)(struct net_device *dev,
|
||||
struct ethtool_ts_stats *ts_stats);
|
||||
int (*get_module_info)(struct net_device *,
|
||||
struct ethtool_modinfo *);
|
||||
int (*get_module_eeprom)(struct net_device *,
|
||||
|
@ -117,12 +117,11 @@ enum {
|
||||
|
||||
/* request header */
|
||||
|
||||
/* use compact bitsets in reply */
|
||||
#define ETHTOOL_FLAG_COMPACT_BITSETS (1 << 0)
|
||||
/* provide optional reply for SET or ACT requests */
|
||||
#define ETHTOOL_FLAG_OMIT_REPLY (1 << 1)
|
||||
/* request statistics, if supported by the driver */
|
||||
#define ETHTOOL_FLAG_STATS (1 << 2)
|
||||
enum ethtool_header_flags {
|
||||
ETHTOOL_FLAG_COMPACT_BITSETS = 1 << 0, /* use compact bitsets in reply */
|
||||
ETHTOOL_FLAG_OMIT_REPLY = 1 << 1, /* provide optional reply for SET or ACT requests */
|
||||
ETHTOOL_FLAG_STATS = 1 << 2, /* request statistics, if supported by the driver */
|
||||
};
|
||||
|
||||
#define ETHTOOL_FLAG_ALL (ETHTOOL_FLAG_COMPACT_BITSETS | \
|
||||
ETHTOOL_FLAG_OMIT_REPLY | \
|
||||
@ -478,12 +477,26 @@ enum {
|
||||
ETHTOOL_A_TSINFO_TX_TYPES, /* bitset */
|
||||
ETHTOOL_A_TSINFO_RX_FILTERS, /* bitset */
|
||||
ETHTOOL_A_TSINFO_PHC_INDEX, /* u32 */
|
||||
ETHTOOL_A_TSINFO_STATS, /* nest - _A_TSINFO_STAT */
|
||||
|
||||
/* add new constants above here */
|
||||
__ETHTOOL_A_TSINFO_CNT,
|
||||
ETHTOOL_A_TSINFO_MAX = (__ETHTOOL_A_TSINFO_CNT - 1)
|
||||
};
|
||||
|
||||
enum {
|
||||
ETHTOOL_A_TS_STAT_UNSPEC,
|
||||
|
||||
ETHTOOL_A_TS_STAT_TX_PKTS, /* u64 */
|
||||
ETHTOOL_A_TS_STAT_TX_LOST, /* u64 */
|
||||
ETHTOOL_A_TS_STAT_TX_ERR, /* u64 */
|
||||
|
||||
/* add new constants above here */
|
||||
__ETHTOOL_A_TS_STAT_CNT,
|
||||
ETHTOOL_A_TS_STAT_MAX = (__ETHTOOL_A_TS_STAT_CNT - 1)
|
||||
|
||||
};
|
||||
|
||||
/* PHC VCLOCKS */
|
||||
|
||||
enum {
|
||||
|
@ -13,14 +13,18 @@ struct tsinfo_req_info {
|
||||
struct tsinfo_reply_data {
|
||||
struct ethnl_reply_data base;
|
||||
struct ethtool_ts_info ts_info;
|
||||
struct ethtool_ts_stats stats;
|
||||
};
|
||||
|
||||
#define TSINFO_REPDATA(__reply_base) \
|
||||
container_of(__reply_base, struct tsinfo_reply_data, base)
|
||||
|
||||
#define ETHTOOL_TS_STAT_CNT \
|
||||
(__ETHTOOL_A_TS_STAT_CNT - (ETHTOOL_A_TS_STAT_UNSPEC + 1))
|
||||
|
||||
const struct nla_policy ethnl_tsinfo_get_policy[] = {
|
||||
[ETHTOOL_A_TSINFO_HEADER] =
|
||||
NLA_POLICY_NESTED(ethnl_header_policy),
|
||||
NLA_POLICY_NESTED(ethnl_header_policy_stats),
|
||||
};
|
||||
|
||||
static int tsinfo_prepare_data(const struct ethnl_req_info *req_base,
|
||||
@ -34,6 +38,12 @@ static int tsinfo_prepare_data(const struct ethnl_req_info *req_base,
|
||||
ret = ethnl_ops_begin(dev);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
if (req_base->flags & ETHTOOL_FLAG_STATS &&
|
||||
dev->ethtool_ops->get_ts_stats) {
|
||||
ethtool_stats_init((u64 *)&data->stats,
|
||||
sizeof(data->stats) / sizeof(u64));
|
||||
dev->ethtool_ops->get_ts_stats(dev, &data->stats);
|
||||
}
|
||||
ret = __ethtool_get_ts_info(dev, &data->ts_info);
|
||||
ethnl_ops_complete(dev);
|
||||
|
||||
@ -79,10 +89,47 @@ static int tsinfo_reply_size(const struct ethnl_req_info *req_base,
|
||||
}
|
||||
if (ts_info->phc_index >= 0)
|
||||
len += nla_total_size(sizeof(u32)); /* _TSINFO_PHC_INDEX */
|
||||
if (req_base->flags & ETHTOOL_FLAG_STATS)
|
||||
len += nla_total_size(0) + /* _TSINFO_STATS */
|
||||
nla_total_size_64bit(sizeof(u64)) * ETHTOOL_TS_STAT_CNT;
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static int tsinfo_put_stat(struct sk_buff *skb, u64 val, u16 attrtype)
|
||||
{
|
||||
if (val == ETHTOOL_STAT_NOT_SET)
|
||||
return 0;
|
||||
if (nla_put_uint(skb, attrtype, val))
|
||||
return -EMSGSIZE;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int tsinfo_put_stats(struct sk_buff *skb,
|
||||
const struct ethtool_ts_stats *stats)
|
||||
{
|
||||
struct nlattr *nest;
|
||||
|
||||
nest = nla_nest_start(skb, ETHTOOL_A_TSINFO_STATS);
|
||||
if (!nest)
|
||||
return -EMSGSIZE;
|
||||
|
||||
if (tsinfo_put_stat(skb, stats->tx_stats.pkts,
|
||||
ETHTOOL_A_TS_STAT_TX_PKTS) ||
|
||||
tsinfo_put_stat(skb, stats->tx_stats.lost,
|
||||
ETHTOOL_A_TS_STAT_TX_LOST) ||
|
||||
tsinfo_put_stat(skb, stats->tx_stats.err,
|
||||
ETHTOOL_A_TS_STAT_TX_ERR))
|
||||
goto err_cancel;
|
||||
|
||||
nla_nest_end(skb, nest);
|
||||
return 0;
|
||||
|
||||
err_cancel:
|
||||
nla_nest_cancel(skb, nest);
|
||||
return -EMSGSIZE;
|
||||
}
|
||||
|
||||
static int tsinfo_fill_reply(struct sk_buff *skb,
|
||||
const struct ethnl_req_info *req_base,
|
||||
const struct ethnl_reply_data *reply_base)
|
||||
@ -119,6 +166,9 @@ static int tsinfo_fill_reply(struct sk_buff *skb,
|
||||
if (ts_info->phc_index >= 0 &&
|
||||
nla_put_u32(skb, ETHTOOL_A_TSINFO_PHC_INDEX, ts_info->phc_index))
|
||||
return -EMSGSIZE;
|
||||
if (req_base->flags & ETHTOOL_FLAG_STATS &&
|
||||
tsinfo_put_stats(skb, &data->stats))
|
||||
return -EMSGSIZE;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -324,7 +324,13 @@ def main():
|
||||
return
|
||||
|
||||
if args.show_time_stamping:
|
||||
tsinfo = dumpit(ynl, args, 'tsinfo-get')
|
||||
req = {
|
||||
'header': {
|
||||
'flags': 'stats',
|
||||
},
|
||||
}
|
||||
|
||||
tsinfo = dumpit(ynl, args, 'tsinfo-get', req)
|
||||
|
||||
print(f'Time stamping parameters for {args.device}:')
|
||||
|
||||
@ -338,6 +344,9 @@ def main():
|
||||
|
||||
print('Hardware Receive Filter Modes:')
|
||||
[print(f'\t{v}') for v in bits_to_dict(tsinfo['rx-filters'])]
|
||||
|
||||
print('Statistics:')
|
||||
[print(f'\t{k}: {v}') for k, v in tsinfo['stats'].items()]
|
||||
return
|
||||
|
||||
print(f'Settings for {args.device}:')
|
||||
|
Loading…
Reference in New Issue
Block a user