Files
openairinterface5g/radio/ETHERNET/ethernet_lib.c
Robert Schmidt 6d75ca8e9c Move fhstate into eth structure
fhstate is only used in the ETHERNET library, so move it to a smaller
scope. In AW2S it is write-only, so we can remove it.

Signed-off-by: Robert Schmidt <robert.schmidt@openairinterface.org>
2026-07-02 08:46:09 +02:00

601 lines
21 KiB
C

/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
/*!
* \brief API to stream I/Q samples over standard ethernet
*/
#include "utils.h"
#include <arpa/inet.h>
#include <linux/if_packet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <netinet/ether.h>
#include <unistd.h>
#include <errno.h>
#include <linux/sysctl.h>
#include "common_lib.h"
#include "ethernet_lib.h"
int num_devices_eth = 0;
struct sockaddr_in dest_addr[MAX_INST];
int dest_addr_len[MAX_INST];
int load_lib(openair0_device_t *device, openair0_config_t *openair0_cfg, eth_params_t *cfg, uint8_t flag);
int trx_eth_start(openair0_device_t *device)
{
eth_state_t *eth = (eth_state_t *)device->priv;
if (eth->flags == ETH_UDP_IF5_ECPRI_MODE) {
AssertFatal(device->thirdparty_init != NULL, "device->thirdparty_init is null\n");
AssertFatal(device->thirdparty_init(device) == 0, "third-party init failed\n");
device->openair0_cfg->samples_per_packet = 256;
eth->num_fd = 1; // max(device->openair0_cfg->rx_num_channels,device->openair0_cfg->tx_num_channels);
udp_ctx_t *u[1 + eth->num_fd];
eth->utx = malloc_or_fail(sizeof(eth->utx));
for (int i = 0; i < eth->num_fd; i++) {
u[i] = malloc_or_fail(sizeof(udp_ctx_t));
u[i]->thread_id = i;
u[i]->device = device;
printf("UDP Read Thread %d on core %d\n", i, device->openair0_cfg->rxfh_cores[i]);
threadCreate(&u[i]->pthread,
udp_read_thread,
u[i],
"udp read thread",
device->openair0_cfg->rxfh_cores[i],
OAI_PRIORITY_RT_MAX);
eth->utx[i] = malloc_or_fail(sizeof(udp_ctx_t));
eth->utx[i]->thread_id = i;
eth->utx[i]->device = device;
printf("UDP Write Thread %d on core %d\n", i, device->openair0_cfg->txfh_cores[i]);
threadCreate(&eth->utx[i]->pthread,
udp_write_thread,
eth->utx[i],
"udp write thread",
device->openair0_cfg->txfh_cores[i],
OAI_PRIORITY_RT_MAX);
}
eth->sampling_rate_ratio_n = 1;
eth->sampling_rate_ratio_d = 1;
if (device->openair0_cfg->nr_flag == 1) { // This check if RRU knows about NR numerologies
if (device->openair0_cfg->num_rb_dl <= 162 && device->openair0_cfg->num_rb_dl > 106) {
eth->sampling_rate_ratio_n = 2;
eth->txrx_offset = 5500;
} else if (device->openair0_cfg->num_rb_dl <= 106 && device->openair0_cfg->num_rb_dl > 51) {
eth->sampling_rate_ratio_d = 3;
eth->sampling_rate_ratio_n = 4;
eth->txrx_offset = 3750;
} else if (device->openair0_cfg->num_rb_dl == 51) {
eth->sampling_rate_ratio_n = 1;
eth->sampling_rate_ratio_d = 1;
eth->txrx_offset = 7500;
} else
AssertFatal(1 == 0, "num_rb_dl %d not ok with ECPRI\n", device->openair0_cfg->num_rb_dl);
} else {
if (device->openair0_cfg->num_rb_dl == 100 || device->openair0_cfg->num_rb_dl == 51) {
eth->sampling_rate_ratio_d = 1;
eth->txrx_offset = 7500;
} else if (device->openair0_cfg->num_rb_dl == 75) {
eth->sampling_rate_ratio_d = 4;
eth->sampling_rate_ratio_n = 3;
eth->txrx_offset = 7500;
} else if (device->openair0_cfg->num_rb_dl == 50) {
eth->sampling_rate_ratio_d = 2;
eth->txrx_offset = 7500;
} else if (device->openair0_cfg->num_rb_dl == 25) {
eth->sampling_rate_ratio_d = 4;
eth->txrx_offset = 7500;
} else
AssertFatal(1 == 0, "num_rb_dl %d not ok with ECPRI\n", device->openair0_cfg->num_rb_dl);
}
#ifdef USE_TX_TPOOL
int threadCnt = device->openair0_cfg->tx_num_channels;
if (threadCnt < 2)
LOG_E(PHY, "Number of threads for gNB should be more than 1. Allocated only %d\n", threadCnt);
char pool[80];
sprintf(pool, "-1");
int s_offset = 0;
for (int icpu = 1; icpu < threadCnt; icpu++) {
sprintf(pool + 2 + s_offset, ",-1");
s_offset += 3;
}
device->threadPool = (tpool_t *)malloc(sizeof(tpool_t));
initTpool(pool, device->threadPool, cpumeas(CPUMEAS_GETSTATE));
#endif
}
/* initialize socket */
if (eth->flags == ETH_RAW_MODE) {
printf("Setting ETHERNET to ETH_RAW_IF5_MODE\n");
if (eth_socket_init_raw(device) != 0)
return -1;
/* RRU gets device configuration - RAU sets device configuration*/
printf("Setting Timenout to 999999 usecs\n");
if (ethernet_tune(device, RCV_TIMEOUT, 999999) != 0)
return -1;
/*
if (device->host_type == RAU_HOST) {
if(eth_set_dev_conf_raw(device)!=0) return -1;
} else {
if(eth_get_dev_conf_raw(device)!=0) return -1;
}*/
/* adjust MTU wrt number of samples per packet */
if (eth->compression == ALAW_COMPRESS) {
if (ethernet_tune(device, MTU_SIZE, RAW_PACKET_SIZE_BYTES_ALAW(device->openair0_cfg->samples_per_packet)) != 0)
return -1;
} else {
if (ethernet_tune(device, MTU_SIZE, RAW_PACKET_SIZE_BYTES(device->openair0_cfg->samples_per_packet)) != 0)
return -1;
}
if (ethernet_tune(device, RCV_TIMEOUT, 999999) != 0)
return -1;
} else if (eth->flags == ETH_RAW_IF4p5_MODE) {
printf("Setting ETHERNET to ETH_RAW_IF4p5_MODE\n");
if (eth_socket_init_raw(device) != 0)
return -1;
printf("Setting Timenout to 999999 usecs\n");
if (ethernet_tune(device, RCV_TIMEOUT, 999999) != 0)
return -1;
/*
if (device->host_type == RAU_HOST) {
if(eth_set_dev_conf_raw_IF4p5(device)!=0) return -1;
} else {
if(eth_get_dev_conf_raw_IF4p5(device)!=0) return -1;
}
*/
/* adjust MTU wrt number of samples per packet */
if (ethernet_tune(device, MTU_SIZE, RAW_IF4p5_PRACH_SIZE_BYTES) != 0)
return -1;
if (ethernet_tune(device, RCV_TIMEOUT, 999999) != 0)
return -1;
} else if (eth->flags == ETH_UDP_IF4p5_MODE) {
printf("Setting ETHERNET to UDP_IF4p5_MODE\n");
if (eth_socket_init_udp(device) != 0)
return -1;
printf("Setting Timeout to 999999 usecs\n");
if (ethernet_tune(device, RCV_TIMEOUT, 999999) != 0)
return -1;
/*
if (device->host_type == RAU_HOST) {
if(eth_set_dev_conf_udp(device)!=0) return -1;
} else {
if(eth_get_dev_conf_udp(device)!=0) return -1;
}
*/
/* adjust MTU wrt number of samples per packet */
// if(ethernet_tune (device,MTU_SIZE,UDP_IF4p5_PRACH_SIZE_BYTES)!=0) return -1;
} else {
printf("Setting ETHERNET to UDP_IF5_MODE\n");
if (eth_socket_init_udp(device) != 0)
return -1;
/*
if (device->host_type == RAU_HOST) {
if(eth_set_dev_conf_udp(device)!=0) return -1;
} else {
if(eth_get_dev_conf_udp(device)!=0) return -1;
}*/
// if(ethernet_tune (device,MTU_SIZE,UDP_IF4p5_PRACH_SIZE_BYTES)!=0) return -1;
if (ethernet_tune(device, RCV_TIMEOUT, 999999) != 0)
return -1;
}
/* apply additional configuration */
if (ethernet_tune(device, SND_BUF_SIZE, 67108864) != 0)
return -1;
if (ethernet_tune(device, RCV_BUF_SIZE, 67108864) != 0)
return -1;
if (ethernet_tune(device, KERNEL_SND_BUF_MAX_SIZE, 67108864) != 0)
return -1;
if (ethernet_tune(device, KERNEL_RCV_BUF_MAX_SIZE, 67108864) != 0)
return -1;
if (ethernet_tune(device, TX_Q_LEN, 10000) != 0)
return -1;
return 0;
}
void trx_eth_end(openair0_device_t *device)
{
eth_state_t *eth = (eth_state_t *)device->priv;
/* destroys socket only for the processes that call the eth_end fuction-- shutdown() for beaking the pipe */
for (int i = 0; i < eth->num_fd; i++)
if (close(eth->sockfdd[i]) < 0) {
perror("ETHERNET: Failed to close socket");
exit(0);
} else {
printf("[%s] socket has been successfully closed.\n", (device->host_type == RAU_HOST) ? "RAU" : "RRU");
}
}
int trx_eth_stop(openair0_device_t *device)
{
eth_state_t *eth = (eth_state_t *)device->priv;
if (eth->flags == ETH_UDP_IF5_ECPRI_MODE) {
AssertFatal(device->thirdparty_cleanup != NULL, "device->thirdparty_cleanup is null\n");
AssertFatal(device->thirdparty_cleanup(device) == 0, "third-party cleanup failed\n");
}
return (0);
}
int trx_eth_set_freq(openair0_device_t *device, openair0_config_t *openair0_cfg)
{
return (0);
}
int trx_eth_set_gains(openair0_device_t *device, openair0_config_t *openair0_cfg)
{
return (0);
}
int trx_eth_get_stats(openair0_device_t *device)
{
return (0);
}
int trx_eth_reset_stats(openair0_device_t *device)
{
return (0);
}
int trx_eth_write_init(openair0_device_t *device)
{
return 0;
}
int ethernet_tune(openair0_device_t *device, unsigned int option, int value)
{
eth_state_t *eth = (eth_state_t *)device->priv;
struct timeval timeout;
struct ifreq ifr;
char system_cmd[256];
int ret = 0;
// int i=0;
/****************** socket level options ************************/
switch (option) {
case SND_BUF_SIZE: /* transmit socket buffer size */
for (int i = 0; i < eth->num_fd; i++)
if (setsockopt(eth->sockfdd[i], SOL_SOCKET, SO_SNDBUF, &value, sizeof(value))) {
perror("[ETHERNET] setsockopt()");
} else {
printf("send buffer size= %d bytes\n", value);
}
break;
case RCV_BUF_SIZE: /* receive socket buffer size */
for (int i = 0; i < eth->num_fd; i++) {
if (setsockopt(eth->sockfdd[i], SOL_SOCKET, SO_RCVBUF, &value, sizeof(value))) {
perror("[ETHERNET] setsockopt()");
} else {
printf("receive bufffer size= %d bytes\n", value);
}
}
break;
case RCV_TIMEOUT:
timeout.tv_sec = value / 1000000;
timeout.tv_usec = value % 1000000; // less than rt_period?
if (setsockopt(eth->sockfdc, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout))) {
perror("[ETHERNET] setsockopt()");
} else {
printf("receive timeout= %u usec\n", (unsigned int)timeout.tv_usec);
}
for (int i = 0; i < eth->num_fd; i++) {
if (setsockopt(eth->sockfdd[i], SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout))) {
perror("[ETHERNET] setsockopt()");
} else {
printf("receive timeout= %u usec\n", (unsigned int)timeout.tv_usec);
}
}
break;
case SND_TIMEOUT:
timeout.tv_sec = value / 1000000000;
timeout.tv_usec = value % 1000000000; // less than rt_period?
if (setsockopt(eth->sockfdc, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout))) {
perror("[ETHERNET] setsockopt()");
} else {
printf("send timeout= %d,%d sec\n", (int)timeout.tv_sec, (int)timeout.tv_usec);
}
for (int i = 0; i < eth->num_fd; i++) {
if (setsockopt(eth->sockfdd[i], SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout))) {
perror("[ETHERNET] setsockopt()");
} else {
printf("send timeout= %d,%d sec\n", (int)timeout.tv_sec, (int)timeout.tv_usec);
}
}
break;
/******************* interface level options *************************/
case MTU_SIZE: /* change MTU of the eth interface */
ifr.ifr_addr.sa_family = AF_INET;
strncpy(ifr.ifr_name, eth->if_name, sizeof(ifr.ifr_name) - 1);
ifr.ifr_mtu = value;
for (int i = 0; i < eth->num_fd; i++) {
if (ioctl(eth->sockfdd[i], SIOCSIFMTU, (caddr_t)&ifr) < 0)
perror("[ETHERNET] Can't set the MTU");
else
printf("[ETHERNET] %s MTU size has changed to %d\n", eth->if_name, ifr.ifr_mtu);
}
break;
case TX_Q_LEN: /* change TX queue length of eth interface */
ifr.ifr_addr.sa_family = AF_INET;
strncpy(ifr.ifr_name, eth->if_name, sizeof(ifr.ifr_name) - 1);
ifr.ifr_qlen = value;
for (int i = 0; i < eth->num_fd; i++) {
if (ioctl(eth->sockfdd[i], SIOCSIFTXQLEN, (caddr_t)&ifr) < 0)
perror("[ETHERNET] Can't set the txqueuelen");
else
printf("[ETHERNET] %s txqueuelen size has changed to %d\n", eth->if_name, ifr.ifr_qlen);
}
break;
/******************* device level options *************************/
case COALESCE_PAR:
ret = snprintf(system_cmd, sizeof(system_cmd), "ethtool -C %s rx-usecs %d", eth->if_name, value);
if (ret > 0) {
ret = system(system_cmd);
if (ret == -1) {
fprintf(stderr, "[ETHERNET] Can't start shell to execute %s %s", system_cmd, strerror(errno));
} else {
printf("[ETHERNET] status of %s is %d\n", system_cmd, WEXITSTATUS(ret));
}
printf("[ETHERNET] Coalesce parameters %s\n", system_cmd);
} else {
perror("[ETHERNET] Can't set coalesce parameters\n");
}
break;
case PAUSE_PAR:
if (value == 1)
ret = snprintf(system_cmd, sizeof(system_cmd), "ethtool -A %s autoneg off rx off tx off", eth->if_name);
else if (value == 0)
ret = snprintf(system_cmd, sizeof(system_cmd), "ethtool -A %s autoneg on rx on tx on", eth->if_name);
else
break;
if (ret > 0) {
ret = system(system_cmd);
if (ret == -1) {
fprintf(stderr, "[ETHERNET] Can't start shell to execute %s %s", system_cmd, strerror(errno));
} else {
printf("[ETHERNET] status of %s is %d\n", system_cmd, WEXITSTATUS(ret));
}
printf("[ETHERNET] Pause parameters %s\n", system_cmd);
} else {
perror("[ETHERNET] Can't set pause parameters\n");
}
break;
case RING_PAR:
ret = snprintf(system_cmd, sizeof(system_cmd), "ethtool -G %s val %d", eth->if_name, value);
if (ret > 0) {
ret = system(system_cmd);
if (ret == -1) {
fprintf(stderr, "[ETHERNET] Can't start shell to execute %s %s", system_cmd, strerror(errno));
} else {
printf("[ETHERNET] status of %s is %d\n", system_cmd, WEXITSTATUS(ret));
}
printf("[ETHERNET] Ring parameters %s\n", system_cmd);
} else {
perror("[ETHERNET] Can't set ring parameters\n");
}
break;
case KERNEL_RCV_BUF_MAX_SIZE:
ret = snprintf(system_cmd,
sizeof(system_cmd),
"sysctl -w net.core.rmem_max=%d;sysctl -w net.core.rmem_default=%d",
value,
value);
if (ret > 0) {
ret = system(system_cmd);
if (ret == -1) {
fprintf(stderr, "[ETHERNET] Can't start shell to execute %s %s", system_cmd, strerror(errno));
} else {
printf("[ETHERNET] status of %s is %d\n", system_cmd, WEXITSTATUS(ret));
}
printf("[ETHERNET] net core rmem %s\n", system_cmd);
} else {
perror("[ETHERNET] Can't set net core rmem\n");
}
break;
case KERNEL_SND_BUF_MAX_SIZE:
ret = snprintf(system_cmd,
sizeof(system_cmd),
"sysctl -w net.core.wmem_max=%d;sysctl -w net.core.wmem_default=%d;sysctl -w "
"net.core.netdev_max_backlog=5000;sysctl -w net.core.optmem_max=524288;",
value,
value);
if (ret > 0) {
ret = system(system_cmd);
if (ret == -1) {
fprintf(stderr, "[ETHERNET] Can't start shell to execute %s %s", system_cmd, strerror(errno));
} else {
printf("[ETHERNET] status of %s is %d\n", system_cmd, WEXITSTATUS(ret));
}
printf("[ETHERNET] net core wmem %s\n", system_cmd);
} else {
perror("[ETHERNET] Can't set net core wmem\n");
}
break;
default:
break;
}
return 0;
}
int transport_init(openair0_device_t *device, openair0_config_t *openair0_cfg, eth_params_t *eth_params)
{
eth_state_t *eth = (eth_state_t *)malloc(sizeof(eth_state_t));
memset(eth, 0, sizeof(eth_state_t));
eth->flags = eth_params->transp_preference;
// load third-party driver
if (eth->flags == ETH_UDP_IF5_ECPRI_MODE)
load_lib(device, openair0_cfg, eth_params, RAU_REMOTE_THIRDPARTY_RADIO_HEAD);
if (eth_params->if_compress == 0) {
eth->compression = NO_COMPRESS;
} else if (eth_params->if_compress == 1) {
eth->compression = ALAW_COMPRESS;
} else {
printf("transport_init: Unknown compression scheme %d - default to ALAW", eth_params->if_compress);
eth->compression = ALAW_COMPRESS;
}
eth->num_fd = 1;
printf("[ETHERNET]: Initializing openair0_device_t for %s ...\n", ((device->host_type == RAU_HOST) ? "RAU" : "RRU"));
printf("[ETHERNET]: num_fd %d\n", eth->num_fd);
device->transp_type = ETHERNET_TP;
device->trx_start_func = trx_eth_start;
device->trx_get_stats_func = trx_eth_get_stats;
device->trx_reset_stats_func = trx_eth_reset_stats;
device->trx_end_func = trx_eth_end;
device->trx_stop_func = trx_eth_stop;
device->trx_set_freq_func = trx_eth_set_freq;
device->trx_set_gains_func = trx_eth_set_gains;
device->trx_write_init = trx_eth_write_init;
device->trx_read_func2 = NULL;
device->trx_read_func = NULL;
device->trx_write_func2 = NULL;
device->trx_write_func = NULL;
if (eth->flags == ETH_RAW_MODE) {
device->trx_write_func = trx_eth_write_raw;
device->trx_read_func = trx_eth_read_raw;
} else if (eth->flags == ETH_UDP_MODE || eth->flags == ETH_UDP_IF5_ECPRI_MODE) {
device->trx_write_func2 = trx_eth_write_udp;
device->trx_read_func2 = trx_eth_read_udp;
device->trx_ctlsend_func = trx_eth_ctlsend_udp;
device->trx_ctlrecv_func = trx_eth_ctlrecv_udp;
memset(&eth->fhstate, 0, sizeof(eth->fhstate));
printf("Setting %d RX channels to waiting\n", openair0_cfg->rx_num_channels);
for (int i = openair0_cfg->rx_num_channels; i < 8; i++)
eth->fhstate.r[i] = 1;
} else if (eth->flags == ETH_RAW_IF4p5_MODE) {
device->trx_write_func = trx_eth_write_raw_IF4p5;
device->trx_read_func = trx_eth_read_raw_IF4p5;
} else if (eth->flags == ETH_UDP_IF4p5_MODE) {
device->trx_write_func = trx_eth_write_udp_IF4p5;
device->trx_read_func = trx_eth_read_udp_IF4p5;
device->trx_ctlsend_func = trx_eth_ctlsend_udp;
device->trx_ctlrecv_func = trx_eth_ctlrecv_udp;
} else {
// device->trx_write_func = trx_eth_write_udp_IF4p5;
// device->trx_read_func = trx_eth_read_udp_IF4p5;
}
eth->if_name = eth_params->local_if_name;
device->priv = eth;
device->openair0_cfg = &openair0_cfg[0];
return 0;
}
/**************************************************************************************************************************
* DEBUGING-RELATED FUNCTIONS *
**************************************************************************************************************************/
void dump_packet(char *title, unsigned char *pkt, int bytes, unsigned int tx_rx_flag)
{
static int numSend = 1;
static int numRecv = 1;
int num, k;
char tmp[48];
unsigned short int cksum;
num = (tx_rx_flag) ? numSend++ : numRecv++;
for (k = 0; k < 24; k++)
sprintf(tmp + k, "%02X", pkt[k]);
cksum = calc_csum((unsigned short *)pkt, bytes >> 2);
printf("%s-%s (%06d): %s 0x%04X\n", title, (tx_rx_flag) ? "TX" : "RX", num, tmp, cksum);
}
unsigned short calc_csum(unsigned short *buf, int nwords)
{
unsigned long sum;
for (sum = 0; nwords > 0; nwords--)
sum += *buf++;
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return ~sum;
}
void dump_dev(openair0_device_t *device)
{
eth_state_t *eth = (eth_state_t *)device->priv;
printf("Ethernet device configuration:\n");
printf(" RB number: %i, sample rate: %lf \n", device->openair0_cfg->num_rb_dl, device->openair0_cfg->sample_rate);
printf(" RAU configured for %i tx/%i rx channels)\n",
device->openair0_cfg->tx_num_channels,
device->openair0_cfg->rx_num_channels);
printf(" Running flags: %s %s (\n",
((eth->flags & ETH_RAW_MODE) ? "RAW socket mode - " : ""),
((eth->flags & ETH_UDP_MODE) ? "UDP socket mode - " : ""));
printf(" Number of iqs dumped when displaying packets: %i\n\n", eth->iqdumpcnt);
}
void inline dump_txcounters(openair0_device_t *device)
{
eth_state_t *eth = (eth_state_t *)device->priv;
printf(" Ethernet device interface tx counters:\n");
printf(" Sent packets: %llu send errors: %i\n", (long long unsigned int)eth->tx_count, eth->num_tx_errors);
}
void inline dump_rxcounters(openair0_device_t *device)
{
eth_state_t *eth = (eth_state_t *)device->priv;
printf(" Ethernet device interface rx counters:\n");
printf(" Received packets: %llu missed packets errors: %i\n", (long long unsigned int)eth->rx_count, eth->num_underflows);
}
void inline dump_buff(openair0_device_t *device, char *buff, unsigned int tx_rx_flag, int nsamps)
{
char *strptr;
eth_state_t *eth = (eth_state_t *)device->priv;
/*need to add ts number of iqs in printf need to fix dump iqs call */
strptr = ((tx_rx_flag == TX_FLAG) ? "TX" : "RX");
printf("\n %s, nsamps=%i \n", strptr, nsamps);
if (tx_rx_flag == 1) {
dump_txcounters(device);
printf(" First %i iqs of TX buffer\n", eth->iqdumpcnt);
dump_iqs(buff, eth->iqdumpcnt);
} else {
dump_rxcounters(device);
printf(" First %i iqs of RX buffer\n", eth->iqdumpcnt);
dump_iqs(buff, eth->iqdumpcnt);
}
}
void dump_iqs(char *buff, int iq_cnt)
{
int i;
for (i = 0; i < iq_cnt; i++) {
printf("s%02i: Q=%+ij I=%+i%s", i, ((iqoai_t *)(buff))[i].q, ((iqoai_t *)(buff))[i].i, ((i + 1) % 3 == 0) ? "\n" : " ");
}
}