Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
osie
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexedi
osie
Commits
136840d5
Commit
136840d5
authored
May 30, 2024
by
Léo-Paul Géneau
👾
Browse files
Options
Browse Files
Download
Plain Diff
coupler/makefile: use implicit rules instead of extra flags
See merge request
!53
parents
5ef2a5b2
5283c3fc
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
41 additions
and
51 deletions
+41
-51
coupler/Makefile
coupler/Makefile
+1
-4
coupler/cli.h
coupler/cli.h
+16
-17
coupler/common.h
coupler/common.h
+1
-1
coupler/keep_alive_publisher.h
coupler/keep_alive_publisher.h
+2
-4
coupler/keep_alive_subscriber.h
coupler/keep_alive_subscriber.h
+11
-11
coupler/mod_io_i2c.h
coupler/mod_io_i2c.h
+3
-3
coupler/server.c
coupler/server.c
+7
-11
No files found.
coupler/Makefile
View file @
136840d5
CC
=
gcc
CFLAGS
=
-I
$(OPEN62541_SOURCE_HOME)
-std
=
c99
-Wall
-Wextra
-Wpedantic
-Werror
-Wno-unused-parameter
$(C_COMPILER_EXTRA_FLAGS)
CFLAGS
=
-I
$(OPEN62541_SOURCE_HOME)
LDFLAGS
=
-L
$(OPEN62541_HOME)
/lib
LDFLAGS
=
-L
$(OPEN62541_HOME)
/lib
EXTRA_FLAGS
=
$(C_COMPILER_EXTRA_FLAGS)
OUT_DIR
=
bin
OUT_DIR
=
bin
server
:
server.c
server
:
server.c
$(CC)
-o
$@
$^
$(CFLAGS)
$(LDFLAGS)
-std
=
c99
$(EXTRA_FLAGS)
install
:
install
:
mkdir
-p
$(OUT_DIR)
mkdir
-p
$(OUT_DIR)
...
...
coupler/cli.h
View file @
136840d5
...
@@ -36,26 +36,26 @@ const char *argp_program_bug_address = "info@nexedi.com";
...
@@ -36,26 +36,26 @@ const char *argp_program_bug_address = "info@nexedi.com";
static
char
doc
[]
=
"rPLC coupler server which controls MOD-IO's relays' state over OPC-UA protocol."
;
static
char
doc
[]
=
"rPLC coupler server which controls MOD-IO's relays' state over OPC-UA protocol."
;
static
char
args_doc
[]
=
"..."
;
static
char
args_doc
[]
=
"..."
;
static
struct
argp_option
options
[]
=
{
static
struct
argp_option
options
[]
=
{
{
"port"
,
'p'
,
"4840"
,
0
,
"Port to bind to."
},
{
"port"
,
'p'
,
"4840"
,
0
,
"Port to bind to."
,
0
},
{
"server-ip-address"
,
'a'
,
""
,
0
,
"[not yet available] Server address to bind to."
},
{
"server-ip-address"
,
'a'
,
""
,
0
,
"[not yet available] Server address to bind to."
,
0
},
{
"device"
,
'd'
,
"/dev/i2c-1"
,
0
,
"Linux block device path."
},
{
"device"
,
'd'
,
"/dev/i2c-1"
,
0
,
"Linux block device path."
,
0
},
{
"slave-address-list"
,
's'
,
"0x58"
,
0
,
"Comma separated list of slave I2C addresses."
},
{
"slave-address-list"
,
's'
,
"0x58"
,
0
,
"Comma separated list of slave I2C addresses."
,
0
},
{
"mode"
,
'm'
,
"0"
,
0
,
"Set different modes of operation of coupler. Default (0) is set attached \
{
"mode"
,
'm'
,
"0"
,
0
,
"Set different modes of operation of coupler. Default (0) is set attached \
I2C's state state. Virtual (1) which does NOT set any I2C slaves' state."
},
I2C's state state. Virtual (1) which does NOT set any I2C slaves' state."
,
0
},
{
"username"
,
'u'
,
""
,
0
,
"Username."
},
{
"username"
,
'u'
,
""
,
0
,
"Username."
,
0
},
{
"password"
,
'w'
,
""
,
0
,
"Password."
},
{
"password"
,
'w'
,
""
,
0
,
"Password."
,
0
},
{
"key"
,
'k'
,
""
,
0
,
"x509 key."
},
{
"key"
,
'k'
,
""
,
0
,
"x509 key."
,
0
},
{
"certificate"
,
'c'
,
""
,
0
,
"X509 certificate."
},
{
"certificate"
,
'c'
,
""
,
0
,
"X509 certificate."
,
0
},
{
"id"
,
'i'
,
"0"
,
0
,
"ID of coupler."
},
{
"id"
,
'i'
,
"0"
,
0
,
"ID of coupler."
,
0
},
{
"heart-beat"
,
'b'
,
"0"
,
0
,
"Publish heart beat to other couplers."
},
{
"heart-beat"
,
'b'
,
"0"
,
0
,
"Publish heart beat to other couplers."
,
0
},
{
"heart-beat-interval"
,
't'
,
"50"
,
0
,
"Heart beat interval in ms."
},
{
"heart-beat-interval"
,
't'
,
"50"
,
0
,
"Heart beat interval in ms."
,
0
},
{
"heart-beat-timeout-interval"
,
{
"heart-beat-timeout-interval"
,
'o'
,
"100"
,
0
,
"Heart beat timeout interval in ms."
},
'o'
,
"100"
,
0
,
"Heart beat timeout interval in ms."
,
0
},
{
"heart-beat-id-list"
,
'l'
,
""
,
0
,
"Comma separated list of IDs of couplers to watch for heart beats. \
{
"heart-beat-id-list"
,
'l'
,
""
,
0
,
"Comma separated list of IDs of couplers to watch for heart beats. \
If a heart beat is missing coupler goes to safe mode."
},
If a heart beat is missing coupler goes to safe mode."
,
0
},
{
"network-address-url-data-type"
,
{
"network-address-url-data-type"
,
'n'
,
"opc.udp://224.0.0.22:4840/"
,
0
,
"Network address URL type used for Pub/Sub."
},
'n'
,
"opc.udp://224.0.0.22:4840/"
,
0
,
"Network address URL type used for Pub/Sub."
,
0
},
{
"network-interface"
,
'j'
,
""
,
0
,
"Network interface to use for Pub/Sub."
},
{
"network-interface"
,
'j'
,
""
,
0
,
"Network interface to use for Pub/Sub."
,
0
},
{
0
}
{
0
}
};
};
...
@@ -142,7 +142,6 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
...
@@ -142,7 +142,6 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
void
handleCLI
(
int
argc
,
char
**
argv
)
{
void
handleCLI
(
int
argc
,
char
**
argv
)
{
// parse CLI
// parse CLI
int
i
;
int
i
;
int
length
;
long
result
;
long
result
;
char
*
eptr
;
char
*
eptr
;
...
...
coupler/common.h
View file @
136840d5
...
@@ -84,7 +84,7 @@ char *randomString(size_t length) {
...
@@ -84,7 +84,7 @@ char *randomString(size_t length) {
if
(
length
)
{
if
(
length
)
{
randomString
=
malloc
(
sizeof
(
char
)
*
(
length
+
1
));
randomString
=
malloc
(
sizeof
(
char
)
*
(
length
+
1
));
if
(
randomString
)
{
if
(
randomString
)
{
for
(
in
t
n
=
0
;
n
<
length
;
n
++
)
{
for
(
size_
t
n
=
0
;
n
<
length
;
n
++
)
{
int
key
=
rand
()
%
(
int
)(
sizeof
(
charset
)
-
1
);
int
key
=
rand
()
%
(
int
)(
sizeof
(
charset
)
-
1
);
randomString
[
n
]
=
charset
[
key
];
randomString
[
n
]
=
charset
[
key
];
}
}
...
...
coupler/keep_alive_publisher.h
View file @
136840d5
...
@@ -179,14 +179,12 @@ void callbackTicHeartBeat()
...
@@ -179,14 +179,12 @@ void callbackTicHeartBeat()
}
}
static
void
enablePublishHeartBeat
(
UA_Server
*
server
,
UA_ServerConfig
*
config
){
static
void
enablePublishHeartBeat
(
UA_Server
*
server
){
in
t
i
;
size_
t
i
;
// add a callback which will increment heart beat tics
// add a callback which will increment heart beat tics
UA_UInt64
callbackId
=
1
;
UA_UInt64
callbackId
=
1
;
UA_Server_addRepeatedCallback
(
server
,
callbackTicHeartBeat
,
NULL
,
HEART_BEAT_INTERVAL
,
&
callbackId
);
UA_Server_addRepeatedCallback
(
server
,
callbackTicHeartBeat
,
NULL
,
HEART_BEAT_INTERVAL
,
&
callbackId
);
UA_UInt32
defaultUInt32
=
0
;
UA_UInt32
couplerID
=
COUPLER_ID
;
UA_Float
defaultFloat
=
0
.
0
;
UA_Float
defaultFloat
=
0
.
0
;
const
PublishedVariable
publishedVariableArray
[]
=
{
const
PublishedVariable
publishedVariableArray
[]
=
{
// representing time in millis since start of process
// representing time in millis since start of process
...
...
coupler/keep_alive_subscriber.h
View file @
136840d5
...
@@ -63,10 +63,10 @@ static void dataChangeNotificationCallback(UA_Server *server, UA_UInt32 monitore
...
@@ -63,10 +63,10 @@ static void dataChangeNotificationCallback(UA_Server *server, UA_UInt32 monitore
milli_seconds_now
=
getMilliSecondsSinceEpoch
();
milli_seconds_now
=
getMilliSecondsSinceEpoch
();
// split <ID>.<heart_beats>, just converting to int is enough
// split <ID>.<heart_beats>, just converting to int is enough
coupler_id
=
(
int
)
heart_beat
;
coupler_id
=
(
int
)
heart_beat
;
if
(
coupler_id
!=
COUPLER_ID
)
{
if
(
coupler_id
!=
(
unsigned
int
)
COUPLER_ID
)
{
/
/UA_LOG_INFO(UA_Log_Stdout, \
/
*UA_LOG_INFO(UA_Log_Stdout,
// UA_LOGCATEGORY_USERLAND, \
UA_LOGCATEGORY_USERLAND,
// "HEART BEAT: %d (%ld)", coupler_id, milli_seconds_now);
"HEART BEAT: %d (%ld)", coupler_id, milli_seconds_now);*/
// convert coupler_id to str
// convert coupler_id to str
char
*
coupler_id_str
=
convertInt2Str
(
coupler_id
);
char
*
coupler_id_str
=
convertInt2Str
(
coupler_id
);
...
@@ -246,11 +246,11 @@ void callbackCheckHeartBeat() {
...
@@ -246,11 +246,11 @@ void callbackCheckHeartBeat() {
* Check if for liveness of related couplers. Called upon a certain interval.
* Check if for liveness of related couplers. Called upon a certain interval.
* If a related coupler is down got to safe mode.
* If a related coupler is down got to safe mode.
*/
*/
int
i
,
coupler_id
,
last_seen_timestamp_int
,
timestamp_delta
;
int
coupler_id
,
last_seen_timestamp_int
,
timestamp_delta
;
bool
is_down
;
bool
is_down
;
unsigned
long
int
milli_seconds
=
getMilliSecondsSinceEpoch
();
unsigned
long
int
milli_seconds
=
getMilliSecondsSinceEpoch
();
size_t
n
=
sizeof
(
HEART_BEAT_ID_LIST
)
/
sizeof
(
HEART_BEAT_ID_LIST
[
0
]);
size_t
n
=
sizeof
(
HEART_BEAT_ID_LIST
)
/
sizeof
(
HEART_BEAT_ID_LIST
[
0
]);
for
(
in
t
i
=
0
;
i
<
n
;
i
++
)
{
for
(
size_
t
i
=
0
;
i
<
n
;
i
++
)
{
coupler_id
=
HEART_BEAT_ID_LIST
[
i
];
coupler_id
=
HEART_BEAT_ID_LIST
[
i
];
if
(
coupler_id
>
0
)
{
if
(
coupler_id
>
0
)
{
// convert to str as this is the hash key
// convert to str as this is the hash key
...
@@ -264,7 +264,7 @@ void callbackCheckHeartBeat() {
...
@@ -264,7 +264,7 @@ void callbackCheckHeartBeat() {
is_down
=
(
timestamp_delta
>
HEART_BEAT_TIMEOUT_INTERVAL
);
is_down
=
(
timestamp_delta
>
HEART_BEAT_TIMEOUT_INTERVAL
);
if
(
is_down
)
{
if
(
is_down
)
{
// count for stats the switch to SAFE mode
// count for stats the switch to SAFE mode
if
(
CURRENT_STATE
!=
STATE_DOWN
)
{
if
(
CURRENT_STATE
!=
(
unsigned
int
)
STATE_DOWN
)
{
CURRENT_STATE
=
STATE_DOWN
;
CURRENT_STATE
=
STATE_DOWN
;
SAFE_MODE_STATE_COUNTER
+=
1
;
SAFE_MODE_STATE_COUNTER
+=
1
;
UA_LOG_INFO
(
UA_Log_Stdout
,
\
UA_LOG_INFO
(
UA_Log_Stdout
,
\
...
@@ -276,11 +276,11 @@ void callbackCheckHeartBeat() {
...
@@ -276,11 +276,11 @@ void callbackCheckHeartBeat() {
}
}
else
{
else
{
// all good, we received a keep alive in time
// all good, we received a keep alive in time
if
(
CURRENT_STATE
==
STATE_NO_INITIAL_HEART_BEAT
)
{
if
(
CURRENT_STATE
==
(
unsigned
int
)
STATE_NO_INITIAL_HEART_BEAT
)
{
// initial keep alive received, printout
// initial keep alive received, printout
UA_LOG_INFO
(
UA_Log_Stdout
,
UA_LOGCATEGORY_USERLAND
,
"INITIAL HEART BEAT received: %s"
,
coupler_id_str
);
UA_LOG_INFO
(
UA_Log_Stdout
,
UA_LOGCATEGORY_USERLAND
,
"INITIAL HEART BEAT received: %s"
,
coupler_id_str
);
}
}
else
if
(
CURRENT_STATE
==
STATE_DOWN
)
{
else
if
(
CURRENT_STATE
==
(
unsigned
int
)
STATE_DOWN
)
{
// initial keep alive received, printout
// initial keep alive received, printout
UA_LOG_INFO
(
UA_Log_Stdout
,
UA_LOGCATEGORY_USERLAND
,
UA_LOG_INFO
(
UA_Log_Stdout
,
UA_LOGCATEGORY_USERLAND
,
"UP (recovered %d times): %s"
,
SAFE_MODE_STATE_COUNTER
,
coupler_id_str
);
"UP (recovered %d times): %s"
,
SAFE_MODE_STATE_COUNTER
,
coupler_id_str
);
...
@@ -292,7 +292,7 @@ void callbackCheckHeartBeat() {
...
@@ -292,7 +292,7 @@ void callbackCheckHeartBeat() {
}
}
else
{
else
{
// still no hear beat from this coupler ...
// still no hear beat from this coupler ...
if
(
CURRENT_STATE
!=
STATE_NO_INITIAL_HEART_BEAT
){
if
(
CURRENT_STATE
!=
(
unsigned
int
)
STATE_NO_INITIAL_HEART_BEAT
){
CURRENT_STATE
=
STATE_NO_INITIAL_HEART_BEAT
;
CURRENT_STATE
=
STATE_NO_INITIAL_HEART_BEAT
;
UA_LOG_INFO
(
UA_Log_Stdout
,
UA_LOGCATEGORY_USERLAND
,
"NO INITIAL HEART BEAT: %s"
,
coupler_id_str
);
UA_LOG_INFO
(
UA_Log_Stdout
,
UA_LOGCATEGORY_USERLAND
,
"NO INITIAL HEART BEAT: %s"
,
coupler_id_str
);
}
}
...
@@ -302,7 +302,7 @@ void callbackCheckHeartBeat() {
...
@@ -302,7 +302,7 @@ void callbackCheckHeartBeat() {
}
}
static
int
enableSubscribeToHeartBeat
(
UA_Server
*
server
,
UA_ServerConfig
*
config
){
static
void
enableSubscribeToHeartBeat
(
UA_Server
*
server
,
UA_ServerConfig
*
config
){
// enable subscribe to keep-alive messages
// enable subscribe to keep-alive messages
UA_String
transportProfile
=
UA_STRING
(
DEFAULT_TRANSPORT_PROFILE
);
UA_String
transportProfile
=
UA_STRING
(
DEFAULT_TRANSPORT_PROFILE
);
UA_NetworkAddressUrlDataType
networkAddressUrl
=
{
UA_STRING_NULL
,
UA_STRING
(
NETWORK_ADDRESS_URL_DATA_TYPE
)};
UA_NetworkAddressUrlDataType
networkAddressUrl
=
{
UA_STRING_NULL
,
UA_STRING
(
NETWORK_ADDRESS_URL_DATA_TYPE
)};
...
...
coupler/mod_io_i2c.h
View file @
136840d5
...
@@ -85,7 +85,6 @@ static int setRelayState(int command, int i2c_addr)
...
@@ -85,7 +85,6 @@ static int setRelayState(int command, int i2c_addr)
* Set relays' state over I2C
* Set relays' state over I2C
*/
*/
int
file
;
int
file
;
char
filename
[
20
];
if
(
I2C_VIRTUAL_MODE
)
if
(
I2C_VIRTUAL_MODE
)
{
{
// we're in a virtual mode, likely on x86 platform or without I2C support
// we're in a virtual mode, likely on x86 platform or without I2C support
...
@@ -121,6 +120,7 @@ static int setRelayState(int command, int i2c_addr)
...
@@ -121,6 +120,7 @@ static int setRelayState(int command, int i2c_addr)
printf
(
"Error writing to i2c slave (0x%x).
\n
"
,
i2c_addr
);
printf
(
"Error writing to i2c slave (0x%x).
\n
"
,
i2c_addr
);
}
}
close
(
file
);
close
(
file
);
return
0
;
}
}
static
int
getDigitalInputState
(
int
i2c_addr
,
char
**
digital_input
)
static
int
getDigitalInputState
(
int
i2c_addr
,
char
**
digital_input
)
...
@@ -129,7 +129,6 @@ static int getDigitalInputState(int i2c_addr, char **digital_input)
...
@@ -129,7 +129,6 @@ static int getDigitalInputState(int i2c_addr, char **digital_input)
* get digital input state over I2C
* get digital input state over I2C
*/
*/
int
file
;
int
file
;
char
filename
[
20
];
if
(
I2C_VIRTUAL_MODE
)
if
(
I2C_VIRTUAL_MODE
)
{
{
// we're in a virtual mode, likely on x86 platform or without I2C support
// we're in a virtual mode, likely on x86 platform or without I2C support
...
@@ -174,6 +173,7 @@ static int getDigitalInputState(int i2c_addr, char **digital_input)
...
@@ -174,6 +173,7 @@ static int getDigitalInputState(int i2c_addr, char **digital_input)
*
digital_input
=
&
read_buf
[
0
];
*
digital_input
=
&
read_buf
[
0
];
}
}
close
(
file
);
close
(
file
);
return
0
;
}
}
static
int
getAnalogInputStateAIN
(
int
i2c_addr
,
int
**
analog_input
,
uint8_t
read_reg
)
static
int
getAnalogInputStateAIN
(
int
i2c_addr
,
int
**
analog_input
,
uint8_t
read_reg
)
...
@@ -182,7 +182,6 @@ static int getAnalogInputStateAIN(int i2c_addr, int **analog_input, uint8_t read
...
@@ -182,7 +182,6 @@ static int getAnalogInputStateAIN(int i2c_addr, int **analog_input, uint8_t read
* get digital input state over I2C
* get digital input state over I2C
*/
*/
int
file
;
int
file
;
char
filename
[
20
];
if
(
I2C_VIRTUAL_MODE
)
if
(
I2C_VIRTUAL_MODE
)
{
{
// we're in a virtual mode, likely on x86 platform or without I2C support
// we're in a virtual mode, likely on x86 platform or without I2C support
...
@@ -231,6 +230,7 @@ static int getAnalogInputStateAIN(int i2c_addr, int **analog_input, uint8_t read
...
@@ -231,6 +230,7 @@ static int getAnalogInputStateAIN(int i2c_addr, int **analog_input, uint8_t read
*
analog_input
=
&
analog_data
;
*
analog_input
=
&
analog_data
;
}
}
close
(
file
);
close
(
file
);
return
0
;
}
}
void
safeShutdownI2CSlaveList
()
void
safeShutdownI2CSlaveList
()
...
...
coupler/server.c
View file @
136840d5
...
@@ -123,9 +123,6 @@ int main(int argc, char **argv)
...
@@ -123,9 +123,6 @@ int main(int argc, char **argv)
signal
(
SIGINT
,
stopHandler
);
signal
(
SIGINT
,
stopHandler
);
signal
(
SIGTERM
,
stopHandler
);
signal
(
SIGTERM
,
stopHandler
);
UA_String
serverUrls
[
1
];
size_t
serverUrlsSize
=
0
;
char
serverUrlBuffer
[
1
][
512
];
server
=
UA_Server_new
();
server
=
UA_Server_new
();
if
(
!
ENABLE_X509
){
if
(
!
ENABLE_X509
){
...
@@ -156,12 +153,11 @@ int main(int argc, char **argv)
...
@@ -156,12 +153,11 @@ int main(int argc, char **argv)
/* Loading of a revocation list currently unsupported */
/* Loading of a revocation list currently unsupported */
UA_ByteString
*
revocationList
=
NULL
;
UA_ByteString
*
revocationList
=
NULL
;
size_t
revocationListSize
=
0
;
size_t
revocationListSize
=
0
;
UA_StatusCode
retval
=
UA_ServerConfig_setDefaultWithSecurityPolicies
(
config
,
OPC_UA_PORT
,
UA_ServerConfig_setDefaultWithSecurityPolicies
(
config
,
OPC_UA_PORT
,
&
certificate
,
&
privateKey
,
&
certificate
,
&
privateKey
,
trustList
,
trustListSize
,
trustList
,
trustListSize
,
issuerList
,
issuerListSize
,
issuerList
,
issuerListSize
,
revocationList
,
revocationListSize
);
revocationList
,
revocationListSize
);
//The place to fill the hole is very important
//The place to fill the hole is very important
config
->
applicationDescription
.
applicationUri
=
UA_STRING_ALLOC
(
"urn:open62541.server.application"
);
config
->
applicationDescription
.
applicationUri
=
UA_STRING_ALLOC
(
"urn:open62541.server.application"
);
}
}
...
@@ -174,7 +170,7 @@ int main(int argc, char **argv)
...
@@ -174,7 +170,7 @@ int main(int argc, char **argv)
};
};
config
->
accessControl
.
clear
(
&
config
->
accessControl
);
config
->
accessControl
.
clear
(
&
config
->
accessControl
);
UA_
StatusCode
retval1
=
UA_
AccessControl_default
(
config
,
false
,
NULL
,
UA_AccessControl_default
(
config
,
false
,
NULL
,
&
config
->
securityPolicies
[
config
->
securityPoliciesSize
-
1
].
policyUri
,
1
,
logins
);
&
config
->
securityPolicies
[
config
->
securityPoliciesSize
-
1
].
policyUri
,
1
,
logins
);
}
}
...
@@ -189,7 +185,7 @@ int main(int argc, char **argv)
...
@@ -189,7 +185,7 @@ int main(int argc, char **argv)
// enable publish keep-alive messages
// enable publish keep-alive messages
if
(
ENABLE_HEART_BEAT
)
{
if
(
ENABLE_HEART_BEAT
)
{
enablePublishHeartBeat
(
server
,
config
);
enablePublishHeartBeat
(
server
);
}
}
// enable subscribe to keep-alive messages
// enable subscribe to keep-alive messages
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment