Skip to content
Snippets Groups Projects
Commit 1390000b authored by Martin Killenberg's avatar Martin Killenberg
Browse files

removed old, outdated cosade example

parent 7968590f
No related branches found
No related tags found
No related merge requests found
#ifndef _INDEPENDENT_CONTOL_CORE_H_
#define _INDEPENDENT_CONTOL_CORE_H_
#include <boost/scoped_ptr.hpp>
#include <ChimeraTK/ControlSystemAdapter/DevicePVManager.h>
#include <ChimeraTK/ControlSystemAdapter/ProcessArray.h>
#include <ChimeraTK/ControlSystemAdapter/DeviceSynchronizationUtility.h>
#include <ChimeraTK/ControlSystemAdapter/SynchronizationDirection.h>
/** Some dummy "hardware". You can read/write a voltage (int). */
class Hardware{
int _voltage;
public:
void setVoltage(int v){_voltage=v;}
int getVoltage() const {return _voltage;}
Hardware(): _voltage(42){}
};
/** This is just a simple example class.
*
* All functions are definded inline for the sake of the example.
* It is strongly recommended to use proper header/object separation for
* real code!
*/
class IndependentControlCore{
private:
ChimeraTK::DevicePVManager::SharedPtr _processVariableManager;
/** The target voltage to be transmitted to the hardware */
ChimeraTK::ProcessArray<int>::SharedPtr _targetVoltage;
/** The monitor voltage which is read back from the hardware */
ChimeraTK::ProcessArray<int>::SharedPtr _monitorVoltage;
Hardware _hardware; ///< Some hardware
boost::scoped_ptr< boost::thread > _deviceThread;
void mainLoop();
public:
/** The constructor gets an instance of the variable factory to use.
* The variables in the factory should already be initialised because the hardware is initialised here.
*/
IndependentControlCore(boost::shared_ptr<ChimeraTK::DevicePVManager> const & processVariableManager)
//initialise all process variables, using the factory
: _processVariableManager( processVariableManager ),
_targetVoltage( processVariableManager->createProcessArray<int>(ChimeraTK::controlSystemToDevice,"TARGET_VOLTAGE", 1) ),
_monitorVoltage( processVariableManager->createProcessArray<int>(ChimeraTK::deviceToControlSystem,"MONITOR_VOLTAGE", 1) ){
// initialise the hardware here
_targetVoltage->accessData(0) = 0;
_monitorVoltage->accessData(0) = 0;
_hardware.setVoltage( _targetVoltage->accessData(0) );
// start the device thread, which is executing the main loop
_deviceThread.reset( new boost::thread( boost::bind( &IndependentControlCore::mainLoop, this ) ) );
}
~IndependentControlCore(){
// stop the device thread before any other destructors are called
_deviceThread->interrupt();
_deviceThread->join();
}
};
inline void IndependentControlCore::mainLoop(){
ChimeraTK::DeviceSynchronizationUtility syncUtil(_processVariableManager);
while (!boost::this_thread::interruption_requested()) {
syncUtil.receiveAll();
_monitorVoltage->accessData(0) = _hardware.getVoltage();
_hardware.setVoltage( _targetVoltage->accessData(0) );
syncUtil.sendAll();
boost::this_thread::sleep_for( boost::chrono::milliseconds(100) );
}
}
#endif // _INDEPENDENT_CONTOL_CORE_H_
#define the dependeny locations here
DOOCSROOT = $(HOME)/doocs.git/doocs
# to define DOOCSROOT as an absolute path
include $(DOOCSROOT)/$(DOOCSARCH)/DEFINEDOOCSROOT
# to define the arch dependend things
include $(DOOCSROOT)/$(DOOCSARCH)/CONFIG
#Check that the ControlSystemAdapter-config script is in your path
CPPFLAGS += $(shell ControlSystemAdapter-config --cppflags)
LDFLAGS += $(shell ControlSystemAdapter-config --ldflags)
#DOOCS_Adapter_SRC_DIR = ../..
OBJDIR = $(DOOCSROOT)/$(DOOCSARCH)/obj/server/test/cosade
SRCDIR = $(PWD)
ADAPTER_OBJDIR = $(DOOCSROOT)/$(DOOCSARCH)/obj/library/common/DoocsAdapter
SOURCEOBJ = $(OBJDIR)/cosade_server.o
SOURCEHFILES =
ALLPROGS = $(OBJDIR)/cosade_server
include ../CPP_DEBUG_FLAGS.CONFIG
CPPFLAGS += -Wall -Wextra -Wshadow -pedantic -Wuninitialized $(CPP_DEBUG_FLAGS)
CPPFLAGS += -I../include -I../example -Iinclude -isystem/local/lib/include
LDFLAGS += -lboost_thread -lboost_system
#link the adapter with runpath, so it is found at execution time
LDFLAGS += -L$(ADAPTER_OBJDIR) -lDoocsAdapter -Wl,-rpath=$(ADAPTER_OBJDIR),--enable-new-dtags
all: $(ALLPROGS)
$(OBJDIR)/.depend depend:
@if [ ! -f $(OBJDIR) ] ; then \
echo ---------- create dir $(OBJDIR) --------------; \
mkdir -p $(OBJDIR) ; \
fi
for i in $(SRCDIR)/*.cc ;do $(CCDEP) $$i ;done > $(OBJDIR)/.depend_temp
cat $(OBJDIR)/.depend_temp | sed -e "/:/s/^/\$$\(OBJDIR\)\//g" > $(OBJDIR)/.depend
chmod g+w $(OBJDIR)/.depend*
include $(OBJDIR)/.depend
$(OBJDIR)/cosade_server: $(SOURCEOBJ)
$(LINK.cc) \
-o $(OBJDIR)/cosade_server $(SOURCEOBJ) \
-lEqServer -lDOOCSapi \
$(LDFLAGS) $(LDLIBS)
@chmod g+w $(OBJDIR)/cosade_server
@echo "---------------- $(OBJDIR)/cosade_server done---------------"
static $(OBJDIR)/static_cosade_server: $(SOURCEOBJ)
$(LINK.cc.static) $(LDFLAGS) -o $(OBJDIR)/static_cosade_server $(SOURCEOBJ) \
-lEqServer -lDOOCSapi \
$(LDLIBS)
@chmod g+w $(OBJDIR)/static_cosade_server
@echo "----------------$(OBJDIR)/static_cosade_server done---------------"
clean:
rm -f $(SOURCEOBJ) $(OBJDIR)/*.o $(OBJDIR)/cosade_server $(OBJDIR)/.depend* *.gcda *.gcno
test: $(OBJDIR)/CTestTestfile.cmake
(cd $(OBJDIR); ctest)
$(OBJDIR)/CTestTestfile.cmake: CTestTestfile.cmake.in
cat $< | sed "{s|@__OBJDIR__@|$(OBJDIR)|}" > $@
Note: The cosade example was so outdated that it had to be removed.
FIXME: create an example how to use the adapter.
#include <DoocsAdapter.h>
// Include all the control applications you want in this server
#include "IndependentControlCore.h"
BEGIN_DOOCS_SERVER("Cosade server", 10)
// Create static instances for all applications cores. They must not have overlapping
// process variable names ("location/protery" must be unique).
static IndependentControlCore independentControlCore(doocsAdapter.getDevicePVManager());
END_DOOCS_SERVER()
# Conf file created at 20:11.16 1. Oct. 2015
# eq_fct_type's are defined in eq_fct_code.h
eq_conf:
oper_uid: -1
oper_gid: 422
xpert_uid: 0
xpert_gid: 0
ring_buffer: 10000
memory_buffer: 500
eq_fct_name: "NODENAME._SVR"
eq_fct_type: 1
{
SVR.NAME: "NODENAME._SVR"
STS: 0x1c
ERROR.STR: 0 0 0 1078761493 "ok"
SYS_MASK: 1
FCT_CODE: 1
X_POS: 0
Z_POS: 0
Z_POS.STRING: ""
DEVICE.INFO: 235672 0 0 0 "Device OK"
MESSAGE: ""
LAST_UPDATE: 1112798768 350 0 0
LAST_USR1: 0 0 0 0
STS.ERROR: 0
STS.NEWERROR: 1
STS.ERRORMASK: 1
STS.ONLINE: 1
DEVICE.ONLINE: 1
DEVICE.OFFLINE: 0
SVR.ALIAS: 0
SVR.ARCFLUSH: 0
SVR.ARCFLUSH_B: 0
SVR.UPDATE: 1443723076
SVR.RATE: 1 0 0 0
SVR.RESIZE: 200
SVR.FILE: "cosade_server.conf"
SVR.DESC: ""
SVR.PROGRAMMER: "killenberg"
SVR.XMLFILE: ""
SVR.ERRORLOG: "/doocs/nodename/server/cosade_server/cosade_server.log"
SVR.STORE.RATE: 10
SVR.STORE.AUTO: 4
SVR.HOST_NAME: "ferrari2-17"
SVR.PROCESSNAME: "cosade_server"
SVR.RPC_NUMBER: 610498009
SVR.STARTTIME: 1443722776
SVR.LIBINFO: "18.10.3-trusty1"
SVR.LIBDATE: "Aug 20 16:59"
SVR.WDADDR: ""
SVR.CONTR: 0x0
SVR.MUST_RUN: 0
SVR.STOP_SVR: 0
SVR.START_CMD: ""
SVR.RPC_CALL_TIME.COMMENT: "Time per Call"
SVR.RPC_CALL_TIME.EGU: 1 1 100000 0 "rate"
SVR.RPC_CALL_TIME.XEGU: 0 0 100 0 "ms"
SVR.UPDATE_TIME.COMMENT: "Time per Update"
SVR.UPDATE_TIME.EGU: 1 1 100000 0 "rate"
SVR.UPDATE_TIME.XEGU: 0 0 100 0 "ms"
SVR.USR1_TIME.COMMENT: "run time of SIGUSR1"
SVR.USR1_TIME.EGU: 1 1 1e+06 1427728880 "counts"
SVR.USR1_TIME.XEGU: 0 0 100 1427728880 "ms"
SVR.USR1_PERIOD.COMMENT: "time between SIGUSR1"
SVR.USR1_PERIOD.EGU: 1 1 1e+06 1427728880 "counts"
SVR.USR1_PERIOD.XEGU: 0 0 500 1427728880 "ms"
SVR.ERROR_COUNT: 0
SVR.DEVMAX: 0
SVR.TINERUN: 0
SVR.TINEVERS: "4.05.0005"
SVR.TINEPREF: ""
SVR.TINESUFF: ""
SVR.TINE_DBG: 0
SVR.TINE_LOG: 0
SVR.TINE_FEC: 0 0 0 0 ""
SVR.TINE_PORT: 0
SVR.TINE_MTU: 1472
SVR.TINE_CTSZ: 32
SVR.TINE_MCTTL: 16
SVR.TINE_BLIM: 1000
SVR.TINE_CDLY: 20
SVR.TINE_GROUP: 0
SVR.FACILITY: "TEST.DOOCS"
SVR.DEVICE: "COSADE"
SVR.BPN: 0
SVR.SPR: 0
}
eq_fct_name: "COSADELOCATION"
eq_fct_type: 10
{
NAME: "COSADELOCATION"
STS: 0xc
ERROR.STR: 0 0 0 1112798768 "ok"
SYS_MASK: 222
FCT_CODE: 10
X_POS: 0
Z_POS: 0
Z_POS.STRING: ""
DEVICE.INFO: 0 0 0 0 "HALLO"
MESSAGE: ""
LAST_UPDATE: 1443723076 34 0 0
LAST_USR1: 0 0 0 0
STS.ERROR: 0
STS.NEWERROR: 1
STS.ERRORMASK: 0
STS.ONLINE: 1
MONITOR_VOLTAGE: 0
TARGET_VOLTAGE: 0
}
#!/usr/bin/python2
# The doocs python tools are currently only available from a network drive,
# so we can as well hard-code the path
import sys
sys.path.append("/home/ttflinac/user/python-2.7/Debian/")
import doocs
import unittest
import time
class CosadeTest(unittest.TestCase):
def setUp(self):
self.__location__ = "TEST.DOOCS/LOCALHOST_610498009/COSADELOCATION/"
print("FIXME: starting the doocs server should happen here. Do it manually to run this test.")
def tearDown(self):
print("FIXME: stopping the doocs server should happen here")
# After some time then monitor voltage should reflect target voltage.
# As update is only called every second, this can take some time
# (up to a second plus safety factor = two seconds).
# To decrase the latency we check every 100 ms and sleep if the value
# is not there yet.
# Afterwards check that the property has arrived (might still be
def waitForValueOnProperty(self, property_name, expected_value):
# 20 tries of 0.1 second are 2 seconds
for i in range(20):
# if the read value is correct break the loop and return
if doocs.read(self.__location__ + property_name) == expected_value:
break
# Otherwise sleep and retry (after the last sleep there still
# is a read in the evaluation)
time.sleep(0.1)
self.assertEqual(doocs.read(self.__location__ + property_name),expected_value)
def testWriteAndReadBack(self):
# set the target voltage to 0
doocs.write(self.__location__ + "TARGET_VOLTAGE","0")
self.waitForValueOnProperty("MONITOR_VOLTAGE",0)
# now change it and recheck (it could have been 0 in the first place)
doocs.write(self.__location__ + "TARGET_VOLTAGE","42")
self.waitForValueOnProperty("MONITOR_VOLTAGE",42)
if __name__ == '__main__':
unittest.main()
<?xml version="1.0" encoding="ISO-8859-1"?>
<jdddPanel><DOOCSLayeredPane name="EditorPanel" bounds="138,5,600,700"><fillColor>210,210,210</fillColor>
<DOOCSDial name="TargetDial" bounds="210,100,80,20"><adr>TEST.DOOCS/LOCALHOST_610498009/COSADELOCATION/TARGET_VOLTAGE</adr><precision>2</precision></DOOCSDial>
<DOOCSLabel name="TargetLabel" bounds="90,100,100,20"><text>TargetVoltage</text></DOOCSLabel>
<DOOCSLabel name="MonitorLabel" bounds="90,140,100,20"><text>Monitor Voltage</text></DOOCSLabel>
<DOOCSValue name="MonitorValue" bounds="210,140,80,20"><adr>TEST.DOOCS/LOCALHOST_610498009/COSADELOCATION/MONITOR_VOLTAGE</adr><border>true</border></DOOCSValue>
</DOOCSLayeredPane>
<thumbnail>iVBORw0KGgoAAAANSUhEUgAAAD0AAABHCAYAAAC0209OAAAAyUlEQVR42u3TOwqDUBAF0Cza3QiCWNi4AD+NOwi4h+xA8sHAhCfYhKRJZzwXLu8x3YGZ0ziOcaSeUtJnmqbDFBoaGhoaGvqA6C3zfYn5tsT18Vzf9nxZ57tA930fbdtG0zQxDENUVRVd10VRFOs8zT6h35PQqdYbesfoLMu+djc3nef5esPptsuyjLquf7rpLdYbGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGvpv0EfMCznEm/HUVywmAAAAAElFTkSuQmCC</thumbnail>
</jdddPanel>
\ No newline at end of file
#define the dependeny locations here
DOOCSROOT = $(HOME)/doocs.git/doocs
# to define DOOCSROOT as an absolute path
include $(DOOCSROOT)/$(DOOCSARCH)/DEFINEDOOCSROOT
# to define the arch dependend things
include $(DOOCSROOT)/$(DOOCSARCH)/CONFIG
#Check that the ControlSystemAdapter-config script is in your path
CPPFLAGS += $(shell ControlSystemAdapter-config --cppflags)
LDFLAGS += $(shell ControlSystemAdapter-config --ldflags)
DOOCS_Adapter_SRC_DIR = ../..
OBJDIR = $(DOOCSROOT)/$(DOOCSARCH)/obj/library/common/DoocsAdapter
SRCDIR = $(PWD)
TEST_SOURCES = $(shell (cd src; ls *.cpp))
TEST_EXECUTABLES = $(basename $(TEST_SOURCES))
SOURCEOBJ = $(OBJDIR)/
SOURCEHFILES = $(SRCDIR)/eq_cosade.h $(SRCDIR)/IndependentControlCore.h
ALLPROGS = $(OBJDIR)/cosade_server
CPPFLAGS += -Wall -Wextra -Wshadow -pedantic -Wuninitialized
CPPFLAGS += -I $(ControlSystemAdapter_DIR)/include
CPPFLAGS += -I $(DOOCS_Adapter_DIR)/include
#LDFLAGS += -L /space/killenb/DOOCS_Adapter_trunk/DOOCS_Adapter/build/ -lDOOCSControlsSystemAdapter
echo:
echo TEST_SORCES: $(TEST_EXECUTABLES)
all: $(ALLPROGS)
$(OBJDIR)/.depend_tests depend:
@if [ ! -f $(OBJDIR) ] ; then \
echo ---------- create dir $(OBJDIR) --------------; \
mkdir -p $(OBJDIR) ; \
fi
for i in $(SRCDIR)/*.cpp ;do $(CCDEP) $$i ;done > $(OBJDIR)/.depend_tests
chmod g+w $(OBJDIR)/.depend*
include $(OBJDIR)/.depend_tests
$(OBJDIR)/cosade_server: $(SOURCEOBJ)
$(LINK.cc) $(LDFLAGS) \
-o $(OBJDIR)/cosade_server $(SOURCEOBJ) \
-lEqServer -lTTFapi \
$(LDLIBS)
@chmod g+w $(OBJDIR)/cosade_server
@echo "---------------- $(OBJDIR)/cosade_server done---------------"
static $(OBJDIR)/static_cosade_server: $(SOURCEOBJ)
$(LINK.cc.static) $(LDFLAGS) -o $(OBJDIR)/static_cosade_server $(SOURCEOBJ) \
-lEqServer -lTTFapi \
$(LDLIBS)
@chmod g+w $(OBJDIR)/static_cosade_server
@echo "----------------$(OBJDIR)/static_cosade_server done---------------"
clean:
rm -f $(SOURCEOBJ) $(OBJDIR)/*.o *.ps $(OBJDIR)/cosade_server $(OBJDIR)/.depend*
// test the IndependentControlCore without DOOCS
#define BOOST_TEST_MODULE IndependentControlCoreTest
// Only after defining the name include the unit test header.
#include <boost/test/included/unit_test.hpp>
#include <ChimeraTK/ControlSystemAdapter/ControlSystemSynchronizationUtility.h>
#include "IndependentControlCore.h"
#include "emptyServerFunctions.h"
using namespace boost::unit_test_framework;
using namespace ChimeraTK;
BOOST_AUTO_TEST_SUITE( IndependentControlCoreTestSuite )
BOOST_AUTO_TEST_CASE( independentControlCoreTest ){
std::pair<boost::shared_ptr<ControlSystemPVManager>,
boost::shared_ptr<DevicePVManager> > pvManagers = createPVManager();
boost::shared_ptr<ControlSystemPVManager> csManager = pvManagers.first;
boost::shared_ptr<DevicePVManager> devManager = pvManagers.second;
IndependentControlCore controlCore(devManager);
ControlSystemSynchronizationUtility syncUtil(csManager);
ProcessArray<int>::SharedPtr targetVoltage
= csManager->getProcessArray<int>("TARGET_VOLTAGE");
ProcessArray<int>::SharedPtr monitorVoltage
= csManager->getProcessArray<int>("MONITOR_VOLTAGE");
// start with -1 for both voltages
targetVoltage->accessData(0) = -1;
monitorVoltage->accessData(0) = -1;
// the other side has a timeout of 100 ms, so we wait 1 second to be safe,
// but we look every 10 ms (not too frequent, but only with 10 % of additional latency)
//syncUtil.waitForNotifications(1000000, 10000);
//Note: as waitForNotification does not work as expected, the functionality is
// done manually for just the one variable we are receiving.
for (size_t i=0; i< 100; ++i){
boost::this_thread::sleep_for( boost::chrono::milliseconds(10) );
if (monitorVoltage->readNonBlocking()){
break;
}
}
// the target voltage must not have changed
BOOST_CHECK( targetVoltage->accessData(0) == -1 );
// the monitor voltage has to be 0 now, like in the hardware
BOOST_CHECK( monitorVoltage->accessData(0) == 0 );
targetVoltage->accessData(0) = 42;
targetVoltage->write();
// Wait until we read something back and check that the expected value is there.
// This is not nevessarily the case on the first read, so we put a limit to 10.
// Note that the number of actual receive attempty is higher, because we
// read every 10 ms while the other side is sending every 100 ms.
for (size_t receiveCounter=0; receiveCounter< 10; /*empty*/){
boost::this_thread::sleep_for( boost::chrono::milliseconds(10) );
if (monitorVoltage->readNonBlocking()){
++receiveCounter;
if (monitorVoltage->accessData(0) == 42){
break;
}
}
}
BOOST_CHECK(monitorVoltage->accessData(0) == 42);
}
BOOST_AUTO_TEST_SUITE_END()
#define BOOST_TEST_MODULE IndependentControlCoreTest
#include <boost/test/included/unit_test.hpp>
using namespace boost::unit_test_framework;
#include "IndependentControlCore.h"
#include <ChimeraTK/ControlSystemAdapter/PVManager.h>
#include <ChimeraTK/ControlSystemAdapter/StubControlSystemPVFactory.h>
#include <ChimeraTK/ControlSystemAdapter/ControlSystemPVManager.h>
using namespace ChimeraTK;
#include <climits>
// A struct which sets up the IndependentControlCore with the StubProcessVariableAdapter
struct IndependentControlCoreStubFixture{
boost::shared_ptr<ControlSystemPVManager> controlSystemPVManager;
boost::shared_ptr<DevicePVManager> devicePVManager;
boost::shared_ptr<IndependentControlCore> controlCore;
boost::shared_ptr< ControlSystemProcessScalar<int> > targetVoltage;
boost::shared_ptr< ControlSystemProcessScalar<int> > monitorVoltage;
std::list<ControlSystemProcessVariable::SharedPtr> toDeviceProcessVariables;
std::list<ControlSystemProcessVariable::SharedPtr> fromDeviceProcessVariables;
std::list<ControlSystemProcessVariable::SharedPtr> emptyPVList;
IndependentControlCoreStubFixture(){
std::pair< boost::shared_ptr<ControlSystemPVManager>, boost::shared_ptr<DevicePVManager> > pvManagers =
createPVManager( ControlSystemPVFactory::SharedPtr(new StubControlSystemPVFactory) );
controlSystemPVManager = pvManagers.first;
devicePVManager = pvManagers.second;
IndependentControlCore::registerProcessVariables( devicePVManager );
targetVoltage = controlSystemPVManager->getProcessScalar<int>("TARGET_VOLTAGE");
toDeviceProcessVariables.push_back( targetVoltage );
monitorVoltage = controlSystemPVManager->getProcessScalar<int>("MONITOR_VOLTAGE");
fromDeviceProcessVariables.push_back( monitorVoltage );
std::list<ControlSystemProcessVariable::SharedPtr> initialisationListToDevice;
initialisationListToDevice.push_back( targetVoltage );
initialisationListToDevice.push_back( monitorVoltage );
targetVoltage->set(35);
monitorVoltage->set(35);
std::cout << "TEST: initial sync" << std::endl;
boost::thread initialisationThread( boost::bind( &ControlSystemPVManager::synchronize,
&(*controlSystemPVManager),
initialisationListToDevice, emptyPVList, 200 ) );
// do not time out until the intialisation thread has set the variables (max. approx 33 minutes)
while (true){
devicePVManager->processSynchronization(LONG_MAX);//LONG_MAX,200);
if( initialisationThread.try_join_for(boost::chrono::microseconds(20)) ){
break;
}
}
controlCore.reset( new IndependentControlCore( devicePVManager ) );
}
~IndependentControlCoreStubFixture(){
std::cout << "fixture destructor" << std::endl;
}
};
BOOST_FIXTURE_TEST_SUITE( IndependentControlCoreTestSuite, IndependentControlCoreStubFixture )
BOOST_AUTO_TEST_CASE( testInitialisation ){
BOOST_CHECK( *targetVoltage == 35 );
BOOST_CHECK( *monitorVoltage == 35 );
// boost::this_thread::sleep(boost::posix_time::seconds(2));
}
BOOST_AUTO_TEST_CASE( testMainLoop ){
targetVoltage->set(38);
std::cout << "TEST: to device sync" << std::endl;
controlSystemPVManager->synchronize( toDeviceProcessVariables, emptyPVList );
//boost::this_thread::sleep(boost::posix_time::seconds(2));
std::cout << "TEST: from device sync" << std::endl;
controlSystemPVManager->synchronize( emptyPVList, fromDeviceProcessVariables );
BOOST_CHECK( *monitorVoltage == 38 );
}
BOOST_AUTO_TEST_SUITE_END()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment