Commit 3e7f50cd authored by Alessandro Rubini's avatar Alessandro Rubini

Merge branch 'arch-sim'

parents 6b84951e 70709194
......@@ -44,9 +44,18 @@ config ARCH_WRS
help
Build PPSi for use in the WR switch. The computer is a standard
ARM-Linux host with hardware timestamping and internal PLLs
needed to achieve sub-ns synchnonization.
needed to achieve sub-ns synchronization.
The configuration selects the "White Rabbit" protocol extension.
config ARCH_SIMULATOR
bool "PPSi Simulator (hosted on Linux)"
help
Build a PPSi simulator. It's almost a unix full running slave,
with a simulated master stimulating it for test purposes. This
avoids to wait a long time to see how PPSi behaves.
This architecture uses standard Unix system calls, but the
code includes some Linux dependencies.
endchoice
config ARCH
......@@ -56,6 +65,7 @@ config ARCH
default "bare-x86-64" if ARCH_BARE_X86_64
default "wrpc" if ARCH_WRPC
default "wrs" if ARCH_WRS
default "sim" if ARCH_SIMULATOR
......
# All files are under A (short for ARCH): I'm lazy
A := arch-$(ARCH)
CFLAGS += -Itools
OBJ-y += $A/sim-startup.o \
$A/main-loop.o \
$A/sim-io.o \
$A/sim-conf.o \
lib/cmdline.o \
lib/conf.o \
lib/dump-funcs.o \
lib/libc-functions.o \
lib/div64.o
# Support only "sim" time operations
TIME := sim
include time-$(TIME)/Makefile
all: $(TARGET)
# to build the target, we need -lstd again, in case we call functions that
# were not selected yet (e.g., pp_open_globals() ).
$(TARGET): $(TARGET).o
$(CC) -Wl,-Map,$(TARGET).map2 -o $@ $(TARGET).o -lrt
#ifndef __ARCH_H__
#define __ARCH_H__
/* Architecture-specific defines, included by top-level stuff */
#include <arpa/inet.h> /* ntohs etc */
#include <stdlib.h> /* abs */
#endif /* __ARCH_H__ */
#ifndef __PPSI_ARCH_CONSTANTS_H__
#define __PPSI_ARCH_CONSTANTS_H__
#ifndef __PPSI_CONSTANTS_H__
#Warning "Please include <ppsi/constants.h> before <arch/constants.h>"
#endif
/* nothing to do here, we keep project-wide defaults */
#endif /* __PPSI_ARCH_CONSTANTS_H__ */
/*
* Copyright (C) 2013 CERN (www.cern.ch)
* Author: Pietro Fezzardi (pietrofezzardi@gmail.com)
*
* Released to the public domain
*/
/*
* This is the main loop for the simulator.
*/
#include <stdlib.h>
#include <errno.h>
#include <sys/select.h>
#include <linux/if_ether.h>
#include <ppsi/ppsi.h>
#include "ppsi-sim.h"
/* Call pp_state_machine for each instance. To be called periodically,
* when no packets are incoming */
static int run_all_state_machines(struct pp_globals *ppg)
{
int j;
int delay_ms = 0, delay_ms_j;
for (j = 0; j < ppg->nlinks; j++) {
struct pp_instance *ppi = INST(ppg, j);
sim_set_global_DS(ppi);
delay_ms_j = pp_state_machine(ppi, NULL, 0);
/* delay_ms is the least delay_ms among all instances */
if (j == 0)
delay_ms = delay_ms_j;
if (delay_ms_j < delay_ms)
delay_ms = delay_ms_j;
}
return delay_ms;
}
void sim_main_loop(struct pp_globals *ppg)
{
struct pp_instance *ppi;
struct sim_ppg_arch_data *data = SIM_PPG_ARCH(ppg);
int64_t delay_ns, tmp_ns;
int j, i;
/* Initialize each link's state machine */
for (j = 0; j < ppg->nlinks; j++) {
ppi = INST(ppg, j);
ppi->is_new_state = 1;
}
delay_ns = run_all_state_machines(ppg) * 1000LL * 1000LL;
while (data->duration_ns >= 0) {
/*
* If Ebest was changed in previous loop, run best
* master clock before checking for new packets, which
* would affect port state again
*/
if (ppg->ebest_updated) {
for (j = 0; j < ppg->nlinks; j++) {
int new_state;
struct pp_instance *ppi = INST(ppg ,j);
new_state = bmc(ppi);
if (new_state != ppi->state) {
ppi->state = new_state;
ppi->is_new_state = 1;
}
}
ppg->ebest_updated = 0;
}
while (data->n_pending && data->pending->delay_ns <= delay_ns) {
ppi = INST(ppg, data->pending->which_ppi);
sim_fast_forward_ns(ppg, data->pending->delay_ns);
delay_ns -= data->pending->delay_ns;
i = ppi->n_ops->recv(ppi, ppi->rx_frame,
PP_MAX_FRAME_LENGTH - 4,
&ppi->last_rcv_time);
if (i < PP_MINIMUM_LENGTH) {
pp_diag(ppi, frames, 1, "Error or short frame: "
"%d < %d\n", i, PP_MINIMUM_LENGTH);
continue;
}
sim_set_global_DS(ppi);
tmp_ns = 1000LL * 1000LL * pp_state_machine(ppi,
ppi->rx_ptp, i - NP(ppi)->ptp_offset);
if (tmp_ns < delay_ns)
delay_ns = tmp_ns;
}
/* here we have no pending packets or the timeout for a state
* machine is expired (so delay_ns == 0). If the timeout is not
* expired we just fast forward till it's not expired, since we
* know that there are no packets pending. */
sim_fast_forward_ns(ppg, delay_ns);
delay_ns = run_all_state_machines(ppg) * 1000LL * 1000LL;
}
return;
}
/*
* Author: Pietro Fezzardi (pietrofezzardi@gmail.com)
*
* Released to the public domain
*/
/*
* This structure represents a clock abstraction. You can use it to
* have different pp_instances with different notions of time.
* When you try to get the time the sim_get_time function is performing the
* calculations needed to convert the raw data from the "real" clock to the
* timescale of the simulated one.
* When you set the time you're not actually setting it in the hardware but
* only in the parameters in this structure.
*/
struct pp_sim_time_instance {
int64_t current_ns; // with nsecs it's enough for ~300 years
int64_t freq_ppm_real; // drift of the simulated hw clock
int64_t freq_ppm_servo; // drift applied from servo to correct the hw
// Future parameters can be added
};
/*
* This structure holds the parameter representing the delays on the outgoing
* link of every pp_instance. All the values are expressed in the *absolute*
* timescale, which is represented by the master time.
*/
struct pp_sim_net_delay {
unsigned int t_prop_ns; // propagation delay on outgoing link
uint64_t jit_ns; // jitter in nsec on outgoing link
uint64_t last_outgoing_jit_ns;
};
/*
* This structure represets a pending packet. which_ppi is the destination ppi,
* chtype is the channel and delay_ns is the time that should pass from when the
* packet is sent until it will be received from the destination.
* Every time a packet is sent a structure like this is filled and stored in the
* ppg->arch_data so that we always know which is the first packet that has to
* be received and when.
*/
struct sim_pending_pkt {
int64_t delay_ns;
int which_ppi;
int chtype;
};
/*
* Structure holding an array of pending packets. 64 does not have a special
* meaning. They could be less if you look inside the ptp specification, but
* I put 64 just to be sure.
* The aim of this structure is to store information on flying packets and when
* the'll be received, because the standard state machine timeouts are not
* enough for this. Infact the main loop need to know if there are some packets
* arriving and when, otherwise it will not know how much fast forwarding is
* needed. If you fast forward based on timeouts they will expire before any
* packet has arrived and the state machine will do nothing.
*/
struct sim_ppg_arch_data {
int n_pending;
struct sim_pending_pkt pending[64];
int64_t duration_ns;
};
static inline struct sim_ppg_arch_data *SIM_PPG_ARCH(struct pp_globals *ppg)
{
return (struct sim_ppg_arch_data *)(ppg->arch_data);
}
/*
* Structure holding parameters and informations restricted to just a single
* instance, like timing informations. Infact in the simulator, even if both
* master and slave are ppi inside the same ppg, they act like every one of
* them had its own clock.
* Some more data we need in every instance are Data Sets, runtime options,
* servo and TimeProperties. We need them because, since we have two ppi
* acting like two different machines, we have to use different copies of
* these data for every one of them. So we just put the per-instance data
* here and we will the pointers in ppg according to our needs every time
* we will need to use them.
* Even more stuff can be added if needed
*/
struct sim_ppi_arch_data {
struct pp_sim_time_instance time;
struct pp_sim_net_delay n_delay;
/* servo */
struct pp_servo *servo;
/* Runtime options */
struct pp_runtime_opts *rt_opts;
/* Data sets */
DSDefault *defaultDS;
DSCurrent *currentDS;
DSParent *parentDS;
DSTimeProperties *timePropertiesDS;
/* other pp_instance, used in net ops */
struct pp_instance *other_ppi;
};
/* symbolic names to address master and slave in ppg->pp_instances */
#define SIM_SLAVE 1
#define SIM_MASTER 0
static inline struct sim_ppi_arch_data *SIM_PPI_ARCH(struct pp_instance *ppi)
{
return (struct sim_ppi_arch_data *)(ppi->arch_data);
}
static inline struct pp_instance *pp_sim_get_master(struct pp_globals *ppg)
{
return INST(ppg, SIM_MASTER);
}
static inline struct pp_instance *pp_sim_get_slave(struct pp_globals *ppg)
{
return INST(ppg, SIM_SLAVE);
}
static inline int pp_sim_is_master(struct pp_instance *ppi)
{
return ((ppi - ppi->glbs->pp_instances) == SIM_MASTER);
}
static inline int pp_sim_is_slave(struct pp_instance *ppi)
{
return ((ppi - ppi->glbs->pp_instances) == SIM_SLAVE);
}
extern int sim_fast_forward_ns(struct pp_globals *ppg, int64_t ff_ns);
extern int sim_set_global_DS(struct pp_instance *ppi);
extern void sim_main_loop(struct pp_globals *ppg);
/*
* Copyright (C) 2013 CERN (www.cern.ch)
* Author: Pietro Fezzardi (pietrofezzardi@gmail.com)
*
* Released according to GNU LGPL, version 2.1 or any later
*/
#include <ppsi/ppsi.h>
#include "ppsi-sim.h"
static int f_ppm_real(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
struct pp_instance *ppi_slave;
/* master clock is supposed to be perfect. parameters about ppm are
* modifiable only for slave ppi */
ppi_slave = pp_sim_get_slave(ppg);
SIM_PPI_ARCH(ppi_slave)->time.freq_ppm_real = arg->i * 1000;
return 0;
}
static int f_ppm_servo(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
struct pp_instance *ppi_slave;
/* master clock is supposed to be perfect. parameters about ppm are
* modifiable only for slave ppi */
ppi_slave = pp_sim_get_slave(ppg);
SIM_PPI_ARCH(ppi_slave)->time.freq_ppm_servo = arg->i * 1000;
return 0;
}
static int f_ofm(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
struct pp_sim_time_instance *t_master, *t_slave;
t_master = &SIM_PPI_ARCH(pp_sim_get_master(ppg))->time;
t_slave = &SIM_PPI_ARCH(pp_sim_get_slave(ppg))->time;
t_slave->current_ns = t_master->current_ns + arg->ts.tv_nsec +
arg->ts.tv_sec * (long long)PP_NSEC_PER_SEC;
return 0;
}
static int f_init_time(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
struct pp_sim_time_instance *t_inst;
t_inst = &SIM_PPI_ARCH(pp_sim_get_master(ppg))->time;
t_inst->current_ns = arg->ts.tv_nsec +
arg->ts.tv_sec * (long long)PP_NSEC_PER_SEC;
return 0;
}
static int f_fwd_t_prop(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
struct sim_ppi_arch_data *data;
data = SIM_PPI_ARCH(pp_sim_get_master(ppg));
data->n_delay.t_prop_ns = arg->i;
return 0;
}
static int f_bckwd_t_prop(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
struct sim_ppi_arch_data *data;
data = SIM_PPI_ARCH(pp_sim_get_slave(ppg));
data->n_delay.t_prop_ns = arg->i;
return 0;
}
static int f_t_prop(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
f_fwd_t_prop(lineno, ppg, arg);
f_bckwd_t_prop(lineno, ppg, arg);
return 0;
}
static int f_fwd_jit(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
struct sim_ppi_arch_data *data;
data = SIM_PPI_ARCH(pp_sim_get_master(ppg));
data->n_delay.jit_ns = arg->i;
return 0;
}
static int f_bckwd_jit(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
struct sim_ppi_arch_data *data;
data = SIM_PPI_ARCH(pp_sim_get_slave(ppg));
data->n_delay.jit_ns = arg->i;
return 0;
}
static int f_jit(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
f_fwd_jit(lineno, ppg, arg);
f_bckwd_jit(lineno, ppg, arg);
return 0;
}
static int f_duration(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg)
{
SIM_PPG_ARCH(ppg)->duration_ns = arg->ts.tv_nsec +
arg->ts.tv_sec * (long long)PP_NSEC_PER_SEC;
return 0;
}
struct pp_argline pp_arch_arglines[] = {
{f_ppm_real, "sim_ppm_real", ARG_INT},
{f_ppm_servo, "sim_init_ppm_servo", ARG_INT},
{f_ofm, "sim_init_ofm", ARG_TIME},
{f_init_time, "sim_init_master_time", ARG_TIME},
{f_t_prop, "sim_t_prop_ns", ARG_INT},
{f_fwd_t_prop, "sim_fwd_t_prop_ns", ARG_INT},
{f_bckwd_t_prop,"sim_bckwd_t_prop_ns", ARG_INT},
{f_jit, "sim_jit_ns", ARG_INT},
{f_fwd_jit, "sim_fwd_jit_ns", ARG_INT},
{f_bckwd_jit, "sim_bckwd_jit_ns", ARG_INT},
{f_duration, "sim_duration_sec", ARG_TIME},
{}
};
/*
* Copyright (C) 2011 CERN (www.cern.ch)
* Author: Alessandro Rubini
*
* Released to the public domain
*/
#include <stdio.h>
#include <ppsi/ppsi.h>
void pp_puts(const char *s)
{
fputs(s, stdout);
}
/*
* Copyright (C) 2013 CERN (www.cern.ch)
* Author: Pietro Fezzardi (pietrofezzardi@gmail.com)
*
* Released to the public domain
*/
#include <stdio.h>
#include <ppsi/ppsi.h>
#include "ppsi-sim.h"
static struct pp_runtime_opts sim_master_rt_opts = {
.clock_quality = {
.clockClass = PP_CLASS_WR_GM_LOCKED,
.clockAccuracy = PP_DEFAULT_CLOCK_ACCURACY,
.offsetScaledLogVariance = PP_DEFAULT_CLOCK_VARIANCE,
},
.inbound_latency = {0, PP_DEFAULT_INBOUND_LATENCY},
.outbound_latency = {0, PP_DEFAULT_OUTBOUND_LATENCY},
.max_rst = PP_DEFAULT_MAX_RESET,
.max_dly = PP_DEFAULT_MAX_DELAY,
.no_adjust = PP_DEFAULT_NO_ADJUST,
.no_rst_clk = PP_DEFAULT_NO_RESET_CLOCK,
.ap = PP_DEFAULT_AP,
.ai = PP_DEFAULT_AI,
.s = PP_DEFAULT_DELAY_S,
.announce_intvl = PP_DEFAULT_ANNOUNCE_INTERVAL,
.sync_intvl = PP_DEFAULT_SYNC_INTERVAL,
.prio1 = PP_DEFAULT_PRIORITY1,
.prio2 = PP_DEFAULT_PRIORITY2,
.domain_number = PP_DEFAULT_DOMAIN_NUMBER,
.ttl = PP_DEFAULT_TTL,
};
/*
* In arch-sim we use two pp_instaces in the same pp_globals to represent
* two different machines. This means *completely differnt* machines, with
* their own Data Sets. Given we can't put more all the different Data Sets
* in the same ppg, we stored them in the ppi->arch_data of every istance.
* This function is used to set the inner Data Sets pointer of the ppg to
* point to the Data Sets related to the pp_instange passed as argument
*/
int sim_set_global_DS(struct pp_instance *ppi)
{
struct sim_ppi_arch_data *data = SIM_PPI_ARCH(ppi);
ppi->glbs->defaultDS = data->defaultDS;
ppi->glbs->currentDS = data->currentDS;
ppi->glbs->parentDS = data->parentDS;
ppi->glbs->timePropertiesDS = data->timePropertiesDS;
ppi->glbs->servo = data->servo;
ppi->glbs->rt_opts = data->rt_opts;
return 0;
}
static int sim_ppi_init(struct pp_instance *ppi, int which_ppi)
{
struct sim_ppi_arch_data *data;
ppi->arch_data = calloc(1, sizeof(struct sim_ppi_arch_data));
ppi->portDS = calloc(1, sizeof(*ppi->portDS));
if ((!ppi->arch_data) || (!ppi->portDS))
return -1;
data = SIM_PPI_ARCH(ppi);
data->defaultDS = calloc(1, sizeof(*data->defaultDS));
data->currentDS = calloc(1, sizeof(*data->currentDS));
data->parentDS = calloc(1, sizeof(*data->parentDS));
data->timePropertiesDS = calloc(1,
sizeof(*data->timePropertiesDS));
data->servo = calloc(1, sizeof(*data->servo));
if ((!data->defaultDS) ||
(!data->currentDS) ||
(!data->parentDS) ||
(!data->timePropertiesDS) ||
(!data->servo))
return -1;
if (which_ppi == SIM_MASTER)
data->rt_opts = &sim_master_rt_opts;
else
data->rt_opts = &__pp_default_rt_opts;
data->other_ppi = INST(ppi->glbs, -(which_ppi - 1));
return 0;
}
int main(int argc, char **argv)
{
struct pp_globals *ppg;
struct pp_instance *ppi;
int i;
setbuf(stdout, NULL);
pp_printf("PPSi. Commit %s, built on " __DATE__ "\n", PPSI_VERSION);
ppg = calloc(1, sizeof(struct pp_globals));
ppg->max_links = 2; // master and slave, nothing else
ppg->arch_data = calloc(1, sizeof(struct sim_ppg_arch_data));
ppg->pp_instances = calloc(ppg->max_links, sizeof(struct pp_instance));
if ((!ppg->arch_data) || (!ppg->pp_instances))
return -1;
/* Alloc data stuctures inside the pp_instances */
for (i = 0; i < ppg->max_links; i++) {
ppi = INST(ppg, i);
ppi->glbs = ppg; // must be done before using sim_set_global_DS
if (sim_ppi_init(ppi, i))
return -1;
}
/*
* Configure the master with standard configuration, only from default
* string. The master is not configurable, but there's no need to do
* it cause we are ok with a standard one. We just want to see the
* behaviour of the slave.
* NOTE: the master instance is initialized before parsing the command
* line, so the diagnostics cannot be enabled here. We cannot put the
* master config later because the initial time for the master is needed
* to set the initial offset for the slave
*/
sim_set_global_DS(pp_sim_get_master(ppg));
pp_config_string(ppg, strdup("port SIM_MASTER; iface MASTER;"
"proto udp; role master;"
"sim_duration_sec 3600;" // one hour
"sim_init_master_time .9;"));
/* parse commandline for configuration options */
sim_set_global_DS(pp_sim_get_slave(ppg));
if (pp_parse_cmdline(ppg, argc, argv) != 0)
return -1;
/* If no item has been parsed, provide default file or string */
if (ppg->cfg.cfg_items == 0)
pp_config_file(ppg, 0, PP_DEFAULT_CONFIGFILE);
if (ppg->cfg.cfg_items == 0)
pp_config_string(ppg, strdup("port SIM_SLAVE; iface SLAVE;"
"proto udp; role slave;"));
for (i = 0; i < ppg->nlinks; i++) {
ppi = INST(ppg, i);
sim_set_global_DS(ppi);
ppi->iface_name = ppi->cfg.iface_name;
if (ppi->cfg.proto == PPSI_PROTO_RAW)
pp_printf("Warning: simulator doesn't support raw "
"ethernet. Using UDP\n");
ppi->ethernet_mode = 0;
NP(ppi)->ch[PP_NP_GEN].fd = -1;
NP(ppi)->ch[PP_NP_EVT].fd = -1;
if (ppi->cfg.role == PPSI_ROLE_MASTER) {
ppi->master_only = 1;
ppi->slave_only = 0;
} else if (ppi->cfg.role == PPSI_ROLE_SLAVE) {
ppi->master_only = 0;
ppi->slave_only = 1;
}
ppi->t_ops = &DEFAULT_TIME_OPS;
ppi->n_ops = &DEFAULT_NET_OPS;
if (pp_sim_is_master(ppi))
pp_init_globals(ppg, &sim_master_rt_opts);
else
pp_init_globals(ppg, &__pp_default_rt_opts);
}
sim_main_loop(ppg);
return 0;
}
......@@ -65,9 +65,9 @@ int main(int argc, char **argv)
return -1;
/* If no item has been parsed, provide a default file or string */
if (ppg->cfg_items == 0)
if (ppg->cfg.cfg_items == 0)
pp_config_file(ppg, 0, PP_DEFAULT_CONFIGFILE);
if (ppg->cfg_items == 0)
if (ppg->cfg.cfg_items == 0)
pp_config_string(ppg, strdup("link 0; iface eth0; proto udp"));
for (i = 0; i < ppg->nlinks; i++) {
......
......@@ -130,11 +130,11 @@ int main(int argc, char **argv)
return -1;
/* If no item has been parsed, provide a default file or string */
if (ppg->cfg_items == 0)
if (ppg->cfg.cfg_items == 0)
pp_config_file(ppg, 0, "/wr/etc/ppsi.conf");
if (ppg->cfg_items == 0)
if (ppg->cfg.cfg_items == 0)
pp_config_file(ppg, 0, PP_DEFAULT_CONFIGFILE);
if (ppg->cfg_items == 0) {
if (ppg->cfg.cfg_items == 0) {
/* Default configuration for WR switch is all ports */
char s[128];
int i;
......
#
# Automatically generated make config: don't edit
#
# CONFIG_ARCH_UNIX is not set
# CONFIG_ARCH_BARE_I386 is not set
# CONFIG_ARCH_BARE_X86_64 is not set
# CONFIG_ARCH_WRPC is not set
# CONFIG_ARCH_WRS is not set
CONFIG_ARCH_SIMULATOR=y
CONFIG_ARCH="sim"
# CONFIG_EXT_WR is not set
CONFIG_EXT_NONE=y
CONFIG_EXTENSION=""
CONFIG_CROSS_COMPILE=""
CONFIG_ARCH_CFLAGS=""
CONFIG_ARCH_LDFLAGS=""
......@@ -35,7 +35,7 @@
@setchapternewpage off
@set update-month November 2013
@set update-month March 2014
@set release __RELEASE_GIT_ID__
@finalout
......@@ -188,6 +188,14 @@ The package currently supports the following architectures:
engine. PPSi in this environment currently supports only raw
Ethernet, not @sc{udp}. The architecture uses @i{time-wrpc}.
@item sim
This is a simulator. It uses special time and network operations
to simulate a master and a slave exchanging ptp frames.
Arch-specific configuration options are implemented in
@i{arch-sim/sim-conf.c}. Use of the simulator is briefly
descibed in @ref{Configuring the Simulator}.
@item bare-i386
This architecture uses system calls towards the Linux kernel but
......@@ -369,9 +377,8 @@ As of 2013-05 the project suffers from these known bugs:
@item We removed @i{peer-delay} support. We plan to add it back, and
actually move White Rabbit to use @i{peer-delay} @sc{ptp} instead of
@i{end-to-end} @sc{ptp}.
@item The servo for standard-@sc{ptp} must be audited. Pietro Fezzardi
is working on @i{arch-sim} to simulate network transactions and
make serious work on the servo algorithm and parameters.
@item The servo for standard-@sc{ptp} must be audited. We are doing
it now using @i{arch-sim} support,
@item @sc{udp} over @sc{IPV6} is not yet supported.
@end itemize
......@@ -410,9 +417,9 @@ Each configuration item is made up of a keyword and an optional
argument. The argument can be either a number or a string. The
parser looks up keywords in three tables: a global table, an
architecture-specific table and an extension-specific table.
Currently, only @i{arch-sim} has specific configuration items.
Currently no architectures or extensions provide configuration
keywords, while the list of global items is on show in @t{lib/conf.c},
The list of global configuration items is on show in @t{lib/conf.c},
as the @t{pp_global_arglines} array. Future versions of this manual
may document the keywords, if the time allows it.
......@@ -420,6 +427,41 @@ may document the keywords, if the time allows it.
configuration options. This applies to the priorities, intervals and
thresholds, as well as the @i{slave-only} flag.
@c ==========================================================================
@node Configuring the Simulator
@section Configuring the Simulator
To tun the PPSi simulator you need to rely on diagnostics and specific
configuration items. The configuration items are defined in
@i{arch-sim/sim-conf.c} and are not individually documented here at
this point.
After building with ``@t{make sim_defconfig}'', you can look at how
PPSi behaves in different situation. For example, to see how the servo
works with default parameters you can activate servo messages at level
2, and only look at the offset from master:
@smallexample
./ppsi -d 0002 | grep 'Offset from master'
@end smallexample
The diagnostic values are specified in the range 0 to 2 and represent,
in this order: state machine, time, frames, servo, bmc, extensions.
See @ref{Diagnostic Macros} for details.
The simulator runs by default for one hour of simulated time (in a
fraction of a second of running time), and the initial offset from
master to slave is 0.9 seconds.
To pass configuration options, you can use the @t{-C} command line option.
so, for example, to start with 0.1 seconds of offset and 1000 ns of
transmission jitter, you can run like this:
@smallexample
./ppsi -d 0002 -C "sim_init_master_time .1; sim_jit_ns 1000"
@end smallexample
@c ##########################################################################
@node PTP Clock Class
@chapter PTP Clock Class
......
......@@ -184,6 +184,10 @@ struct pp_instance {
struct pp_instance_cfg cfg;
};
struct pp_globals_cfg {
int cfg_items; /* Remember how many we parsed */
int cur_ppi_n; /* Remember which instance we are configuring */
};
/*
* Structure for the multi-port ppsi instance.
......@@ -208,7 +212,7 @@ struct pp_globals {
int nlinks;
int max_links;
int cfg_items; /* Remember how many we parsed */
struct pp_globals_cfg cfg;
void *arch_data; /* if arch needs it */
/* FIXME Here include all is common to many interfaces */
......
......@@ -280,7 +280,8 @@ union pp_cfg_arg {
/*
* Configuration: we are structure-based, and a typedef simplifies things
*/
typedef int (*cfg_handler)(int lineno, union pp_cfg_arg *arg);
typedef int (*cfg_handler)(int lineno, struct pp_globals *ppg,
union pp_cfg_arg *arg);
struct pp_argname {
char *name;
......
......@@ -14,97 +14,105 @@
#include <sys/types.h>
#include <sys/stat.h>
/* config lines are global or ppi-specific. Keep track of globals and ppi */
static struct pp_globals *current_ppg;
static struct pp_instance *current_ppi;
static inline struct pp_instance *CUR_PPI(struct pp_globals *ppg)
{
if (ppg->cfg.cur_ppi_n < 0)
return NULL;
return INST(ppg, ppg->cfg.cur_ppi_n);
}
/* A "port" (or "link", for compatibility) line creates or uses a pp instance */
static int f_port(int lineno, union pp_cfg_arg *arg)
static int f_port(int lineno, struct pp_globals *ppg, union pp_cfg_arg *arg)
{
int i;
/* First look for an existing port with the same name */
for (i = 0; i < current_ppg->nlinks; i++) {
current_ppi = INST(current_ppg, i);
if (!strcmp(arg->s, current_ppi->cfg.port_name))
for ( i = 0; i < ppg->nlinks; i++) {
ppg->cfg.cur_ppi_n = i;
if (!strcmp(arg->s, CUR_PPI(ppg)->cfg.port_name))
return 0;
}
/* Allocate a new ppi */
if (current_ppg->nlinks >= current_ppg->max_links) {
/* check if there are still some free pp_instances to be used */
if (ppg->nlinks >= ppg->max_links) {
pp_printf("config line %i: out of available ports\n",
lineno);
/* we are out of available ports. set cur_ppi_n to -1 so if
* someone tries to set some parameter to this pp_instance it
* will cause an error, instead of overwrite the parameters of
* antother pp_instance */
ppg->cfg.cur_ppi_n = -1;
return -1;
}
current_ppi = INST(current_ppg, current_ppg->nlinks);
ppg->cfg.cur_ppi_n = ppg->nlinks;
/* FIXME: strncpy (it is missing in bare archs by now) */
strcpy(current_ppi->cfg.port_name, arg->s);
current_ppg->nlinks++;
strcpy(CUR_PPI(ppg)->cfg.port_name, arg->s);
ppg->nlinks++;
return 0;
}
#define CHECK_PPI(need) /* Quick hack to factorize errors later */ \
({if (need && !current_ppi) { \
({if (need && !CUR_PPI(ppg)) { \
pp_printf("config line %i: no port for this config\n", lineno);\
return -1; \
} \
if (!need && current_ppi) { \
if (!need && CUR_PPI(ppg)) { \
pp_printf("config line %i: global config under \"port\"\n", \
lineno); \
return -1; \
}})
static int f_if(int lineno, union pp_cfg_arg *arg)
static int f_if(int lineno, struct pp_globals *ppg, union pp_cfg_arg *arg)
{
CHECK_PPI(1);
strcpy(current_ppi->cfg.iface_name, arg->s);
strcpy(CUR_PPI(ppg)->cfg.iface_name, arg->s);
return 0;
}
/* The following ones are so similar. Bah... set a pointer somewhere? */
static int f_proto(int lineno, union pp_cfg_arg *arg)
static int f_proto(int lineno, struct pp_globals *ppg, union pp_cfg_arg *arg)
{
CHECK_PPI(1);
current_ppi->cfg.proto = arg->i;
CUR_PPI(ppg)->cfg.proto = arg->i;
return 0;
}
static int f_role(int lineno, union pp_cfg_arg *arg)
static int f_role(int lineno, struct pp_globals *ppg, union pp_cfg_arg *arg)
{
CHECK_PPI(1);
current_ppi->cfg.role = arg->i;
CUR_PPI(ppg)->cfg.role = arg->i;
return 0;
}
static int f_ext(int lineno, union pp_cfg_arg *arg)
static int f_ext(int lineno, struct pp_globals *ppg, union pp_cfg_arg *arg)
{
CHECK_PPI(1);
current_ppi->cfg.ext = arg->i;
CUR_PPI(ppg)->cfg.ext = arg->i;
return 0;
}
/* The following two are identical as well. I really need a pointer... */
static int f_class(int lineno, union pp_cfg_arg *arg)
static int f_class(int lineno, struct pp_globals *ppg, union pp_cfg_arg *arg)
{
CHECK_PPI(0);
GOPTS(current_ppg)->clock_quality.clockClass = arg->i;
GOPTS(ppg)->clock_quality.clockClass = arg->i;
return 0;
}
static int f_accuracy(int lineno, union pp_cfg_arg *arg)
static int f_accuracy(int lineno, struct pp_globals *ppg, union pp_cfg_arg *arg)
{
CHECK_PPI(0);
GOPTS(current_ppg)->clock_quality.clockAccuracy = arg->i;
GOPTS(ppg)->clock_quality.clockAccuracy = arg->i;
return 0;
}
/* Diagnostics can be per-port or global */
static int f_diag(int lineno, union pp_cfg_arg *arg)
static int f_diag(int lineno, struct pp_globals *ppg, union pp_cfg_arg *arg)
{
unsigned long level = pp_diag_parse(arg->s);
if (current_ppi)
current_ppi->flags = level;
if (ppg->cfg.cur_ppi_n >= 0)
CUR_PPI(ppg)->flags = level;
else
pp_global_flags = level;
return 0;
......@@ -244,7 +252,6 @@ static int pp_config_line(struct pp_globals *ppg, char *line, int lineno)
struct pp_argname *n;
char *word;
current_ppg = ppg;
pp_diag(NULL, config, 2, "parsing line %i: \"%s\"\n", lineno, line);
word = first_word(line, &line);
/* now line points to the next word, with no leading blanks */
......@@ -252,8 +259,20 @@ static int pp_config_line(struct pp_globals *ppg, char *line, int lineno)
if (word[0] == '#')
return 0;
if (!*word) {
/* empty or blank-only */
current_ppi = NULL;
/* empty or blank-only
* FIXME: this sets cur_ppi_n to an unvalid value. this means
* that every blank line in config file unsets the current
* pp_instance being configured. For this reason after a blank
* line in config file the pp_instance to be configured needs to
* be stated anew. Probably this is a desired feature because
* this behavior was already present here with the previous
* implementation, based on pointers. Is this sane? This means
* configuration file is somehow indentation-dependent (because
* presence or absence of blank lines matters). This feature is
* not documented anywhere AFAIK. If it's a feature it should
* be. If its a bug it should be fixed.
*/
ppg->cfg.cur_ppi_n = -1;
return 0;
}
......@@ -322,7 +341,7 @@ static int pp_config_line(struct pp_globals *ppg, char *line, int lineno)
break;
}
if (l->f(lineno, &cfg_arg))
if (l->f(lineno, ppg, &cfg_arg))
return -1;
return 0;
......@@ -348,7 +367,9 @@ static int pp_parse_conf(struct pp_globals *ppg, char *conf, int len)
if (term == '\n')
lineno++;
} while (term); /* if terminator was already 0, we are done */
pp_config_line(ppg, "", 0); /* done: clear current_ppi */
/* clear current ppi, don't store current ppi across different
* configuration files or strings */
ppg->cfg.cur_ppi_n = -1;
return errcount ? -1 : 0;
}
......@@ -385,10 +406,10 @@ int pp_config_file(struct pp_globals *ppg, int force, char *fname)
conf_fd = pp_open_conf_file(fname);
if (conf_fd < 0) {
if (force)
ppg->cfg_items++;
ppg->cfg.cfg_items++;
return -1;
}
ppg->cfg_items++;
ppg->cfg.cfg_items++;
/* read the whole file, it is split up later on */
......
# This Makefile in included by architectures that select time-sim
# as a default, or by builds with explicit TIME=sim.
# Object files are added straight, as they are always needed
OBJ-y += time-sim/sim-time.o time-sim/sim-socket.o
/*
* Copyright (C) 2013 CERN (www.cern.ch)
* Author: Pietro Fezzardi (pietrofezzardi@gmail.com)
*
* Released according to the GNU LGPL, version 2.1 or any later version.
*/
/* Socket interface for GNU/Linux (and most likely other posix systems) */
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <ppsi/ppsi.h>
#include "ptpdump.h"
#include "../arch-sim/ppsi-sim.h"
#define PP_MASTER_GEN_PORT (10000 + PP_GEN_PORT)
#define PP_MASTER_EVT_PORT (10000 + PP_EVT_PORT)
#define PP_SLAVE_GEN_PORT (20000 + PP_GEN_PORT)
#define PP_SLAVE_EVT_PORT (20000 + PP_EVT_PORT)
/* Returns 1 if p1 has higher priority than p2 */
static int compare_pending(struct sim_pending_pkt *p1,
struct sim_pending_pkt *p2)
{
/* expires earlier ---> higher priority */
if (p1->delay_ns < p2->delay_ns)
return 1;
/* same expire time ---> higher priority to the slave because it has to
* handle Sync and Follow_Up in a row. If both are for the slave handle
* PP_NP_EVT first */
if (p1->delay_ns == p2->delay_ns) {
if (p1->which_ppi > p2->which_ppi)
return 1;
if (p1->which_ppi == p2->which_ppi)
return (p1->chtype > p2->chtype);
}
return 0;
}
static int insert_pending(struct sim_ppg_arch_data *data,
struct sim_pending_pkt *new)
{
struct sim_pending_pkt *pkt, tmp;
int i = data->n_pending;
data->pending[i] = *new;
pkt = &data->pending[i - 1];
while (compare_pending(new, pkt) && (i > 0)) {
tmp = *pkt;
*pkt = *new;
*(pkt + 1) = tmp;
pkt--;
i--;
}
data->n_pending++;
return 0;
}
static int pending_received(struct sim_ppg_arch_data *data)
{
int i;
if (data->n_pending == 0)
return 0;
for (i = 0; i < data->n_pending; i++)
data->pending[i] = data->pending[i+1];
data->n_pending--;
return 0;
}
static int sim_recv_msg(struct pp_instance *ppi, int fd, void *pkt, int len,
TimeInternal *t)
{
ssize_t ret;
struct msghdr msg;
struct iovec vec[1];
union {
struct cmsghdr cm;
char control[512];
} cmsg_un;
vec[0].iov_base = pkt;
vec[0].iov_len = PP_MAX_FRAME_LENGTH;
memset(&msg, 0, sizeof(msg));
memset(&cmsg_un, 0, sizeof(cmsg_un));
/* msg_name, msg_namelen == 0: not used */
msg.msg_iov = vec;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_un.control;
msg.msg_controllen = sizeof(cmsg_un.control);
ret = recvmsg(fd, &msg, 0);
if (ret <= 0) {
if (errno == EAGAIN || errno == EINTR)
return 0;
return ret;
}
if (msg.msg_flags & MSG_TRUNC) {
pp_error("%s: truncated message\n", __func__);
return 0;
}
/* get time stamp of packet */
if (msg.msg_flags & MSG_CTRUNC) {
pp_error("%s: truncated ancillary data\n", __func__);
return 0;
}
ppi->t_ops->get(ppi, t);
/* This is not really hw... */
pp_diag(ppi, time, 2, "recv stamp: %i.%09i (%s)\n",
(int)t->seconds, (int)t->nanoseconds, "user");
return ret;
}
static int sim_net_recv(struct pp_instance *ppi, void *pkt, int len,
TimeInternal *t)
{
struct sim_ppg_arch_data *data = SIM_PPG_ARCH(ppi->glbs);
struct pp_channel *ch;
int ret;
/*
* only UDP
* We can return one frame only. Look in the global structure to know if
* the pending packet is on PP_NP_GEN or PP_NP_EVT
*/
if (data->n_pending <= 0)
return 0;
ch = &(NP(ppi)->ch[data->pending->chtype]);
ret = -1;
if (ch->pkt_present > 0) {
ret = sim_recv_msg(ppi, ch->fd, pkt, len, t);
if (ret > 0)
ch->pkt_present--;
}
if (ret > 0 && pp_diag_allow(ppi, frames, 2))
dump_payloadpkt("recv: ", pkt, ret, t);
/* remove received packet from pending */
pending_received(SIM_PPG_ARCH(ppi->glbs));
return ret;
}
static int sim_net_send(struct pp_instance *ppi, void *pkt, int len,
TimeInternal *t, int chtype, int use_pdelay_addr)
{
struct sim_ppi_arch_data *data = SIM_PPI_ARCH(ppi);
struct sockaddr_in addr;
struct sim_pending_pkt pending;
int64_t jit_ns = 0;
int ret;
/* only UDP */
addr.sin_family = AF_INET;
if (pp_sim_is_slave(ppi))
addr.sin_port = htons(chtype == PP_NP_GEN ?
PP_MASTER_GEN_PORT :
PP_MASTER_EVT_PORT);
else
addr.sin_port = htons(chtype == PP_NP_GEN ?
PP_SLAVE_GEN_PORT :
PP_SLAVE_EVT_PORT);
addr.sin_addr.s_addr = NP(ppi)->mcast_addr;
if (t)
ppi->t_ops->get(ppi, t);
ret = sendto(NP(ppi)->ch[chtype].fd, pkt, len, 0,
(struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if (pp_diag_allow(ppi, frames, 2))
dump_payloadpkt("send: ", pkt, len, t);
/* store pending packets in global structure */
pending.chtype = chtype;
if (pp_sim_is_master(ppi))
pending.which_ppi = SIM_SLAVE;
else
pending.which_ppi = SIM_MASTER;
/* check if we are sending a FollowUp. In this case we have to add the
* previous jitter, that was added to the previous Sync.
* Sync and FollowUp are sent out during the same cycle of the state
* machine. The simulator is not designed to fast-forward the time from
* within a cycle of state machine, so Sync and FollowUp are sent out
* without any time interval in the middle. The simulator acts like if
* they were sent exactly at the same time. So we need a way to be sure
* that the FollowUp is received after the Sync. This can be done
* adding the jitter of the Sync to the one of the FollowUp. In this way
* the first will always arrive first to the destination and will not
* cause the FollowUp to be discarded by the slave state machine when it
* comes earlier then the Sync */
if (((*(Enumeration4 *) (pkt + 0)) & 0x0F) == PPM_FOLLOW_UP) {
jit_ns += data->n_delay.last_outgoing_jit_ns;
}
jit_ns += (rand() * data->n_delay.jit_ns) / RAND_MAX;
/* store the jitter, used from the next send if it is a FollowUp */
data->n_delay.last_outgoing_jit_ns = jit_ns;
pending.delay_ns = data->n_delay.t_prop_ns + jit_ns;
insert_pending(SIM_PPG_ARCH(ppi->glbs), &pending);
NP(data->other_ppi)->ch[chtype].pkt_present++;
return ret;
}
static int sim_net_exit(struct pp_instance *ppi)
{
int fd;
int i;
/* only UDP */
for (i = PP_NP_GEN; i <= PP_NP_EVT; i++) {
fd = NP(ppi)->ch[i].fd;
if (fd < 0)
continue;
close(fd);
NP(ppi)->ch[i].fd = -1;
}
return 0;
}
static int sim_open_ch(struct pp_instance *ppi, char *ifname, int chtype)
{
int sock = -1;
int temp;
struct sockaddr_in addr;
char *context;
/* only UDP */
context = "socket()";
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0)
goto err_out;
NP(ppi)->ch[chtype].fd = sock;
temp = 1; /* allow address reuse */
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &temp, sizeof(int)) < 0)
pp_printf("%s: ioctl(SO_REUSEADDR): %s\n", __func__,
strerror(errno));
/* bind sockets */
/* need INADDR_ANY to allow receipt of multi-cast and uni-cast
* messages */
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (pp_sim_is_master(ppi))
addr.sin_port = htons(chtype == PP_NP_GEN ?
PP_MASTER_GEN_PORT :
PP_MASTER_EVT_PORT);
else
addr.sin_port = htons(chtype == PP_NP_GEN ?
PP_SLAVE_GEN_PORT :
PP_SLAVE_EVT_PORT);
context = "bind()";
if (bind(sock, (struct sockaddr *)&addr,
sizeof(struct sockaddr_in)) < 0)
goto err_out;
NP(ppi)->ch[chtype].fd = sock;
/*
* Standard ppsi state machine is designed to drop packets coming from
* itself, based on the clockIdentity. This hack avoids this behaviour,
* changing the clockIdentity of the master.
*/
if (pp_sim_is_master(ppi))
memset(NP(ppi)->ch[chtype].addr, 111, 1);
return 0;
err_out:
pp_printf("%s: %s: %s\n", __func__, context, strerror(errno));
if (sock >= 0)
close(sock);
NP(ppi)->ch[chtype].fd = -1;
return -1;
}
static int sim_net_init(struct pp_instance *ppi)
{
int i;
if (NP(ppi)->ch[0].fd > 0)
sim_net_exit(ppi);
/* The buffer is inside ppi, but we need to set pointers and align */
pp_prepare_pointers(ppi);
/* only UDP, RAW is not supported */
pp_diag(ppi, frames, 1, "sim_net_init UDP\n");
for (i = PP_NP_GEN; i <= PP_NP_EVT; i++) {
if (sim_open_ch(ppi, ppi->iface_name, i))
return -1;
}
return 0;
}
struct pp_network_operations sim_net_ops = {
.init = sim_net_init,
.exit = sim_net_exit,
.recv = sim_net_recv,
.send = sim_net_send,
.check_packet = NULL,
};
/*
* Copyright (C) 2013 CERN (www.cern.ch)
* Author: Pietro Fezzardi (pietrofezzardi@gmail.com)
*
* Released to the public domain
*/
/*
* Time operations for the simulator, design to be used both for
* master and the slave instance.
*/
#include <time.h>
#include <errno.h>
#include <ppsi/ppsi.h>
#include "../arch-sim/ppsi-sim.h"
int sim_fast_forward_ns(struct pp_globals *ppg, int64_t ff_ns)
{
struct pp_sim_time_instance *t_inst;
int i;
int64_t tmp;
for (i = 0; i < ppg->nlinks; i++) {
t_inst = &SIM_PPI_ARCH(INST(ppg, i))->time;
tmp = ff_ns + t_inst->freq_ppm_real * ff_ns / 1000 / 1000 / 1000;
t_inst->current_ns += tmp + (t_inst->freq_ppm_servo) *
tmp / 1000 / 1000 / 1000;
}
pp_diag(0, time, 1, "%s: %li ns\n", __func__, (long)ff_ns);
struct sim_pending_pkt *pkt;
struct sim_ppg_arch_data *data = SIM_PPG_ARCH(ppg);
if (data->n_pending) {
for (i = 0; i < data->n_pending; i++) {
pkt = &data->pending[i];
pkt->delay_ns -= ff_ns;
if (pkt->delay_ns < 0) {
pp_error("pkt->delay_ns = %lli\n",
(long long)pkt->delay_ns);
exit(1);
}
}
}
data->duration_ns -= ff_ns;
return 0;
}
static int sim_time_get(struct pp_instance *ppi, TimeInternal *t)
{
t->nanoseconds = SIM_PPI_ARCH(ppi)->time.current_ns %
(long long)PP_NSEC_PER_SEC;
t->seconds = (SIM_PPI_ARCH(ppi)->time.current_ns - t->nanoseconds) /
(long long)PP_NSEC_PER_SEC;
t->correct = 1;
if (!(pp_global_flags & PP_FLAG_NOTIMELOG))
pp_diag(ppi, time, 2, "%s: %9li.%09li\n", __func__,
(long)t->seconds, (long)t->nanoseconds);
return 0;
}
static int sim_time_set(struct pp_instance *ppi, TimeInternal *t)
{
if (!t) {
/* Change the network notion of utc/tai offset */
return 0;
}
SIM_PPI_ARCH(ppi)->time.current_ns = t->nanoseconds
+ t->seconds * (long long)PP_NSEC_PER_SEC;
pp_diag(ppi, time, 1, "%s: %9i.%09i\n", __func__,
t->seconds, t->nanoseconds);
return 0;
}
static int sim_time_adjust(struct pp_instance *ppi, long offset_ns,
long freq_ppm)
{
if (freq_ppm) {
if (freq_ppm > PP_ADJ_FREQ_MAX)
freq_ppm = PP_ADJ_FREQ_MAX;
if (freq_ppm < -PP_ADJ_FREQ_MAX)
freq_ppm = -PP_ADJ_FREQ_MAX;
SIM_PPI_ARCH(ppi)->time.freq_ppm_servo = freq_ppm;
}
if (offset_ns)
SIM_PPI_ARCH(ppi)->time.current_ns += offset_ns;
pp_diag(ppi, time, 1, "%s: %li %li\n", __func__, offset_ns, freq_ppm);
return 0;
}
static int sim_adjust_freq(struct pp_instance *ppi, long freq_ppm)
{
return sim_time_adjust(ppi, 0, freq_ppm);
}
static int sim_adjust_offset(struct pp_instance *ppi, long offset_ns)
{
return sim_time_adjust(ppi, offset_ns, 0);
}
static inline int sim_init_servo(struct pp_instance *ppi)
{
return SIM_PPI_ARCH(ppi)->time.freq_ppm_real;
}
static unsigned long sim_calc_timeout(struct pp_instance *ppi, int millisec)
{
unsigned long res;
res = millisec + SIM_PPI_ARCH(ppi)->time.current_ns / 1000LL / 1000LL;
return res ? res : 1;
}
struct pp_time_operations sim_time_ops = {
.get = sim_time_get,
.set = sim_time_set,
.adjust = sim_time_adjust,
.adjust_offset = sim_adjust_offset,
.adjust_freq = sim_adjust_freq,
.init_servo = sim_init_servo,
.calc_timeout = sim_calc_timeout,
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment