Refactor USB/UART mutexes, code cleanup, CoreMutex addition

Add a CoreMutex class which implements a deadlock-safe mutex and reqork
the SerialUSB and SerialUART classes to use it to synchronize output
when in a multicore sketch.
parent 2464d604
/*
* CoreMutex for the Raspberry Pi Pico RP2040
*
* Implements a deadlock-safe multicore mutex for sharing things like the
* USB or UARTs.
*
* Copyright (c) 2021 Earle F. Philhower, III <earlephilhower@yahoo.com>
*
* 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.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "pico/mutex.h"
class CoreMutex {
public:
CoreMutex(mutex_t *mutex) {
uint32_t owner;
_mutex = mutex;
_acquired = false;
if (!mutex_try_enter(_mutex, &owner)) {
if (owner == get_core_num()) { // Deadlock!
return;
}
mutex_enter_blocking(_mutex);
}
_acquired = true;
}
~CoreMutex() {
if (_acquired) {
mutex_exit(_mutex);
}
}
operator bool() {
return _acquired;
}
private:
mutex_t *_mutex;
bool _acquired;
};
......@@ -19,6 +19,7 @@
*/
#include "SerialUART.h"
#include "CoreMutex.h"
#include <hardware/uart.h>
#include <hardware/gpio.h>
......@@ -56,6 +57,13 @@ bool SerialUART::setTX(pin_size_t tx) {
}
}
SerialUART::SerialUART(uart_inst_t *uart, pin_size_t tx, pin_size_t rx) {
_uart = uart;
_tx = tx;
_rx = rx;
mutex_init(&_mutex);
}
void SerialUART::begin(unsigned long baud, uint16_t config) {
_baud = baud;
uart_init(_uart, baud);
......@@ -84,12 +92,16 @@ void SerialUART::begin(unsigned long baud, uint16_t config) {
}
void SerialUART::end() {
if (!_running) {
return;
}
uart_deinit(_uart);
_running = false;
}
int SerialUART::peek() {
if (!_running) {
CoreMutex m(&_mutex);
if (!_running || !m) {
return -1;
}
if (_peek >= 0) {
......@@ -100,7 +112,8 @@ int SerialUART::peek() {
}
int SerialUART::read() {
if (!_running) {
CoreMutex m(&_mutex);
if (!_running || !m) {
return -1;
}
if (_peek >= 0) {
......@@ -112,28 +125,32 @@ int SerialUART::read() {
}
int SerialUART::available() {
if (!_running) {
CoreMutex m(&_mutex);
if (!_running || !m) {
return 0;
}
return (uart_is_readable(_uart)) ? 1 : 0;
}
int SerialUART::availableForWrite() {
if (!_running) {
CoreMutex m(&_mutex);
if (!_running || !m) {
return 0;
}
return (uart_is_writable(_uart)) ? 1 : 0;
}
void SerialUART::flush() {
// TODO, must be smarter way. Now, just sleep long enough to guarantee a full FIFO goes out (very conservative)
//int us_per_bit = 1 + (1000000 / _baud);
//delayMicroseconds(us_per_bit * 32 * 8);
CoreMutex m(&_mutex);
if (!_running || !m) {
return;
}
uart_default_tx_wait_blocking();
}
size_t SerialUART::write(uint8_t c) {
if (!_running) {
CoreMutex m(&_mutex);
if (!_running || !m) {
return 0;
}
uart_putc_raw(_uart, c);
......@@ -141,7 +158,8 @@ size_t SerialUART::write(uint8_t c) {
}
size_t SerialUART::write(const uint8_t *p, size_t len) {
if (!_running) {
CoreMutex m(&_mutex);
if (!_running || !m) {
return 0;
}
size_t cnt = len;
......
......@@ -24,12 +24,13 @@
#include <Arduino.h>
#include "api/HardwareSerial.h"
#include <stdarg.h>
#include "CoreMutex.h"
extern "C" typedef struct uart_inst uart_inst_t;
class SerialUART : public HardwareSerial {
public:
SerialUART(uart_inst_t *uart, pin_size_t tx, pin_size_t rx) { _uart = uart; _tx = tx; _rx = rx; }
SerialUART(uart_inst_t *uart, pin_size_t tx, pin_size_t rx);
// Select the pinout. Call before .begin()
bool setRX(pin_size_t pin);
......@@ -79,6 +80,7 @@ private:
pin_size_t _tx, _rx;
int _baud;
int _peek;
mutex_t _mutex;
};
extern SerialUART Serial1; // HW UART 0
......
......@@ -21,6 +21,7 @@
*/
#include <Arduino.h>
#include "CoreMutex.h"
#include "tusb.h"
#include "pico/time.h"
......@@ -182,82 +183,52 @@ void SerialUSB::end() {
}
int SerialUSB::peek() {
if (!_running) {
CoreMutex m(&usb_mutex);
if (!_running || !m) {
return 0;
}
uint8_t c;
uint32_t owner;
if (!mutex_try_enter(&usb_mutex, &owner)) {
if (owner == get_core_num()) return -1; // would deadlock otherwise
mutex_enter_blocking(&usb_mutex);
}
auto ret = tud_cdc_peek(0, &c) ? (int) c : -1;
mutex_exit(&usb_mutex);
return ret;
return tud_cdc_peek(0, &c) ? (int) c : -1;
}
int SerialUSB::read() {
if (!_running) {
CoreMutex m(&usb_mutex);
if (!_running || !m) {
return -1;
}
uint32_t owner;
if (!mutex_try_enter(&usb_mutex, &owner)) {
if (owner == get_core_num()) return -1; // would deadlock otherwise
mutex_enter_blocking(&usb_mutex);
}
if (tud_cdc_connected() && tud_cdc_available()) {
int ch = tud_cdc_read_char();
mutex_exit(&usb_mutex);
return ch;
return tud_cdc_read_char();
}
mutex_exit(&usb_mutex);
return -1;
}
int SerialUSB::available() {
if (!_running) {
CoreMutex m(&usb_mutex);
if (!_running || !m) {
return 0;
}
uint32_t owner;
if (!mutex_try_enter(&usb_mutex, &owner)) {
if (owner == get_core_num()) return 0; // would deadlock otherwise
mutex_enter_blocking(&usb_mutex);
}
auto ret = tud_cdc_available();
mutex_exit(&usb_mutex);
return ret;
return tud_cdc_available();
}
int SerialUSB::availableForWrite() {
if (!_running) {
CoreMutex m(&usb_mutex);
if (!_running || !m) {
return 0;
}
uint32_t owner;
if (!mutex_try_enter(&usb_mutex, &owner)) {
if (owner == get_core_num()) return 0; // would deadlock otherwise
mutex_enter_blocking(&usb_mutex);
}
auto ret = tud_cdc_write_available();
mutex_exit(&usb_mutex);
return ret;
return tud_cdc_write_available();
}
void SerialUSB::flush() {
if (!_running) {
CoreMutex m(&usb_mutex);
if (!_running || !m) {
return;
}
uint32_t owner;
if (!mutex_try_enter(&usb_mutex, &owner)) {
if (owner == get_core_num()) return; // would deadlock otherwise
mutex_enter_blocking(&usb_mutex);
}
tud_cdc_write_flush();
mutex_exit(&usb_mutex);
}
size_t SerialUSB::write(uint8_t c) {
......@@ -265,16 +236,12 @@ size_t SerialUSB::write(uint8_t c) {
}
size_t SerialUSB::write(const uint8_t *buf, size_t length) {
if (!_running) {
CoreMutex m(&usb_mutex);
if (!_running || !m) {
return 0;
}
static uint64_t last_avail_time;
uint32_t owner;
if (!mutex_try_enter(&usb_mutex, &owner)) {
if (owner == get_core_num()) return 0; // would deadlock otherwise
mutex_enter_blocking(&usb_mutex);
}
int i = 0;
if (tud_cdc_connected()) {
for (int i = 0; i < length;) {
......@@ -300,24 +267,17 @@ size_t SerialUSB::write(const uint8_t *buf, size_t length) {
// reset our timeout
last_avail_time = 0;
}
mutex_exit(&usb_mutex);
return i;
}
SerialUSB::operator bool() {
if (!_running) {
CoreMutex m(&usb_mutex);
if (!_running || !m) {
return false;
}
uint32_t owner;
if (!mutex_try_enter(&usb_mutex, &owner)) {
if (owner == get_core_num()) return -1; // would deadlock otherwise
mutex_enter_blocking(&usb_mutex);
}
tud_task();
auto ret = tud_cdc_connected();
mutex_exit(&usb_mutex);
return ret;
return tud_cdc_connected();
}
......
......@@ -20,7 +20,7 @@
# https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5---3rd-party-Hardware-specification
name=Raspberry Pi RP2040 Boards
version=0.9.2
version=0.9.9
runtime.tools.pqt-gcc.path={runtime.platform.path}/system/arm-none-eabi
runtime.tools.pqt-python3.path={runtime.platform.path}/system/python3
runtime.tools.pqt-mklittlefs.path={runtime.platform.path}/system/mklittlefs
......@@ -39,7 +39,7 @@ compiler.warning_flags.more=-Wall
compiler.warning_flags.all=-Wall -Wextra
compiler.defines={build.led}
compiler.includes="-I{runtime.platform.path}/pico_base/" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_unique_id/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_platform/include/" "-I{runtime.platform.path}/pico-sdk/src/common/pico_base/include/" "-I{runtime.platform.path}/pico-sdk/src/rp2040/hardware_regs/include/" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_timer/include/" "-I{runtime.platform.path}/pico-sdk/src/common/pico_stdlib/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_gpio/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_i2c/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_flash/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_base/include" "-I{runtime.platform.path}/pico-examples/build/generated/pico_base" "-I{runtime.platform.path}/pico-sdk/src/boards/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_platform/include" "-I{runtime.platform.path}/pico-sdk/src/rp2040/hardware_regs/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_base/include" "-I{runtime.platform.path}/pico-sdk/src/rp2040/hardware_structs/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_claim/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_sync/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_uart/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_divider/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_time/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_timer/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_sync/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_util/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_runtime/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_clocks/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_resets/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_watchdog/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_xosc/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_pll/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_vreg/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_irq/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_printf/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_bootrom/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_bit_ops/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_divider/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_double/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_int64_ops/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_float/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_binary_info/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_pio/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_stdio/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_stdio_uart/include" "-I{runtime.platform.path}/pico-sdk/src/rp2040/hardware_regs/include/" "-I{runtime.platform.path}/pico-sdk/lib/tinyusb/src/" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_stdio_usb/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_spi/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_pwm/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_adc/include"
compiler.includes="-I{runtime.platform.path}/pico_base/" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_unique_id/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_platform/include/" "-I{runtime.platform.path}/pico-sdk/src/common/pico_base/include/" "-I{runtime.platform.path}/pico-sdk/src/rp2040/hardware_regs/include/" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_timer/include/" "-I{runtime.platform.path}/pico-sdk/src/common/pico_stdlib/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_gpio/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_i2c/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_flash/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_base/include" "-I{runtime.platform.path}/pico-examples/build/generated/pico_base" "-I{runtime.platform.path}/pico-sdk/src/boards/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_platform/include" "-I{runtime.platform.path}/pico-sdk/src/rp2040/hardware_regs/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_base/include" "-I{runtime.platform.path}/pico-sdk/src/rp2040/hardware_structs/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_claim/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_sync/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_uart/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_divider/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_time/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_timer/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_sync/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_util/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_runtime/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_clocks/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_resets/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_watchdog/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_xosc/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_pll/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_vreg/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_irq/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_printf/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_bootrom/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_bit_ops/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_divider/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_double/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_int64_ops/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_float/include" "-I{runtime.platform.path}/pico-sdk/src/common/pico_binary_info/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_pio/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_stdio/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_stdio_uart/include" "-I{runtime.platform.path}/pico-sdk/src/rp2040/hardware_regs/include/" "-I{runtime.platform.path}/pico-sdk/lib/tinyusb/src/" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_stdio_usb/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_spi/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_pwm/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/hardware_adc/include" "-I{runtime.platform.path}/pico-sdk/src/rp2_common/pico_multicore/include"
compiler.flags=-Os -march=armv6-m -mcpu=cortex-m0plus -mthumb -ffunction-sections -fdata-sections
compiler.wrap=-Wl,--wrap=acos -Wl,--wrap=acosf -Wl,--wrap=acosh -Wl,--wrap=acoshf -Wl,--wrap=__aeabi_cdcmpeq -Wl,--wrap=__aeabi_cdcmple -Wl,--wrap=__aeabi_cdrcmple -Wl,--wrap=__aeabi_cfcmpeq -Wl,--wrap=__aeabi_cfcmple -Wl,--wrap=__aeabi_cfrcmple -Wl,--wrap=__aeabi_d2f -Wl,--wrap=__aeabi_d2iz -Wl,--wrap=__aeabi_d2lz -Wl,--wrap=__aeabi_d2uiz -Wl,--wrap=__aeabi_d2ulz -Wl,--wrap=__aeabi_dadd -Wl,--wrap=__aeabi_dcmpeq -Wl,--wrap=__aeabi_dcmpge -Wl,--wrap=__aeabi_dcmpgt -Wl,--wrap=__aeabi_dcmple -Wl,--wrap=__aeabi_dcmplt -Wl,--wrap=__aeabi_dcmpun -Wl,--wrap=__aeabi_ddiv -Wl,--wrap=__aeabi_dmul -Wl,--wrap=__aeabi_drsub -Wl,--wrap=__aeabi_dsub -Wl,--wrap=__aeabi_f2d -Wl,--wrap=__aeabi_f2iz -Wl,--wrap=__aeabi_f2lz -Wl,--wrap=__aeabi_f2uiz -Wl,--wrap=__aeabi_f2ulz -Wl,--wrap=__aeabi_fadd -Wl,--wrap=__aeabi_fcmpeq -Wl,--wrap=__aeabi_fcmpge -Wl,--wrap=__aeabi_fcmpgt -Wl,--wrap=__aeabi_fcmple -Wl,--wrap=__aeabi_fcmplt -Wl,--wrap=__aeabi_fcmpun -Wl,--wrap=__aeabi_fdiv -Wl,--wrap=__aeabi_fmul -Wl,--wrap=__aeabi_frsub -Wl,--wrap=__aeabi_fsub -Wl,--wrap=__aeabi_i2d -Wl,--wrap=__aeabi_i2f -Wl,--wrap=__aeabi_idiv -Wl,--wrap=__aeabi_idivmod -Wl,--wrap=__aeabi_l2d -Wl,--wrap=__aeabi_l2f -Wl,--wrap=__aeabi_ldivmod -Wl,--wrap=__aeabi_lmul -Wl,--wrap=__aeabi_memcpy -Wl,--wrap=__aeabi_memcpy4 -Wl,--wrap=__aeabi_memcpy8 -Wl,--wrap=__aeabi_memset -Wl,--wrap=__aeabi_memset4 -Wl,--wrap=__aeabi_memset8 -Wl,--wrap=__aeabi_ui2d -Wl,--wrap=__aeabi_ui2f -Wl,--wrap=__aeabi_uidiv -Wl,--wrap=__aeabi_uidivmod -Wl,--wrap=__aeabi_ul2d -Wl,--wrap=__aeabi_ul2f -Wl,--wrap=__aeabi_uldivmod -Wl,--wrap=asin -Wl,--wrap=asinf -Wl,--wrap=asinh -Wl,--wrap=asinhf -Wl,--wrap=atan -Wl,--wrap=atan2 -Wl,--wrap=atan2f -Wl,--wrap=atanf -Wl,--wrap=atanh -Wl,--wrap=atanhf -Wl,--wrap=calloc -Wl,--wrap=cbrt -Wl,--wrap=cbrtf -Wl,--wrap=ceil -Wl,--wrap=ceilf -Wl,--wrap=__clz -Wl,--wrap=__clzdi2 -Wl,--wrap=__clzl -Wl,--wrap=__clzll -Wl,--wrap=__clzsi2 -Wl,--wrap=copysign -Wl,--wrap=copysignf -Wl,--wrap=cos -Wl,--wrap=cosf -Wl,--wrap=cosh -Wl,--wrap=coshf -Wl,--wrap=__ctzdi2 -Wl,--wrap=__ctzsi2 -Wl,--wrap=drem -Wl,--wrap=dremf -Wl,--wrap=exp -Wl,--wrap=exp10 -Wl,--wrap=exp10f -Wl,--wrap=exp2 -Wl,--wrap=exp2f -Wl,--wrap=expf -Wl,--wrap=expm1 -Wl,--wrap=expm1f -Wl,--wrap=floor -Wl,--wrap=floorf -Wl,--wrap=fma -Wl,--wrap=fmaf -Wl,--wrap=fmod -Wl,--wrap=fmodf -Wl,--wrap=free -Wl,--wrap=hypot -Wl,--wrap=hypotf -Wl,--wrap=ldexp -Wl,--wrap=ldexpf -Wl,--wrap=log -Wl,--wrap=log10 -Wl,--wrap=log10f -Wl,--wrap=log1p -Wl,--wrap=log1pf -Wl,--wrap=log2 -Wl,--wrap=log2f -Wl,--wrap=logf -Wl,--wrap=malloc -Wl,--wrap=memcpy -Wl,--wrap=memset -Wl,--wrap=__popcountdi2 -Wl,--wrap=__popcountsi2 -Wl,--wrap=pow -Wl,--wrap=powf -Wl,--wrap=powint -Wl,--wrap=powintf -Wl,--wrap=remainder -Wl,--wrap=remainderf -Wl,--wrap=remquo -Wl,--wrap=remquof -Wl,--wrap=round -Wl,--wrap=roundf -Wl,--wrap=sin -Wl,--wrap=sincos -Wl,--wrap=sincosf -Wl,--wrap=sinf -Wl,--wrap=sinh -Wl,--wrap=sinhf -Wl,--wrap=sqrt -Wl,--wrap=sqrtf -Wl,--wrap=tan -Wl,--wrap=tanf -Wl,--wrap=tanh -Wl,--wrap=tanhf -Wl,--wrap=trunc -Wl,--wrap=truncf
......
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