From 336335807d9c4db0e1f64df954898cb380195917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Silva?= <79662866+JesusSilvaUtrera@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:39:36 +0200 Subject: [PATCH 1/2] Update bill of materials (#241) Signed-off-by: JesusSilvaUtrera --- andino_hardware/README.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/andino_hardware/README.md b/andino_hardware/README.md index c1c3dcf2..c72d75ce 100644 --- a/andino_hardware/README.md +++ b/andino_hardware/README.md @@ -4,18 +4,19 @@ This package aims to provide the necessary information to the correct assembly o ## Bill of Materials -| Module | Part | Variant | Comments | -|:--|:------------------------|:---------------------|:-------------------------------------------------------:| -| SBC | Raspberry Pi 4 B (4 Gb) | - | - | -| Chassis | 2 x [Print 3d Chassis](./printing_model/chassis/) + [Wheels](https://www.sparkfun.com/products/13259) | [Robot Smart Car Kit](https://www.amazon.com/perseids-Chassis-Encoder-Wheels-Battery/dp/B07DNYQ3PX/ref=sr_1_1?crid=9WUXNUN54JBG&keywords=Smart%2BCar%2BChassis%2BKit&qid=1685739917&sprefix=smart%2Bcar%2Bchassis%2Bkit%2Caps%2C348&sr=8-1&th=1) | - | -| Motors | 2 x [Hobby Motor with Encoder - Metal Gear (DG01D-E)](https://www.sparkfun.com/products/16413) | [Robot Smart Car Kit](https://www.amazon.com/perseids-Chassis-Encoder-Wheels-Battery/dp/B07DNYQ3PX/ref=sr_1_1?crid=9WUXNUN54JBG&keywords=Smart%2BCar%2BChassis%2BKit&qid=1685739917&sprefix=smart%2Bcar%2Bchassis%2Bkit%2Caps%2C348&sr=8-1&th=1)'s motors + sensor hall encoder | Embbebed encoders are recommended for better accuracy | -| Microcontroller | Arduino Nano | Arduino Uno | Nano is easier to mount given its size | -| Motor Driver | [L298N Dual H Bridge](https://www.amazon.com/Bridge-Stepper-Driver-Module-Controller/dp/B09T6K9RFZ/ref=sr_1_4?crid=37YY7JO6C3WVE&keywords=l298&qid=1685740618&sprefix=l29%2Caps%2C277&sr=8-4) | - | - | -| Laser Scanner | [RPLidar A1M8](https://www.robotshop.com/products/rplidar-a1m8-360-degree-laser-scanner-development-kit?_pos=3&_sid=b0aefcea1&_ss=r) | [RPLidar A2M8](https://www.robotshop.com/products/rplidar-a2m8-360-laser-scanner) | - | -| Camera | [Raspi Camera Module V2, 8 MP](https://www.robotshop.com/products/raspberry-pi-camera-module-v2) | - | - | -| Powerbank 5V | - | - | Any powerbank is suitable: Mind size / weight / output current(>=2A) | -| (Optional) Power Step up | [DC - DC boost converter](https://www.amazon.com/0-9-Step-Regulator-DC-Converter/dp/B0C6QTJMFN/ref=sr_1_25?crid=G0FHM4SS5TWX&keywords=dc+step+up+converter&qid=1685741155&sprefix=dc+step+up+conver%2Caps%2C371&sr=8-25) | - | If motors support higher voltage than 5V a step-up(e.g: to 9V) can be added between powerbank(5V) and motor driver | -| Fixing & Mount | M3 bolts/fasteners - M3 Spacers - M2.5/2.0 bolts/fasteners for SBC | - | - | +| Number | Module | Part | Links | Comments | +|:--:|:--:|:-----------------------:|:--------------------:|:-------------------------------------------------------:| +| 1 | SBC | Raspberry Pi 4 B (4 Gb) | [PiShop](https://www.pishop.us/product/raspberry-pi-4-model-b-2gb/), [TiendaTec](https://www.tiendatec.es/raspberry-pi/gama-raspberry-pi/1100-raspberry-pi-4-modelo-b-4gb-5056561800349.html) | If you want better performance you could buy the 8GB model | +| 2 | Chassis | 2 x Print 3d Chassis + Rubber Tyre Wheels | [Chassis](./printing_model/chassis/), [Wheels Sparkfun](https://www.sparkfun.com/products/13259) | - | +| 3 | Motors | 2 x Motor with Encoder | [Sparkfun](https://www.sparkfun.com/products/16413) | - | +| 4 | Microcontroller | Arduino Nano | [Amazon](https://www.amazon.es/RUIZHI-Interfaz-Controlador-Mejorada-Compatible/dp/B0CNGKG4MZ/ref=sr_1_6?dib=eyJ2IjoiMSJ9.gnHfW9VtlEjMns12dAyHXLyFAlaikWpFyoOQJpO0iJBR-zelggQTQ9n001SH_P6NQ9DO3gPetP2krm7GAGvJus6vz4Utqu8Hy1gol0Rq7nmtJITd70ZNi3linf9v1g1iP7MlBx98cBGLVvFy-O2kZnJ63uZDwOZzwz_kExJzUWAxroO3AjufqqGOQHswLfDfjH6jpOJt54xxpCaqurDccId2O0uGKOj6WpPz6iLSubpsPB479SWYPSncxWQzz2kO4VjT6HVzPS2uWi19TS-A9WXVZceLBiz9t25Pf39jiGQ.1sLxrQ94HdIoXBq4VcDFMZhzKoL3wyJoY-U6BmDI6fY&dib_tag=se&keywords=arduino+nano+v3&qid=1714468231&sr=8-6) | You can also use an Arduino Uno, but mind size | +| 5 | Motor Driver | L298N Dual H Bridge | [Amazon](https://www.amazon.com/Bridge-Stepper-Driver-Module-Controller/dp/B09T6K9RFZ/ref=sr_1_4?crid=37YY7JO6C3WVE&keywords=l298&qid=1685740618&sprefix=l29%2Caps%2C277&sr=8-4) | - | +| 6 | Laser Scanner | RPLidar A1M8 | [RobotShop](https://www.robotshop.com/products/rplidar-a1m8-360-degree-laser-scanner-development-kit?_pos=3&_sid=b0aefcea1&_ss=r), [Amazon](https://www.amazon.es/dp/B07VLFGT27?ref_=cm_sw_r_cso_wa_apan_dp_RJ3AZC2XCEVDK0X2DCGA&starsLeft=1&th=1) | - | +| 7 | Camera | Raspi Camera Module V2, 8 MP | [Robotshop](https://www.robotshop.com/products/raspberry-pi-camera-module-v2), [Amazon](https://www.amazon.com/Raspberry-Pi-Camera-Module-Megapixel/dp/B01ER2SKFS?th=1) | - | +| 8 | Electrical Power Supply | Powerbank 5V | [Amazon](https://www.amazon.es/Heganus-Powerbank-10000mAh-port%C3%A1til-pr%C3%A1ctico/dp/B082PPPWXY/ref=asc_df_B082PPPWXY/?tag=googshopes-21&linkCode=df0&hvadid=420334509253&hvpos=&hvnetw=g&hvrand=13392500367381615369&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=9181150&hvtargid=pla-878722533582&psc=1&mcid=642b7553488f350a8726c7bfb183a667&tag=&ref=&adgrpid=95757266066&hvpone=&hvptwo=&hvadid=420334509253&hvpos=&hvnetw=g&hvrand=13392500367381615369&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=9181150&hvtargid=pla-878722533582) | Any powerbank is suitable: Mind size / weight / output current(>=2A) | +| 9 | (Optional) Power Step up | DC - DC boost converter | [Amazon America](https://www.amazon.com/0-9-Step-Regulator-DC-Converter/dp/B0C6QTJMFN/ref=sr_1_25?crid=G0FHM4SS5TWX&keywords=dc+step+up+converter&qid=1685741155&sprefix=dc+step+up+conver%2Caps%2C371&sr=8-25), [Amazon Europe](https://www.amazon.com/Converter-Adjustable-Voltage-Regulator-Compatible/dp/B089JYBF25/ref=sr_1_3?crid=3EB0RWDAO1UED&dib=eyJ2IjoiMSJ9.OVkOHemqP_yF8PlJmBNcovwOq6TzYQJADN7pCYP7m9hgHNOuzIA3jqIt5kZK9azOh0Nu3D7ucFbFjgBJprKpAQsC1VhKtCS1z6QLs6w0Ht4seE97e8yWkUkP6fPOry_5D1nyfsh0aMc7wLknNr5R9yDWTg6cYralThbLeU8qfIcpq5m66m9luKznRZiv2eUaXvI0rmcQyLKR2Z5NO_xktttAXuvHAnEnBwpk_3LZ1xA.r84ipJcrDH3o24_JEB5q7jNYEzRKyi56VO3e-xi7QXo&dib_tag=se&keywords=dc%2Bstep%2Bup%2Bconverter&qid=1714468875&sprefix=dc%2Bstep%2Bup%2Bconverter%2Caps%2C170&sr=8-3&th=1) | If motors support higher voltage than 5V a step-up(e.g: to 9V) can be added between powerbank(5V) and motor driver | +| 10 | Fixing & Mount | M3 bolts/fasteners - M3 Spacers - M2.5/2.0 bolts/fasteners for SBC | [Mercado Libre](https://articulo.mercadolibre.com.ar/MLA-823234605-kit-tornillos-electronica-500-unid-fresada-philips-m3-oferta-_JM#position=1&search_layout=stack&type=item&tracking_id=2a14497e-a3dc-4a0f-98fb-b3b524117284), [Amazon](https://www.amazon.com/Taiss-620PCS-Metric-Assortment-Washers/dp/B0CWXRG6VL/ref=sr_1_2_sspa?crid=3R3BT7LOQWZ4B&dib=eyJ2IjoiMSJ9.EBY3VtTnCGRri20ECsEwpF2eTrWOhlADXq8Rbv78LP7JVW0giUfPQ5-G3e5cVq7svNoKIPbFGf0jQoImIPuJvU72yWC0XaaXyHE03TjX1zVT-AxcCUr6bvvqnQrrwFNowZjHy2ZibnHX4sDMx3aixEmx5XUGq43KVEID5FIGzTw6xsLQd410DewktxUFWCHLSD8HR8BeAUKcP3mzciuPmc8dcz9TzY5cZ_wYFO-WyEQ.B5-OkrGZbzkIn8cw4Zb_LtQUoxX1qKuiVqI6PTNmpZk&dib_tag=se&keywords=kit+M3+tuercas+y+tornillos&qid=1714469030&sprefix=kit+m3+tuercas+y+tornillos%2Caps%2C149&sr=8-2-spons&sp_csd=d2lkZ2V0TmFtZT1zcF9hdGY&psc=1) | You would also need a set of screwdrivers if you don't have one | +| 11 | Other 3D printed parts | Camera Mount | [3D models](./printing_model/raspi_cam_mount/) | These parts are for fixing the Raspi Cam at the front of the robot | ## Connection Diagram @@ -295,7 +296,7 @@ Refer to [`usage`](../README.md#usage) section. #### Network Via terminal the wifi connection can be switched by doing: -List available wifi networks: +List available wifi networks: ``` sudo nmcli dev wifi list ``` From a609f4e5cdba37a5f87b17868acd239971cff061 Mon Sep 17 00:00:00 2001 From: Javier Balloffet Date: Wed, 8 May 2024 10:40:23 -0300 Subject: [PATCH 2/2] Add Shell class tests (#242) Signed-off-by: Javier Balloffet --- andino_firmware/platformio.ini | 1 + .../test/desktop/test_shell/shell_test.cpp | 304 ++++++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 andino_firmware/test/desktop/test_shell/shell_test.cpp diff --git a/andino_firmware/platformio.ini b/andino_firmware/platformio.ini index f3b40bbe..b63cf7ae 100644 --- a/andino_firmware/platformio.ini +++ b/andino_firmware/platformio.ini @@ -32,6 +32,7 @@ build_src_filter = + + + + + ; Environment for Arduino Uno. [env:uno] diff --git a/andino_firmware/test/desktop/test_shell/shell_test.cpp b/andino_firmware/test/desktop/test_shell/shell_test.cpp new file mode 100644 index 00000000..71789d7e --- /dev/null +++ b/andino_firmware/test/desktop/test_shell/shell_test.cpp @@ -0,0 +1,304 @@ +// BSD 3-Clause License +// +// Copyright (c) 2024, Ekumen Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#include "shell.h" + +#include +#include + +#include "serial_stream.h" + +namespace andino { +namespace test { +namespace { + +using ::testing::Return; + +class MockSerialStream : public andino::SerialStream { + public: + MockSerialStream() : andino::SerialStream() {} + MOCK_METHOD(void, begin, (unsigned long baud), (const, override)); + MOCK_METHOD(int, available, (), (const, override)); + MOCK_METHOD(int, read, (), (const, override)); + MOCK_METHOD(size_t, print, (const char* c), (const, override)); + MOCK_METHOD(size_t, print, (char c), (const, override)); + MOCK_METHOD(size_t, print, (unsigned char b, int base), (const, override)); + MOCK_METHOD(size_t, print, (int num, int base), (const, override)); + MOCK_METHOD(size_t, print, (unsigned int num, int base), (const, override)); + MOCK_METHOD(size_t, print, (long num, int base), (const, override)); + MOCK_METHOD(size_t, print, (unsigned long num, int base), (const, override)); + MOCK_METHOD(size_t, print, (double num, int digits), (const, override)); + MOCK_METHOD(size_t, println, (const char* c), (const, override)); + MOCK_METHOD(size_t, println, (char c), (const, override)); + MOCK_METHOD(size_t, println, (unsigned char b, int base), (const, override)); + MOCK_METHOD(size_t, println, (int num, int base), (const, override)); + MOCK_METHOD(size_t, println, (unsigned int num, int base), (const, override)); + MOCK_METHOD(size_t, println, (long num, int base), (const, override)); + MOCK_METHOD(size_t, println, (unsigned long num, int base), (const, override)); + MOCK_METHOD(size_t, println, (double num, int digits), (const, override)); +}; + +class ShellTest : public testing::Test { + protected: + void SetUp() override { + shell.set_serial_stream(&serial_stream_); + shell.set_default_callback(cmd_unknown_cb); + shell.register_command(kCommand1, cmd_1_cb); + shell.register_command(kCommand2, cmd_2_cb); + shell.register_command(kCommand3, cmd_3_cb); + } + + static void cmd_unknown_cb(int argc, char** argv) { + called_callback_ = 0; + save_arguments(argc, argv); + } + + static void cmd_1_cb(int argc, char** argv) { + called_callback_ = 1; + save_arguments(argc, argv); + } + + static void cmd_2_cb(int argc, char** argv) { + called_callback_ = 2; + save_arguments(argc, argv); + } + + static void cmd_3_cb(int argc, char** argv) { + called_callback_ = 3; + save_arguments(argc, argv); + } + + static void save_arguments(int argc, char** argv) { + argc_ = argc; + for (int i = 0; i < argc; i++) { + strcpy(argv_[i], argv[i]); + } + } + + static constexpr const char* kCommand1{"a"}; + static constexpr const char* kCommand2{"ab"}; + static constexpr const char* kCommand3{"cde"}; + + static int called_callback_; + static int argc_; + static char argv_[5][10]; + + andino::Shell shell; + MockSerialStream serial_stream_; +}; + +int ShellTest::called_callback_ = -1; +int ShellTest::argc_ = 0; +char ShellTest::argv_[5][10] = {'\0'}; + +TEST_F(ShellTest, ProcessInputEmpty) { + EXPECT_CALL(serial_stream_, available()).Times(1).WillOnce(Return(0)); + + shell.process_input(); +} + +TEST_F(ShellTest, ProcessInputMessageSingleCharacterCommandSingleArg) { + const char* input_message = "a\r"; + const char* expected_argv[] = {"a"}; + + int available_call_count = strlen(input_message) + 1; + EXPECT_CALL(serial_stream_, available()).Times(available_call_count); + ON_CALL(serial_stream_, available()) + .WillByDefault( + testing::Invoke([&available_call_count]() -> int { return --available_call_count; })); + + int input_index = 0; + EXPECT_CALL(serial_stream_, read()).Times(strlen(input_message)); + ON_CALL(serial_stream_, read()) + .WillByDefault(testing::Invoke( + [input_message, &input_index]() -> int { return input_message[input_index++]; })); + + shell.process_input(); + + ASSERT_EQ(called_callback_, 1); + ASSERT_EQ(argc_, 1); + EXPECT_STREQ(argv_[0], expected_argv[0]); +} + +TEST_F(ShellTest, ProcessInputMessageUnknownCommand) { + const char* input_message = "z\r"; + const char* expected_argv[] = {"z"}; + + int available_call_count = strlen(input_message) + 1; + EXPECT_CALL(serial_stream_, available()).Times(available_call_count); + ON_CALL(serial_stream_, available()) + .WillByDefault( + testing::Invoke([&available_call_count]() -> int { return --available_call_count; })); + + int input_index = 0; + EXPECT_CALL(serial_stream_, read()).Times(strlen(input_message)); + ON_CALL(serial_stream_, read()) + .WillByDefault(testing::Invoke( + [input_message, &input_index]() -> int { return input_message[input_index++]; })); + + shell.process_input(); + + ASSERT_EQ(called_callback_, 0); + ASSERT_EQ(argc_, 1); + EXPECT_STREQ(argv_[0], expected_argv[0]); +} + +TEST_F(ShellTest, ProcessInputMessageTwoCharacterCommandSingleArg) { + const char* input_message = "ab\r"; + const char* expected_argv[] = {"ab"}; + + int available_call_count = strlen(input_message) + 1; + EXPECT_CALL(serial_stream_, available()).Times(available_call_count); + ON_CALL(serial_stream_, available()) + .WillByDefault( + testing::Invoke([&available_call_count]() -> int { return --available_call_count; })); + + int input_index = 0; + EXPECT_CALL(serial_stream_, read()).Times(strlen(input_message)); + ON_CALL(serial_stream_, read()) + .WillByDefault(testing::Invoke( + [input_message, &input_index]() -> int { return input_message[input_index++]; })); + + shell.process_input(); + + ASSERT_EQ(called_callback_, 2); + ASSERT_EQ(argc_, 1); + EXPECT_STREQ(argv_[0], expected_argv[0]); +} + +TEST_F(ShellTest, ProcessInputMessageThreeCharacterCommandSingleArg) { + const char* input_message = "cde\r"; + const char* expected_argv[] = {"cde"}; + + int available_call_count = strlen(input_message) + 1; + EXPECT_CALL(serial_stream_, available()).Times(available_call_count); + ON_CALL(serial_stream_, available()) + .WillByDefault( + testing::Invoke([&available_call_count]() -> int { return --available_call_count; })); + + int input_index = 0; + EXPECT_CALL(serial_stream_, read()).Times(strlen(input_message)); + ON_CALL(serial_stream_, read()) + .WillByDefault(testing::Invoke( + [input_message, &input_index]() -> int { return input_message[input_index++]; })); + + shell.process_input(); + + ASSERT_EQ(called_callback_, 3); + ASSERT_EQ(argc_, 1); + EXPECT_STREQ(argv_[0], expected_argv[0]); +} + +TEST_F(ShellTest, ProcessInputMessageTwoArgs) { + const char* input_message = "a 12\r"; + const char* expected_argv[] = {"a", "12"}; + + int available_call_count = strlen(input_message) + 1; + EXPECT_CALL(serial_stream_, available()).Times(available_call_count); + ON_CALL(serial_stream_, available()) + .WillByDefault( + testing::Invoke([&available_call_count]() -> int { return --available_call_count; })); + + int input_index = 0; + EXPECT_CALL(serial_stream_, read()).Times(strlen(input_message)); + ON_CALL(serial_stream_, read()) + .WillByDefault(testing::Invoke( + [input_message, &input_index]() -> int { return input_message[input_index++]; })); + + shell.process_input(); + + ASSERT_EQ(called_callback_, 1); + ASSERT_EQ(argc_, 2); + EXPECT_STREQ(argv_[0], expected_argv[0]); + EXPECT_STREQ(argv_[1], expected_argv[1]); +} + +TEST_F(ShellTest, ProcessInputMessageThreeArgs) { + const char* input_message = "ab 12 3\r"; + const char* expected_argv[] = {"ab", "12", "3"}; + + int available_call_count = strlen(input_message) + 1; + EXPECT_CALL(serial_stream_, available()).Times(available_call_count); + ON_CALL(serial_stream_, available()) + .WillByDefault( + testing::Invoke([&available_call_count]() -> int { return --available_call_count; })); + + int input_index = 0; + EXPECT_CALL(serial_stream_, read()).Times(strlen(input_message)); + ON_CALL(serial_stream_, read()) + .WillByDefault(testing::Invoke( + [input_message, &input_index]() -> int { return input_message[input_index++]; })); + + shell.process_input(); + + ASSERT_EQ(called_callback_, 2); + ASSERT_EQ(argc_, 3); + EXPECT_STREQ(argv_[0], expected_argv[0]); + EXPECT_STREQ(argv_[1], expected_argv[1]); + EXPECT_STREQ(argv_[2], expected_argv[2]); +} + +TEST_F(ShellTest, ProcessInputMessageFourArgs) { + const char* input_message = "cde 12 3 456\r"; + const char* expected_argv[] = {"cde", "12", "3", "456"}; + + int available_call_count = strlen(input_message) + 1; + EXPECT_CALL(serial_stream_, available()).Times(available_call_count); + ON_CALL(serial_stream_, available()) + .WillByDefault( + testing::Invoke([&available_call_count]() -> int { return --available_call_count; })); + + int input_index = 0; + EXPECT_CALL(serial_stream_, read()).Times(strlen(input_message)); + ON_CALL(serial_stream_, read()) + .WillByDefault(testing::Invoke( + [input_message, &input_index]() -> int { return input_message[input_index++]; })); + + shell.process_input(); + + ASSERT_EQ(called_callback_, 3); + ASSERT_EQ(argc_, 4); + EXPECT_STREQ(argv_[0], expected_argv[0]); + EXPECT_STREQ(argv_[1], expected_argv[1]); + EXPECT_STREQ(argv_[2], expected_argv[2]); + EXPECT_STREQ(argv_[3], expected_argv[3]); +} + +} // namespace +} // namespace test +} // namespace andino + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + if (RUN_ALL_TESTS()) { + } + + // Always return zero-code and allow PlatformIO to parse results. + return 0; +}