Commit e616f26c authored by Adam Wujek's avatar Adam Wujek

Merge master

Signed-off-by: 's avatarAdam Wujek <dev_public@wujek.eu>
parents 70765afa 7006f56b
Introduction
=============
Most of the source code has been written according to the LGPL v2.1 license,
however some restrictions described below need to be considered.
Please look the the ppsi documentation for more information about licensing.
LGPL v2.1 License
==================
For some specific architecture (arch-wrs), the ppsi can be compiled using only
LGPL source code (if some diagnostic functionalities are disabled) and therefore
be distribute as a LGPL library together with proprietary code.
GPL v2 License
===============
If ppsi is compiled embedded within the wrpc-sw, it wil therefore fall under the
GPL v2 license due to the following points:
printf
-----------
Both the full and the partial printf code is distributed according to
the GPL-2, as it comes from the Linux kernel. This means that any
code using our diagnostics fall under the GPL requirements; you may
compile and use the diagnostic code internally with your own
proprietary code but you can't distribute binaries with diagnostics
without the complete source code and associated rights. You may avoid
the GPL requirements by using different printf implementations; if so
we'd love to have them contributed back in the package.
arch-wrc (lm32)
----------------
The same issue about the GPL license applies to the @i{div64_32}
function. We need this implementation in our @i{wrpc} code base
because the default @i{libgcc} division is very big, and we are always
tight with our in-FPGA memory space.
......@@ -4,6 +4,9 @@ A := arch-$(ARCH)
CFLAGS += -Itools
# needed for --gc-sections option of ld
PPSI_O_LDFLAGS = --entry=main
OBJ-y += $A/unix-startup.o \
$A/main-loop.o \
$A/unix-io.o \
......@@ -14,7 +17,8 @@ OBJ-y += $A/unix-startup.o \
lib/dump-funcs.o \
lib/drop.o \
lib/assert.o \
lib/div64.o
lib/div64.o \
lib/time-arith.o
# The user can set TIME=, but we pick unix time by default
TIME ?= unix
......
/*
* Copyright (C) 2011 CERN (www.cern.ch)
* Copyright (C) 2011-2022 CERN (www.cern.ch)
* Author: Alessandro Rubini
*
* Released to the public domain
......@@ -24,8 +24,26 @@ static int run_all_state_machines(struct pp_globals *ppg)
int j;
int delay_ms = 0, delay_ms_j;
/* TODO: check if in GM mode and initialized */
for (j = 0; j < ppg->nlinks; j++) {
struct pp_instance *ppi = INST(ppg, j);
int old_lu = ppi->link_up;
/* TODO: add the proper discovery of link_up */
ppi->link_up = 1;
if (old_lu != ppi->link_up) {
pp_diag(ppi, fsm, 1, "iface %s went %s\n",
ppi->iface_name, ppi->link_up ? "up" : "down");
if (ppi->link_up) {
ppi->state = PPS_INITIALIZING;
/* TODO: Get calibration values here */
}
}
delay_ms_j = pp_state_machine(ppi, NULL, 0);
/* delay_ms is the least delay_ms among all instances */
......@@ -35,6 +53,22 @@ static int run_all_state_machines(struct pp_globals *ppg)
delay_ms = delay_ms_j;
}
/* BMCA must run at least once per announce interval 9.2.6.8 */
if (pp_gtimeout(ppg, PP_TO_BMC)) {
/* Calculation of erbest, ebest, ... */
bmc_calculate_ebest(ppg);
pp_gtimeout_reset(ppg, PP_TO_BMC);
delay_ms = 0;
/* TODO: Check PLL state if needed/available */
} else {
/* check if the BMC timeout is the next to run */
int delay_bmca;
if ((delay_bmca = pp_gnext_delay_1(ppg, PP_TO_BMC)) < delay_ms)
delay_ms = delay_bmca;
}
return delay_ms;
}
......@@ -48,8 +82,6 @@ void unix_main_loop(struct pp_globals *ppg)
for (j = 0; j < ppg->nlinks; j++) {
ppi = INST(ppg, j);
/* just tell that the links are up */
ppi->link_up = TRUE;
/*
* The main loop here is based on select. While we are not
......@@ -62,32 +94,14 @@ void unix_main_loop(struct pp_globals *ppg)
delay_ms = run_all_state_machines(ppg);
while (1) {
int i;
/*
* 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;
}
int packet_available;
i = unix_net_ops.check_packet(ppg, delay_ms);
packet_available = unix_net_ops.check_packet(ppg, delay_ms);
if (i < 0)
if (packet_available < 0)
continue;
if (i == 0) {
if (packet_available == 0) {
delay_ms = run_all_state_machines(ppg);
continue;
}
......@@ -99,7 +113,7 @@ void unix_main_loop(struct pp_globals *ppg)
delay_ms = -1;
for (j = 0; j < ppg->nlinks; j++) {
int tmp_d;
int tmp_d, i;
ppi = INST(ppg, j);
if ((ppi->ch[PP_NP_GEN].pkt_present) ||
......
/*
* Copyright (C) 2011 CERN (www.cern.ch)
* Copyright (C) 2011-2022 CERN (www.cern.ch)
* Author: Alessandro Rubini
*
* Released to the public domain
......@@ -23,16 +23,36 @@
#include <ppsi/ppsi.h>
#include "ppsi-unix.h"
char *format_hex(char *s, const unsigned char *mac, int cnt);
char *format_hex8(char *s, const unsigned char *mac);
/* ppg and fields */
static struct pp_globals ppg_static;
static defaultDS_t defaultDS;
static currentDS_t currentDS;
static parentDS_t parentDS;
static timePropertiesDS_t timePropertiesDS;
static struct pp_servo servo;
extern struct pp_ext_hooks pp_hooks;
/**
* Enable/disable asymmetry correction
*/
static void enable_asymmetryCorrection(struct pp_instance *ppi, Boolean enable ) {
if ((ppi->asymmetryCorrectionPortDS.enable = enable) == TRUE ) {
/* Enabled: The delay asymmetry will be calculated */
ppi->asymmetryCorrectionPortDS.scaledDelayCoefficient =
(ppi->cfg.scaledDelayCoefficient != 0) ?
ppi->cfg.scaledDelayCoefficient :
(RelativeDifference)(ppi->cfg.delayCoefficient * REL_DIFF_TWO_POW_FRACBITS);
ppi->portDS->delayAsymCoeff =
pp_servo_calculateDelayAsymCoefficient(ppi->asymmetryCorrectionPortDS.scaledDelayCoefficient);
}
ppi->asymmetryCorrectionPortDS.constantAsymmetry =
picos_to_interval(ppi->cfg.constantAsymmetry_ps);
}
int main(int argc, char **argv)
{
struct pp_globals *ppg;
......@@ -45,12 +65,20 @@ int main(int argc, char **argv)
pp_printf("PPSi. Commit %s, built on " __DATE__ "\n", PPSI_VERSION);
/* So far allow more than one instance of PPSi running on the same
* machine.
TODO: to be considered to allow only one instance of PPSi to run
* at the same time.
* Potential problems my be in:
* shmem (not used in arch-unix)
* race of setting of time if more than one instance run as slave
*/
ppg = &ppg_static;
ppg->defaultDS = &defaultDS;
ppg->currentDS = &currentDS;
ppg->parentDS = &parentDS;
ppg->timePropertiesDS = &timePropertiesDS;
ppg->servo = &servo;
ppg->rt_opts = &__pp_default_rt_opts;
/* We are hosted, so we can allocate */
......@@ -63,17 +91,17 @@ int main(int argc, char **argv)
exit(1);
}
/* Before the configuration is parsed, set defaults */
/* Set default configuration value for all instances */
for (i = 0; i < ppg->max_links; i++) {
ppi = INST(ppg, i);
ppi->proto = PP_DEFAULT_PROTO;
ppi->role = PP_DEFAULT_ROLE;
ppi->delayMechanism = MECH_E2E;
memcpy(&INST(ppg, i)->cfg, &__pp_default_instance_cfg,
sizeof(__pp_default_instance_cfg));
}
/* Set offset here, so config parsing can override it */
if (adjtimex(&t) >= 0)
timePropertiesDS.currentUtcOffset = t.tai;
memset(&t, 0, sizeof(t));
if (adjtimex(&t) >= 0) {
ppg->timePropertiesDS->currentUtcOffset = (Integer16)t.tai;
}
if (pp_parse_cmdline(ppg, argc, argv) != 0)
return -1;
......@@ -81,6 +109,8 @@ int main(int argc, char **argv)
/* If no item has been parsed, provide a default file or string */
if (ppg->cfg.cfg_items == 0)
pp_config_file(ppg, 0, PP_DEFAULT_CONFIGFILE);
/* No config found, add default */
if (ppg->cfg.cfg_items == 0)
pp_config_string(ppg, strdup("link 0; iface eth0; proto udp"));
......@@ -95,21 +125,113 @@ int main(int argc, char **argv)
ppi->iface_name = ppi->cfg.iface_name;
ppi->port_name = ppi->cfg.port_name;
ppi->delayMechanism = ppi->cfg.delayMechanism;
ppi->ext_hooks= &pp_hooks;
ppi->portDS = calloc(1, sizeof(*ppi->portDS));
ppi->servo = calloc(1, sizeof(*ppi->servo));
ppi->ext_hooks = &pp_hooks;
ppi->ptp_support = TRUE;
if (ppi->portDS) {
switch (ppi->cfg.profile) {
case PPSI_PROFILE_WR:
#if CONFIG_HAS_PROFILE_WR
ppi->protocol_extension = PPSI_EXT_WR;
/* Add WR extension portDS */
if ( !(ppi->portDS->ext_dsport =
wrs_shm_alloc(ppsi_head,
sizeof(struct wr_dsport))
)
) {
goto exit_out_of_memory;
}
/* Allocate WR data extension */
if (! (ppi->ext_data =
wrs_shm_alloc(ppsi_head,
sizeof(struct wr_data))
)
) {
goto exit_out_of_memory;
}
/* Set WR extension hooks */
ppi->ext_hooks = &wr_ext_hooks;
enable_asymmetryCorrection(ppi, TRUE);
#else
fprintf(stderr, "ppsi: Profile WR not supported");
exit(1);
#endif
break;
case PPSI_PROFILE_HA:
#if CONFIG_HAS_PROFILE_HA
if (!enable_l1Sync(ppi, TRUE))
goto exit_out_of_memory;
/* Force mandatory attributes - Do not take care of the configuration */
L1E_DSPOR_BS(ppi)->rxCoherentIsRequired = TRUE;
L1E_DSPOR_BS(ppi)->txCoherentIsRequired = TRUE;
L1E_DSPOR_BS(ppi)->congruentIsRequired = TRUE;
L1E_DSPOR_BS(ppi)->L1SyncEnabled = TRUE;
L1E_DSPOR_BS(ppi)->optParamsEnabled = FALSE;
enable_asymmetryCorrection(ppi, TRUE);
#else
fprintf(stderr, "ppsi: Profile HA not supported");
exit(1);
#endif
break;
case PPSI_PROFILE_PTP :
/* Do not take care of L1SYNC */
enable_asymmetryCorrection(ppi,
ppi->cfg.asymmetryCorrectionEnable);
ppi->protocol_extension = PPSI_EXT_NONE;
break;
case PPSI_PROFILE_CUSTOM :
#if CONFIG_HAS_PROFILE_CUSTOM
ppi->protocol_extension = PPSI_EXT_NONE; /* can be changed ...*/
#if CONFIG_HAS_EXT_L1SYNC
if (ppi->cfg.l1SyncEnabled) {
if (!enable_l1Sync(ppi, TRUE))
goto exit_out_of_memory;
/* Read L1SYNC parameters */
L1E_DSPOR_BS(ppi)->rxCoherentIsRequired = ppi->cfg.l1SyncRxCoherencyIsRequired;
L1E_DSPOR_BS(ppi)->txCoherentIsRequired = ppi->cfg.l1SyncTxCoherencyIsRequired;
L1E_DSPOR_BS(ppi)->congruentIsRequired = ppi->cfg.l1SyncCongruencyIsRequired;
L1E_DSPOR_BS(ppi)->optParamsEnabled = ppi->cfg.l1SyncOptParamsEnabled;
if (L1E_DSPOR_BS(ppi)->optParamsEnabled) {
L1E_DSPOR_OP(ppi)->timestampsCorrectedTx = ppi->cfg.l1SyncOptParamsTimestampsCorrectedTx;
}
}
enable_asymmetryCorrection(ppi, ppi->cfg.asymmetryCorrectionEnable);
#endif
#else
fprintf(stderr, "ppsi: Profile CUSTOM not supported");
exit(1);
#endif
break;
}
/* Parameters profile independent */
ppi->timestampCorrectionPortDS.egressLatency = picos_to_interval(ppi->cfg.egressLatency_ps);
ppi->timestampCorrectionPortDS.ingressLatency = picos_to_interval(ppi->cfg.ingressLatency_ps);
ppi->timestampCorrectionPortDS.messageTimestampPointLatency = 0;
ppi->portDS->masterOnly = ppi->cfg.masterOnly; /* can be overridden in pp_init_globals() */
} else {
goto exit_out_of_memory;
}
/* The following default names depend on TIME= at build time */
ppi->n_ops = &DEFAULT_NET_OPS;
ppi->t_ops = &DEFAULT_TIME_OPS;
ppi->portDS = calloc(1, sizeof(*ppi->portDS));
ppi->__tx_buffer = malloc(PP_MAX_FRAME_LENGTH);
ppi->__rx_buffer = malloc(PP_MAX_FRAME_LENGTH);
if (!ppi->portDS || !ppi->__tx_buffer || !ppi->__rx_buffer) {
fprintf(stderr, "ppsi: out of memory\n");
exit(1);
goto exit_out_of_memory;
}
}
pp_init_globals(ppg, &__pp_default_rt_opts);
seed = time(NULL);
......@@ -119,4 +241,32 @@ int main(int argc, char **argv)
unix_main_loop(ppg);
return 0; /* never reached */
exit_out_of_memory:
fprintf(stderr, "ppsi: out of memory\n");
exit(1);
}
char *format_hex(char *s, const unsigned char *mac, int cnt)
{
int i;
*s = '\0';
for (i = 0; i < cnt; i++) {
pp_sprintf(s, "%s%02x:", s, mac[i]);
}
/* remove last colon */
s[cnt * 3 - 1] = '\0'; /* cnt * strlen("FF:") - 1 */
return s;
}
char *format_hex8(char *s, const unsigned char *mac)
{
return format_hex(s, mac, 8);
}
char *format_mac(char *s, const unsigned char *mac)
{
format_hex(s, mac, 6);
return s;
}
This diff is collapsed.
......@@ -5,10 +5,10 @@
* Author: Alessandro Rubini <rubini@gnudd.com>
* Based on ideas by Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*/
#include <stdio.h>
#include <stdlib.h>
......
......@@ -5,10 +5,10 @@
* Author: Alessandro Rubini <rubini@gnudd.com>
* Based on ideas by Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*/
#include <stdio.h>
#include <stdlib.h>
......
......@@ -5,10 +5,10 @@
* Author: Alessandro Rubini <rubini@gnudd.com>
* Based on ideas by Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*/
#ifndef __MINIPC_INT_H__
#define __MINIPC_INT_H__
......
......@@ -7,6 +7,12 @@
* This replicates some code of minipc-core and minipc-server.
* It implements the functions needed to make a freestanding server
* (for example, an lm32 running on an FPGA -- the case I actually need).
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
*/
#include "minipc-int.h"
......
......@@ -5,10 +5,10 @@
* Author: Alessandro Rubini <rubini@gnudd.com>
* Based on ideas and code by Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*/
#include <stdio.h>
#include <stdlib.h>
......
......@@ -5,10 +5,10 @@
* Author: Alessandro Rubini <rubini@gnudd.com>
* Based on ideas by Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*/
#ifndef __MINIPC_H__
#define __MINIPC_H__
......
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <ppsi/ppsi.h>
#include <libwr/util.h>
#include <arpa/inet.h> /* for ntohl */
......
This diff is collapsed.
/*
* Macros for diagnostic prints.
*
* Copyright (C) 2011 CERN (www.cern.ch)
* Licenses: (LGPL-2.1 & PPSI-LGPL-Exception-1.1 | LGPL-3.0)
*/
......
......@@ -310,6 +310,6 @@ char *relative_interval_to_string(RelativeDifference time)
sub_yocto += bitWeight;
bitWeight /= 2;
}
pp_sprintf(time_as_string,"%c%"PRId32".%018Ld", sign, nsecs, sub_yocto);
pp_sprintf(time_as_string,"%c%"PRId32".%018"PRId64, sign, nsecs, sub_yocto);
return time_as_string;
}
......@@ -335,8 +335,6 @@ static int wr_extension_state_changed( struct pp_instance * ppi) {
static int wr_new_slave (struct pp_instance *ppi, void *buf, int len) {
if ( ppi->extState==PP_EXSTATE_ACTIVE ) {
struct wr_dsport *wrp = WR_DSPOR(ppi);
wr_servo_init(ppi);
/* To avoid comparison of sequenceId with parentAnnSequenceId
......
......@@ -147,6 +147,29 @@ static unsigned long bare_calc_timeout(struct pp_instance *ppi, int millisec)
return now_ms + millisec;
}
static int bare_get_GM_lock_state(struct pp_globals *ppg,
pp_timing_mode_state_t *state)
{
*state = PP_TIMING_MODE_STATE_LOCKED;
return 0;
}
static int bare_enable_timing_output(struct pp_globals *ppg, int enable)
{
static int prev_enable = 0;
if (prev_enable != enable) {
pp_diag(NULL, time, 2, "%s dummy timing output\n",
enable ? "enable" : "disable");
prev_enable = enable;
return 0;
}
return 0;
}
struct pp_time_operations bare_time_ops = {
.get_utc_time = bare_time_get_utc_time,
.get_utc_offset = bare_time_get_utc_offset,
......@@ -158,4 +181,6 @@ struct pp_time_operations bare_time_ops = {
.adjust_offset = bare_time_adjust_offset,
.adjust_freq = bare_time_adjust_freq,
.calc_timeout = bare_calc_timeout,
.get_GM_lock_state = bares_get_GM_lock_state,
.enable_timing_output = bare_enable_timing_output
};
......@@ -142,6 +142,29 @@ static unsigned long sim_calc_timeout(struct pp_instance *ppi, int millisec)
return millisec + SIM_PPI_ARCH(ppi)->time.current_ns / 1000LL / 1000LL;
}
static int sim_get_GM_lock_state(struct pp_globals *ppg,
pp_timing_mode_state_t *state)
{
*state = PP_TIMING_MODE_STATE_LOCKED;
return 0;
}
static int sim_enable_timing_output(struct pp_globals *ppg, int enable)
{
static int prev_enable = 0;
if (prev_enable != enable) {
pp_diag(NULL, time, 2, "%s dummy timing output\n",
enable ? "enable" : "disable");
prev_enable = enable;
return 0;
}
return 0;
}
struct pp_time_operations sim_time_ops = {
.get_utc_time = sim_time_get_utc_time,
.get_utc_offset = sim_time_get_utc_offset,
......@@ -154,4 +177,6 @@ struct pp_time_operations sim_time_ops = {
.adjust_freq = sim_adjust_freq,
.init_servo = sim_init_servo,
.calc_timeout = sim_calc_timeout,
.get_GM_lock_state = sim_get_GM_lock_state,
.enable_timing_output = sim_enable_timing_output
};
......@@ -22,7 +22,7 @@
#include <ppsi/ppsi.h>
#include "ptpdump.h"
#include "../arch-unix/ppsi-unix.h"
#include "../arch-unix/include/ppsi-unix.h"
/* unix_recv_msg uses recvmsg for timestamp query */
static int unix_recv_msg(struct pp_instance *ppi, int fd, void *pkt, int len,
......
......@@ -11,6 +11,7 @@
#include <time.h>
#include <sys/timex.h>
#include <ppsi/ppsi.h>
#include "../arch-unix/include/ppsi-unix.h"
#ifndef MOD_TAI
#define MOD_TAI 0x80
......@@ -280,6 +281,29 @@ static unsigned long unix_calc_timeout(struct pp_instance *ppi, int millisec)
return now_ms + millisec;
}
static int unix_get_GM_lock_state(struct pp_globals *ppg,
pp_timing_mode_state_t *state)
{
*state = PP_TIMING_MODE_STATE_LOCKED;
return 0;
}
static int unix_enable_timing_output(struct pp_globals *ppg, int enable)
{
static int prev_enable = 0;
if (prev_enable != enable) {
pp_diag(NULL, time, 2, "%s dummy timing output\n",
enable ? "enable" : "disable");
prev_enable = enable;
return 0;
}
return 0;
}
struct pp_time_operations unix_time_ops = {
.get_utc_time = unix_time_get_utc_time,
.get_utc_offset = unix_time_get_utc_offset,
......@@ -291,5 +315,6 @@ struct pp_time_operations unix_time_ops = {
.adjust_freq = unix_time_adjust_freq,
.init_servo = unix_time_init_servo,
.calc_timeout = unix_calc_timeout,
.get_GM_lock_state = unix_get_GM_lock_state,
.enable_timing_output = unix_enable_timing_output
};
......@@ -169,7 +169,8 @@ static int wr_dump_tlv(char *prefix, struct ptp_tlv *tlv, int totallen)
/* the field includes 6 bytes of the header, ecludes 4 of them. Bah! */
int explen = ntohs(tlv->len) + 4;
if ( CONFIG_HAS_PROFILE_WR ) {
#ifdef CONFIG_PROFILE_WR
{
static char *wr_message_name[] = {
"SLAVE_PRESENT",
"LOCK",
......@@ -283,8 +284,11 @@ static int wr_dump_tlv(char *prefix, struct ptp_tlv *tlv, int totallen)
}
}
return explen;
} else
}
#else
return explen > totallen ? totallen : explen;
#endif
}
static int l1sync_dump_tlv(char *prefix, struct l1sync_tlv *tlv, int totallen)
......
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