01 |
Большинству тех из нас, кому доводилось быть хозяином домашнего питомца, не понаслышке знакома проблема его кормления, когда приходится уезжать надолго. Проблема не так актуальна, если речь идет о хомячках и рыбках — можно попросить друзей или знакомых, ну а если говорить об азиатской овчарке, количество желающих подменить хозяина, уменьшается до нуля. Именно для таких случаев и создана полуавтоматическая GSM-кормушка Codius.AutoFeeder v1.0 с управлением по SMS и обратной связью. |
|
02 | На заметку: |
В понимании работы данного устройства поможет серия статей и роликов о GSM/GPRS модуле SIM800L.
|
|
04 |
Данное устройство очень подробно описано в серии видеороликов: |
|
|
06 |
Компоненты Используемые в проекте компоненты: |
|
08 |
Количества и замечания: |
|
|
10 |
В случае выбора Arduino-шилда в качестве модуля реле, необходимо использовать плату Arduino Uno с MicroUSB-выводом питания, в противном случае разъем USB2.0 Type B будет мешать установке шилда. |
|
12 |
Основные команды управления Управление кормушкой происходит посредством отправки SMS-команд на номер GSM-модуля SIM800L в блоке управления. |
|
13 |
Полный список команд можно получить, отправив сообщение help или ? на номер кормушки. |
|
14 | На заметку: |
Команды можно вводить в любом регистре. Одинаково корректно будут исполнены команды OpEn1 и opeN1.
|
|
15 |
Полный список команд представлен в таблице: |
|
16 |
|
|
17 |
После каждого отправленного сообщения, модуль запрашивает баланс. В случае его уменьшения ниже заданного в коде уровня, дополняет сообщение о статусе информацией о балансе. |
|
18 |
Схема подключения В схеме необходимо обратить внимание на 2 интересных момента: |
|
20 |
Во-первых, для передачи состояния устройства с 4 датчиков используется всего 1 линия (пин A5 на Arduino). Этого удалось достичь, благодаря применению резистивно-последовательной схемы. Для лучшего информирования, в следующих версиях устройства эта часть будет заменена на резистивно-параллельную схему. |
|
21 |
Во-вторых, для борьбы с всплесками ЭДС самоиндукции электромагнитных переключателей в схеме используются высоковольтные мощные выпрямительные диоды 1N5408. |
|
22 |
Скетч Скетч подробно прокомментирован: |
|
23 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 //=====================================================
// Codius.AutoFeeder v1.01
// Kravchenko Viktor, codius.ru
// Project page - http://codius.ru/articles/256
//=====================================================
//#define DEBUG // Для режима отладки нужно раскомментировать эту строку
#ifdef DEBUG // В случае существования DEBUG
#define DEBUG_PRINT(x) Serial.print (x) // Создаем "переадресацию" на стандартную функцию
#define DEBUG_PRINTLN(x) Serial.println (x)
#else // Если DEBUG не существует - игнорируем упоминания функций
#define DEBUG_PRINT(x)
#define DEBUG_PRINTLN(x)
#endif
#include <SoftwareSerial.h> // Библиотека програмной реализации обмена по UART-протоколу на пинах отличных от 0 и 1
SoftwareSerial SIM800(9, 8); // RX, TX - Создаем класс
// Аппаратные настройки ===============================
int pinHallData = A5; // Пин состояния, сигнал с датчиков Холла
int pinHallPower = 12; // Пин питания датчиков Холла, используется для экономии электричества
// Питание для определения состояния будет подаваться, только в момент измерения
int pins[4] = {5, 4, 3, 2}; // Пины управления реле
int pinGreen = 6; // Пин подключения зеленого светодиода
int pinRed = 7; // Пин подключения красного светодиода
int pinGND = 11; // Пин подключения земли для светодиодов (для удобства подключения, все пины с землей заняты)
// Программные настройки ==============================
String phones = "+7928xxxxxxx;+7918xxxxxxx;+7920xxxxxxx"; // Список телефонов, которые могут управлять устройством. Первый номер - админ
int actualStatus = 0; // Переменная для хранения состояния устройства (0 - все закрыто, 4 - все открыто)
float balance = 0.0; // Переменная для хранения данных о балансе SIM-карты
float lowLevelBalance = 50.0; // Переменная порогового значения, при превышении - информирование о балансе
String tasks[10]; // Переменная для хранения списка задач к исполнению
bool executingTask = false; // Флаг исполнения отложенной задачи
long lastUpdate = 0; // Переменная хранящая время последней проверки
long updatePeriod = 90000; // 90 сек - Период автоматической проверки наличия сообщений (в миллисекундах, 1000 - 1 сек)
//=====================================================
void setup() {
for (int i = 0; i < 4; i++) { // Инициализируем пины реле
digitalWrite(pins[i], HIGH); // Так как эта модель реле управляется LOW-сигналом, чтобы не было ложного срабатывания ...
pinMode(pins[i], OUTPUT); // ...во время каждого включения, перед установкой режима пина, устанавливаем его значение
}
pinMode(pinHallData, INPUT); // Инициализируем сигнальный пин с датчиков Холла
pinMode(pinHallPower, OUTPUT); // Инициализируем пин питания датчиков Холла
digitalWrite(pinGND, LOW); // Инициализируем пины светодиодов индикации
pinMode(pinGND, OUTPUT);
pinMode(pinGreen, OUTPUT);
pinMode(pinRed, OUTPUT);
digitalWrite(pinRed, HIGH); // При старте мигаем обоими светодиодами
digitalWrite(pinGreen, HIGH);
delay(100);
digitalWrite(pinRed, LOW);
digitalWrite(pinGreen, LOW);
Serial.begin(9600); // Скорость обмена данными с компьютером
SIM800.begin(9600); // Скорость обмена данными с модемом
DEBUG_PRINTLN("Start!");
delay(5000); // Даем время модему завершить инициализацию
// Отправляем команды инициализации, если все в порядке мигает зеленый индикатор, если нет - красный
if (sendATCommand("AT", true).indexOf("OK") > -1) blinkOK(); else blinkFail(); // Команда готовности GSM-модуля
if (sendATCommand("AT+CLIP=1", true).indexOf("OK") > -1) blinkOK(); else blinkFail(); // Установка АОН
if (sendATCommand("AT+CMGF=1", true).indexOf("OK") > -1) blinkOK(); else blinkFail(); // Установка текстового режима SMS (Text mode)
sendATCommand("AT+CMGDA=\"DEL ALL\"", true); // Удаляем все сообщения, чтобы не забивали память МК
addTask("getBalance"); // Добавляем задачу - "Запрос баланса"
// Добавляем задачу - "Отправить SMS админу о включении устройства"
addTask("sendSMS;" + phones.substring(0, phones.indexOf(";")) + ";Init - OK.\r\nStatus: " + statusToString(actualStatus));
lastUpdate = millis() + 10000; // Ближайшая проверка через 10 сек
}
void blinkOK() { // Функция индикации OK (3 мигания зеленым индикатором)
for (int i = 0; i < 3; i++) {
blinkLed(pinGreen, 100);
delay(100);
}
}
void blinkFail() { // Функция индикации ошибки (длительное мигание красным)
blinkLed(pinRed, 1500);
}
void blinkLed(int pin, int _delay) { // Общая функция для мигания светодиодами
digitalWrite(pin, HIGH);
if (_delay > 0) delay(_delay);
digitalWrite(pin, LOW);
}
String sendATCommand(String cmd, bool waiting) { // Функция отправки AT-команды
blinkLed(pinGreen, 10); // Мигаем зеленым индикатором об отправке данных GSM-модулю
String _response = "";
DEBUG_PRINTLN(cmd); // Дублируем в Serial отправляемую команду
SIM800.println(cmd); // Отправляем команду модулю
if (waiting) { // Если нужно ждать ответ от модема...
_response = waitResponse(); // Результат ответа сохраняем в переменную
if (_response.startsWith(cmd)) { // Если ответ начинается с отправленной команды, убираем её, чтобы не дублировать
_response = _response.substring(_response.indexOf("\r\n", cmd.length()) + 2);
}
DEBUG_PRINTLN(_response); // Выводим ответ в Serial
return _response; // Возвращаем ответ
}
return ""; // Еси ждать ответа не нужно, возвращаем пустую строку
}
String waitResponse() { // Функция ожидания ответа от GSM-модуля
String _buffer; // Переменная для хранения ответа
long _timeout = millis() + 10000; // Таймаут наступит через 10 секунд
while (!SIM800.available() && millis() < _timeout) {}; //Ждем...
if (SIM800.available()) { // Если есть что принимать...
_buffer = SIM800.readString(); // ...принимаем
//DEBUG_PRINTLN("Ok - response");
return _buffer; // и возвращаем полученные данные
}
else { // Если таймаут вышел...
blinkLed(pinRed, 500); // ...мигаем красным светодиодом
//DEBUG_PRINTLN("Timeout...");
}
return ""; // и возвращаем пустую строку
}
bool hasmsg = false; // Флаг наличия сообщений к удалению
void loop() {
String _buffer = ""; // Переменная хранения ответов от GSM-модуля
if (millis() > lastUpdate && !executingTask) { // Цикл автоматической проверки SMS, повторяется каждые updatePeriod (90 сек)
do {
_buffer = sendATCommand("AT+CMGL=\"REC UNREAD\",1", true); // Отправляем запрос чтения непрочитанных сообщений
if (_buffer.indexOf("+CMGL: ") > -1) { // Если есть хоть одно, получаем его индекс
int msgIndex = _buffer.substring(_buffer.indexOf("+CMGL: ") + 7, _buffer.indexOf("\"REC UNREAD\"", _buffer.indexOf("+CMGL: "))).toInt();
char i = 0; // Объявляем счетчик попыток
do {
i++; // Увеличиваем счетчик
_buffer = sendATCommand("AT+CMGR=" + (String)msgIndex + ",1", true); // Пробуем получить текст SMS по индексу
_buffer.trim(); // Убираем пробелы в начале/конце
if (_buffer.endsWith("OK")) { // Если ответ заканчивается на "ОК"
getActionBySMS(_buffer); // Отправляем текст сообщения на обработку
if (!hasmsg) hasmsg = true; // Ставим флаг наличия сообщений для удаления
sendATCommand("AT+CMGR=" + (String)msgIndex, true); // Делаем сообщение прочитанным
break; // Выход из do{}
}
else { // Если сообщение не заканчивается на OK
blinkLed(pinRed, 500); // Мигаем красным светодиодом
//Serial.println ("Error answer");
}
sendATCommand("\n", true); // Перестраховка - вывод новой строки
} while (i < 10); // Пока попыток меньше 10
break;
}
else {
lastUpdate = millis() + updatePeriod; // Если все обработано, обновляем время последнего обновления
if (hasmsg) { // Если были сообщения для обработки
addTask("clearSMS"); // Добавляем задание для удаления сообщений
hasmsg = false; // Сбрасываем флаг наличия сообщений
}
break; // Выходим из цикла
}
} while (1);
}
if (millis() > lastUpdate + 180000 && executingTask) { // Таймаут на выполнение задачи - 3 минуты
//DEBUG_PRINTLN("ExTask-true!");
sendATCommand("\n", true);
executingTask = false; // Если флаг не был сброшен по исполению задачи, сбрасываем его принудительно через 3 минуты
}
if (SIM800.available()) { // Ожидаем прихода данных (ответа) от модема...
blinkLed(pinGreen, 50); // Данные пришли - мигаем зеленым светодиодом
String msg = waitResponse(); // Получаем ответ от модема для анализа
msg.trim(); // Убираем ненужные пробелы в начале/конце
DEBUG_PRINTLN(".. " + msg); // ...и выводим их в Serial
blinkLed(pinGreen, 50); // Мигаем зеленым светодиодом о приходе данных
if (msg.startsWith("+CUSD:")) { // Если USSD-ответ о балансе
String msgBalance = msg.substring(msg.indexOf("\"") + 2); // Парсим ответ
msgBalance = msgBalance.substring(0, msgBalance.indexOf("\n"));
balance = getDigitsFromString(msgBalance); // Сохраняем баланс
deleteFirstTask(); // Удаляем задачу
executingTask = false; // Сбрасываем флаг исполнения
DEBUG_PRINTLN("Balance: " + (String)balance); // Отчитываемся в Serial
}
else if (msg.startsWith("+CMGS:")) { // Результат отправки сообщения
deleteFirstTask(); // Удаляем задачу
executingTask = false; // Сбрасываем флаг исполнения
DEBUG_PRINTLN("SMS sending - task removed."); // Отчитываемся в Serial
addTask("getBalance"); // Добавляем задачу запроса баланса
}
else if (msg.startsWith("RING")) { // При входящем вызове
sendATCommand("ATH", true); // Всегда сбрасываем
}
else if (msg.startsWith("+CMTI:")) { // Незапрашиваемый ответ - приход сообщения
lastUpdate = millis(); // Сбрасываем таймер автопроверки наличия сообщений
}
else if (msg.startsWith("ERROR")) { // Ошибка исполнения команды
//DEBUG_PRINTLN("Error executing last command.");
executingTask = false; // Сбрасываем флаг исполнения, но задачу не удаляем - на повторное исполнение
}
}
if (!executingTask && tasks[0] != "") { // Если никакая задача не исполняется, и список задач не пуст, то запускаем выполнение.
showAllTasks(); // Показать список задач
String task = tasks[0];
if (tasks[0].startsWith("sendSMS")) { // Если задача - отправка SMS - отправляем
task = task.substring(task.indexOf(";") + 1);
executingTask = true; // Флаг исполнения в true
sendSMS(task.substring(0, task.indexOf(";")),
task.substring(task.indexOf(";") + 1));
}
else if ((tasks[0].startsWith("getBalance"))) { // Задача - запрос баланса
executingTask = true; // Флаг исполнения в true
sendATCommand("AT+CUSD=1,\"#102#\"", true); // Отправка запроса баланса
}
else if ((tasks[0].startsWith("clearSMS"))) { // Задача - удалить все прочитанные SMS
sendATCommand("AT+CMGDA=\"DEL READ\"", true); // Флаг исполнения не устанавливаем - здесь не нужно.
deleteFirstTask(); // Удаляем задачу, сразу после исполнения
}
else {
//DEBUG_PRINTLN("Error: unknown task - " + task);
}
}
}
void getActionBySMS(String msg) { // Функция получения действия из SMS
String msgheader = "";
String msgbody = "";
String msgphone = "";
// Парсим SMS, получаем телефон и текст
msg = msg.substring(msg.indexOf("+CMGR: "));
msgheader = msg.substring(0, msg.indexOf("\r"));
msgbody = msg.substring(msgheader.length() + 2);
msgbody = msgbody.substring(0, msgbody.lastIndexOf("OK"));
msgbody.trim();
int firstIndex = msgheader.indexOf("\",\"") + 3;
int secondIndex = msgheader.indexOf("\",\"", firstIndex);
msgphone = msgheader.substring(firstIndex, secondIndex);
msgbody.toLowerCase();
if (msgphone.length() > 10 && phones.indexOf(msgphone) > -1) { // Если номер присутствует в списке номеров
String result = ""; // Обрабатываем это сообщение
if (msgbody.startsWith("open")) { // Если команда "открыть ячейку",
int blockNum = 0; // Открываем с использованием защитных механизмов
if (msgbody.length() > 4) {
blockNum = msgbody.substring(4).toInt();
if (blockNum > 4 || blockNum < 1) blockNum = -1;
}
int blockToOpen = -1;
int block = blockNum;
actualStatus = getStatus(); // Получаем актуальный статус
if (blockNum == -1) { // Ошибочно указан номер ячейки
result = "X - Err: " + msgbody.substring(4, 7);
}
else if (blockNum == 0) { // Номер блока не указан - автоматическое открывание
if (actualStatus >= 4) { // Если уже все открыто, то сообщаем об этом
result = "X";
}
else {
blockToOpen = actualStatus;
result = "OK auto: " + (String)(actualStatus + 1);
}
}
else { // Открывание по номеру ячейки
if (blockNum != actualStatus + 1) {
if (blockNum < actualStatus + 1) {
result = "X:" + (String)block;
}
else {
result = "X:" + (String)block;
}
}
else {
blockToOpen = actualStatus;
result = "OK: " + (String)block;
}
}
//DEBUG_PRINTLN("blockToOpen: " + (String)blockToOpen);
if (blockToOpen >= 0 && blockToOpen < 4) { // Если номер ячейки в допустимых пределах
digitalWrite(pins[blockToOpen], LOW); // Включаем электромагнитный переключатель на 1 секунду
delay(1000);
digitalWrite(pins[blockToOpen], HIGH);
}
actualStatus = getStatus(); // Получаем актуальный статус
result += "\r\nStatus: " + statusToString(actualStatus); // Записываем его в переменную с результатом
if (balance < lowLevelBalance) { // Если баланс ниже заданного предела, добавляем информацию в результат
result += "\r\nBalance: " + (String)balance;
}
//DEBUG_PRINTLN("result: " + result);
addTask(getSendSMSTaskString(msgphone, result)); // Добавляем задачу отправки SMS о статусе
addTask("getBalance"); // Добавляем задачу запроса баланса
showAllTasks(); // Выводим список задач
}
else if (msgbody.startsWith("force")) { // Команда грубого открытия ячейки
int blockNum = 0;
if (msgbody.length() > 5) {
blockNum = msgbody.substring(5).toInt();
if (blockNum > 4 || blockNum < 1) blockNum = -1;
}
if (blockNum > 0) {
for (int i = 0; i < 4; i++) {
digitalWrite(pins[blockNum - 1], LOW);
delay(200);
digitalWrite(pins[blockNum - 1], HIGH);
delay(700);
}
}
}
else if (msgbody.startsWith("status")) { // Команда запроса статуса
result = sendATCommand("AT+CSQ", true); // Добавляем информацию о качестве сигнала
result = result.substring(0, result.indexOf("\n"));
actualStatus = getStatus(); // Получаем статус
//DEBUG_PRINTLN("Status: " + statusToString(actualStatus));
addTask(getSendSMSTaskString(msgphone, "Status: " + statusToString(actualStatus) + "\n" + result)); // Добавляем задачу об отправке SMS со статусом
addTask("getBalance"); // Добавляем задачу о запросе баланса
showAllTasks(); // Выводим все задачи
}
else if (msgbody.startsWith("balance")) { // Команда запроса баланса
addTask(getSendSMSTaskString(msgphone, "Balance: " + String(balance)));// Добавляем задачу об отправке SMS с балансом
addTask("getBalance"); // Добавляем задачу о запросе баланса
showAllTasks(); // Выводим все задачи
}
else if (msgbody.startsWith("callme")) { // Команда осуществить исходящий вызов
sendATCommand("ATD" + msgphone + ";", true);
}
else if (msgbody.startsWith("test")) { // Команда тест - открываем поочереди все ячейки
for (int i = 0; i < 4; i++) {
digitalWrite(pins[i], LOW);
delay(500);
digitalWrite(pins[i], HIGH);
delay(1000);
}
}
else if (msgbody == "?" || msgbody == "help") { // Команда получения помощи по командам
// String task=getSendSMSTaskString(msgphone, getHelpSMS());
// DEBUG_PRINTLN("task: " + task);
// addTask(task);
addTask(getSendSMSTaskString(msgphone, getHelpSMS()));
addTask("getBalance");
showAllTasks();
}
else if (msgbody.startsWith("checknow")) { // Обнулить таймер периодической проверки - проверить сразу
lastUpdate = millis();
}
}
else {
//DEBUG_PRINTLN("Unknown phonenumber");
}
}
String getHelpSMS() { // Текст сообщения с помощью по камандам
return "Balance, CallMe, Open, Open[1-4], Status, Test, Force";
}
String getSendSMSTaskString( String phone, String msg) { // Формируем строку задачи отправки SMS
return "sendSMS;" + phone + ";" + msg;
}
// =================================== Tasks =========================================
void showAllTasks() { // Показать все задачи
DEBUG_PRINTLN("All Tasks:");
for (int i = 0; i < 10; i++) {
if (tasks[i] == "") break;
DEBUG_PRINTLN("Task " + (String)(i + 1) + ": " + tasks[i]);
}
}
void deleteFirstTask() { // Удалить первую задачу, остальные передвинуть вверх на 1
for (int i = 0; i < 10 - 1; i++) {
tasks[i] = tasks[i + 1];
if (tasks[i + 1] == "") break;
}
}
void addTask(String task) { // Добавить задачу в конец очереди
for (int i = 0; i < 10; i++) {
if (tasks[i] == task && (task == "clearSMS" || task == "getBalance")) {
DEBUG_PRINTLN("Task already exists " + (String)(i + 1) + ": " + task);
return;
}
if (tasks[i] == "") {
tasks[i] = task;
DEBUG_PRINTLN("Task " + (String)(i + 1) + " added: " + task);
return;
}
}
DEBUG_PRINTLN("Error!!! Task NOT added: " + task);
}
void sendSMS(String phone, String message) // Функция отправки SMS
{
sendATCommand("AT+CMGS=\"" + phone + "\"", true); // Переходим в режим ввода текстового сообщения
sendATCommand(message + "\r\n" + (String)((char)26), true); // После текста отправляем перенос строки и Ctrl+Z+
}
float getDigitsFromString(String str) { // Функция выбора цифр из сообщения - для парсинга баланса из USSD-запроса
bool flag = false;
String digits = "0123456789.";
String result = "";
for (int i = 0; i < str.length(); i++) {
char symbol = char(str.substring(i, i + 1)[0]);
if (digits.indexOf( symbol) > -1) {
result += symbol;
if (!flag) flag = true;
}
else {
if (flag) break;
}
}
return result.toFloat();
}
// ================================== Status ============================================
int getStatus() { // Получаем статус системы
digitalWrite(pinHallPower, HIGH); // Подаем питание на датчики Холла
//delay(200);
int tmpStatus = actualStatus; // Временная переменная для статуса
int count = 0; // Счетчик
do {
int newStatus = analogRead(pinHallData); // Получаем актуальное состояние
newStatus = ((newStatus - 128) >> 8) + 1; // Округляем для устранения погрешности АЦП
if (tmpStatus != newStatus) { // Пришло значение отличное от предыдущего
count = 0; // Все обнуляем и начинаем считать заново
tmpStatus = newStatus; // Запоминаем новое значение
}
else { // Если состояние такое же, то
count += 1; // Увеличиваем счетчик
}
} while (count < 50); // Если состояние одинаково 10 замеров подряд, значит оно устоявшееся и...
digitalWrite(pinHallPower, LOW); // Отключаем питание датчиков Холла
return (4 - tmpStatus); // ...его можно возвращать
}
String statusToString(int stat) { // Статус в строку для отправки в SMS ("2" -> "OO--")
String result = "";
for (int i = 0; i < 4; i++) {
result += i < stat ? "O" : " -";
}
return result;
} |
|
24 |
Что почитать: |
|
25 |
Похожие запросы:
|
|