Очередная теплица (для комментариев)

brash_o
Offline
Зарегистрирован: 27.04.2015

Добрый день.

Это мой перый более менее серьезный проект, после обучающих уроков.

Нарисовал схему и набросал программу для теплицы, хотел бы услышать комментарии от более опытных людей.

//EEPROM 0 Минимальное значение температуры актуатора
//EEPROM 1 Максимальное значение температуры актуатора
//EEPROM 2 включен ли полив 1 канала
//EEPROM 3 день включения полива 1 канала
//EEPROM 4 час включения полива 1 канала
//EEPROM 5 минута включения полива 1 канала
//EEPROM 6 время полива 1 канала
//EEPROM 7 включен ли полив 2 канала
//EEPROM 8 день включения полива 2 канала
//EEPROM 9 час включения полива 2 канала
//EEPROM 10 минута включения полива 2 канала
//EEPROM 11 время полива 2 канала
//EEPROM 12 месяц последнего полива 1 канала
//EEPROM 13 день следующего полива 1 канала
//EEPROM 14 месяц последнего полива 2 канала
//EEPROM 15 день следующего полива 2 канала
#define LEAP_YEAR(Y)     ( ((1970+Y)>0) && !((1970+Y)%4) && ( ((1970+Y)%100) || !((1970+Y)%400) ) )

const char *monthName[12] = {
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
#include <OneWire.h>
#include <LiquidCrystal.h>
#include <DS1307RTC.h>
#include <TimeLib.h>
#include <Wire.h>
#include <EEPROM.h>
#include <TimerOne.h>

#define MENUITERATIONCOUNT 20

#define LCD_RS 7
#define LCD_E 6
#define LCD_DB4 5
#define LCD_DB5 4
#define LCD_DB6 3
#define LCD_DB7 2
#define TERMCONN 12
#define UPBUTTON 13
#define DOWNBUTTON A0 //14
#define OKBUTTON A1 //15
#define OPENACTUATOR1 11
#define CLOSEACTUATOR1 10
#define STATUSACTUATOR1 A6 //16
#define POWERLINEACTUATOR1 0
#define OPENACTUATOR2 9
#define CLOSEACTUATOR2 8
#define STATUSACTUATOR2 A7 //17
#define POWERLINEACTUATOR2 1
#define IRRCHANEL1 A2 //20
#define IRRCHANEL2 A3 //21
#define SCLRTC A5 //19
#define SDARTC A4 //18

LiquidCrystal lcd(LCD_RS, LCD_E, LCD_DB4, LCD_DB5, LCD_DB6, LCD_DB7); // (RS, E, DB4, DB5, DB6, DB7)
OneWire  ds(TERMCONN);
bool lcdActive = true;
long previousMillis = 0;        // храним время для отключения экрана
long interval_2 = 10000; //задержка подсветки
bool _actuatorOpen = false;

tmElements_t tm; // переменная в которой будет хранится дата и время
/*
   по умолчанию ставятся значения month = 0 day =0
*/
int del = 500; // переменная ожидания между выборами меню(время горения ledpin в милисек)
byte addr[8]; //адресс термодатчика
byte type_s = 0; // тип термодатчика
volatile float _currenttemp = 0;
volatile int _iterationnumber = -1;

void setup() {
  pinMode(UPBUTTON, INPUT);
  digitalWrite(UPBUTTON, HIGH);

  pinMode(DOWNBUTTON, INPUT);
  digitalWrite(DOWNBUTTON, HIGH);

  pinMode(OKBUTTON, INPUT);
  digitalWrite(OKBUTTON, HIGH);

  pinMode(STATUSACTUATOR1, INPUT);
  digitalWrite(STATUSACTUATOR1, HIGH);

  pinMode(STATUSACTUATOR2, INPUT);
  digitalWrite(STATUSACTUATOR2, HIGH);

  pinMode(POWERLINEACTUATOR1, INPUT);
  digitalWrite(POWERLINEACTUATOR1, HIGH);
  pinMode(POWERLINEACTUATOR1, OUTPUT);

  pinMode(POWERLINEACTUATOR2, INPUT);
  digitalWrite(POWERLINEACTUATOR2, HIGH);
  pinMode(POWERLINEACTUATOR2, OUTPUT);

  pinMode(OPENACTUATOR1, INPUT);
  digitalWrite(OPENACTUATOR1, HIGH);
  pinMode(OPENACTUATOR1, OUTPUT);

  pinMode(CLOSEACTUATOR1, INPUT);
  digitalWrite(CLOSEACTUATOR1, HIGH);
  pinMode(CLOSEACTUATOR1, OUTPUT);

  pinMode(OPENACTUATOR2, INPUT);
  digitalWrite(OPENACTUATOR2, HIGH);
  pinMode(OPENACTUATOR2, OUTPUT);

  pinMode(CLOSEACTUATOR2, INPUT);
  digitalWrite(CLOSEACTUATOR2, HIGH);
  pinMode(CLOSEACTUATOR2, OUTPUT);

  pinMode(IRRCHANEL1, INPUT);
  digitalWrite(IRRCHANEL1, HIGH);
  pinMode(IRRCHANEL1, OUTPUT);

  pinMode(IRRCHANEL2, INPUT);
  digitalWrite(IRRCHANEL2, HIGH);
  pinMode(IRRCHANEL2, OUTPUT);

  analogReference(DEFAULT);
  lcd.begin(16, 2);                  // Задаем размерность экрана
  printMessage("Init. Wait", 0);
  printMessage("V 1.0", 1);

  //инициализация датчика температуры
  if ( !ds.search(addr)) {  //Выполняет поиск следующего 1-Wire устройства
    lcd.clear();
    printMessage("No more addr.", 0);
    return;
  }
  if (OneWire::crc8(addr, 7) != addr[7]) {  //Вычисляет CRC код байтов из массива dataArray, длиной length
    lcd.clear();
    printMessage("CRC don't valid", 0);
    return;
  }
  if (addr[0] != 0x10 && addr[0] != 0x22  && addr[0] != 0x28 )
  {
    lcd.clear();
    printMessage("No temp probe", 0);
  }
  if (addr[0] == 0x10)
    type_s = 1; //Тип датчика DS18S20

  //end инициализация датчика температуры

  // инициализация часов реального времени

  if (!RTC.read(tm))
  {
    getDate(__DATE__);
    getTime(__TIME__);
    RTC.write(tm);
  }
  //проверка и установка времени
  setDateTime(tm);
  //end инициализация часов реального времени

  lcd.clear();
  //Сброс даты следующего полива для канала 1
  EEPROM.write(12, 255);
  EEPROM.write(13, 255);
  //Сброс даты следующего полива для канала 2
  EEPROM.update(14, 255);
  EEPROM.update(15, 255);
  ActuatorSetUp();
  IrregationSetUp(0); // 1 канал полива
  IrregationSetUp(1);// 2 канал полива
  Timer1.initialize(30000000); // 300 000 000 = 5 мин, 60 000 000 = 1 мин, 1 000 000 = 1 сек
  Timer1.attachInterrupt(Check);
}

void Check()
{
  Timer1.stop();
  printMessage("Start check", 0);
  _currenttemp = GetTemp();
  printMessage("temp = ", 1);
  lcd.print(_currenttemp);
  RTC.read(tm);
  if (tm.Hour >= 21 || tm.Hour <= 6)
  {
    if (_actuatorOpen)
    {
      termostat ((float)EEPROM.read(0) - 10.0);
      _actuatorOpen = CheckOpenActuator(STATUSACTUATOR1, POWERLINEACTUATOR1) && CheckOpenActuator(STATUSACTUATOR2, POWERLINEACTUATOR2);
    }
  }
  else
  {
    _iterationnumber = _iterationnumber + 1;
    if (_iterationnumber > 3)
      _iterationnumber = 0;
    if (_iterationnumber == 0)
    {
      termostat (_currenttemp);
      _actuatorOpen = true;
    }
  }

  irrigation(0);
  irrigation(1);
  printMessage((String)tm.Month + "/" + (String)tm.Day + " " + (String)tm.Hour + ":" + (String)tm.Minute, 0);
  Timer1.start();
}

void loop() {
  if (digitalRead(OKBUTTON)) {
    delay(del);
    previousMillis = millis();
    lcdActive = true;
    lcd.display();
    ShowMenu();
  }
  if (digitalRead(UPBUTTON)) {
    delay(del);
    previousMillis = millis();
    lcdActive = true;
    lcd.display();
  }
  if (digitalRead(DOWNBUTTON)) {
    delay(del);
    previousMillis = millis();
    lcdActive = true;
    lcd.display();
  }
  if (lcdActive)
    LCD_LED_OFF(); // отключение экрана по таймеру
}

float GetTemp()
{
  byte present = 0;
  byte data[12];
  present = ds.reset(); //Выполняет сброс шины, необходимо перед связью с датчиком
  ds.select(addr);    //Выполняет выбор устройства после сброса, передается ROM Код устройства.
  ds.write(0x44); //провести измерение температуры и записать данные в оперативную память
  delay(1500);
  present = ds.reset();
  ds.select(addr);
  ds.write(0xBE);// считать последовательно 9 байт оперативной памяти

  for (int i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = ds.read();//Считывает информационный байт с устройства
  }
  int16_t raw = (data[1] << 8) | data[0];
  if (type_s) {
    raw = raw << 3; // 9 bit resolution default
    if (data[7] == 0x10) {
      // "count remain" gives full 12 bit resolution
      raw = (raw & 0xFFF0) + 12 - data[6];
    }
  } else {
    byte cfg = (data[4] & 0x60);
    // at lower res, the low bits are undefined, so let's zero them
    if (cfg == 0x00) raw = raw & ~7;  // 9 bit resolution, 93.75 ms
    else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
    else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
    //// default is 12 bit resolution, 750 ms conversion time
  }
  return (float)raw / 16.0;
}

void printMessage(String message, int row)
{
  lcd.setCursor(0, row);
  lcd.print(message);
}

bool getDate(const char *str)
{
  char Month[12];
  int Day, Year;
  uint8_t monthIndex;

  if (sscanf(str, "%s %d %d", Month, &Day, &Year) != 3) return false;
  for (monthIndex = 0; monthIndex < 12; monthIndex++) {
    if (strcmp(Month, monthName[monthIndex]) == 0) break;
  }
  if (monthIndex >= 12) return false;
  tm.Day = Day;
  tm.Month = monthIndex + 1;
  tm.Year = CalendarYrToTm(Year);
  return true;
}

bool getTime(const char *str)
{
  int Hour, Min, Sec;

  if (sscanf(str, "%d:%d:%d", &Hour, &Min, &Sec) != 3) return false;
  tm.Hour = Hour;
  tm.Minute = Min;
  tm.Second = Sec;
  return true;
}

void setDateTime(tmElements_t date)
{
  date.Year = CalendarYrToTm(setDate(tmYearToCalendar(date.Year), 1, "Time"));
  date.Month = setDate(date.Month, 2, "Time");
  date.Day = setDate(date.Day, 3, "Time");
  date.Hour = setDate(date.Hour, 4, "Time");
  date.Minute = setDate(date.Minute, 5, "Time");
  date.Second = 0;
  RTC.write(date);
}

int setDate(int data, const int dataType, String mode)
{
  String showingdata = "";
  switch (dataType)
  {
    case 1: showingdata = "Year"; break;
    case 2: showingdata = "Month"; break;
    case 3: showingdata = "Day"; break;
    case 4: showingdata = "Hours"; break;
    case 5: showingdata = "Min"; break;
    case 6: showingdata = "Temp"; break;
    case 7: showingdata = "On/Off"; break;
  }

  lcd.clear(); //очищаем экран
  int x = 0;
  unsigned long _previousMillis = millis();
  while (1)
  { 
    if (x > MENUITERATIONCOUNT) {
      break;
    }
    printMessage("SET " + showingdata + " " + mode, 0);
    printMessage("-1 " + (String)data + " +1", 1);
    while (millis() - _previousMillis < del)
    {
      if (digitalRead(OKBUTTON)) {
        delay(del);
        lcd.clear();
        return data;
      }
      if (digitalRead(UPBUTTON)) {
        delay(del);
        lcd.clear();
        x = 0;
        data = data + 1;
        data = CheckData(data, dataType);
      }
      if (digitalRead(DOWNBUTTON)) {
        delay(del);
        lcd.clear();
        x = 0;
        data = data - 1;
        data = CheckData(data, dataType);
      }
    }
    _previousMillis = millis();
    x++;
  }
  return data;
}

int CheckData(int data, int dataType)
{
  switch (dataType)
  {
    case 2: //month
      {
        if (data > 12)
        {
          return 1;
        }
        if (data < 1)
        {
          return 12;
        }
        break;
      }
    case 3: // day
      {
        if (data > 31)
        {
          return 1;
        }
        if (data < 1)
        {
          return 31;
        }
        break;
      }
    case 4: // hours
      {
        if (data > 23)
        {
          return 0;
        }
        if (data < 0)
        {
          return 23;
        }
        break;
      }
    case 5://minutes
      {
        if (data > 59)
        {
          return 0;
        }
        if (data < 0)
        {
          return 59;
        }
        break;
      }
    case 6: // temperature
      {
        if (data > 50)
        {
          return 50;
        }
        if (data < 15)
        {
          return 15;
        }
        break;
      }
    case 7://on/off
      {
        if (data < 0) return 1;
        if (data > 1) return 0;
        break;
      }
  }
  return data;

}

void LCD_LED_OFF()//таймер отключения подсветки
{
  unsigned long currentMillis = millis();
  //проверяем не прошел ли нужный интервал, если прошел то
  if (currentMillis - previousMillis > interval_2) {
    // сохраняем время последнего переключения
    previousMillis = currentMillis;
    lcd.noDisplay();
    lcdActive = false;
  }
}

void ShowMenu()
{
  printMessage("Press show menu", 0);
  int x = 0;
  unsigned long _previousMillis = millis();
  while (1)
  { //бесконечный цикл
    if (x > MENUITERATIONCOUNT) {
      break;
    }
    while (millis() - _previousMillis < del)
    {
      if (digitalRead(OKBUTTON)) {
        delay(del);
        ActuatorSetUp();
        IrregationSetUp(0); // 1 канал полива
        IrregationSetUp(1);// 2 канал полива
        break;
      }
    }
    _previousMillis = millis();
    x++;
  }
}

//настройка открывания форточек
void ActuatorSetUp()
{
  int minTemp = EEPROM.read(0);
  if (minTemp == 255)
  {
    minTemp = 25;
  }
  EEPROM.update(0, setDate(minTemp, 6, "close"));
  minTemp = 255;
  minTemp = EEPROM.read(1);
  if (minTemp == 255)
  {
    minTemp = 25;
  }
  EEPROM.update(1, setDate(minTemp, 6, "open"));
}

//настройка полива
void IrregationSetUp(int _chanelNum)
{
  int offset = 0;
  if (_chanelNum == 1) offset = 5;
  //on/off 1 chanel
  int minTemp = EEPROM.read(2 + offset);
  if (minTemp == 255)
  {
    minTemp = 1;
  }
  EEPROM.update(2 + offset, setDate(minTemp, 7, (String)(_chanelNum + 1) + "Irr2"));
  if (minTemp == 0) return;

  //day interval
  minTemp = 255;
  minTemp = EEPROM.read(3 + offset);
  if (minTemp == 255)
  {
    minTemp = 1;
  }
  EEPROM.update(3 + offset, setDate(minTemp, 3, (String)(_chanelNum + 1) + "Irr 3"));

  //hour on
  minTemp = 255;
  minTemp = EEPROM.read(4 + offset);
  if (minTemp == 255)
  {
    minTemp = 19;
  }
  EEPROM.update(4 + offset, setDate(minTemp, 4, (String)(_chanelNum + 1) + "Irr 4"));

  //min on
  minTemp = 255;
  minTemp = EEPROM.read(5 + offset);
  if (minTemp == 255)
  {
    minTemp = 0;
  }
  EEPROM.update(5 + offset, setDate(minTemp, 5, (String)(_chanelNum + 1) + "Irr 5"));

  //on time
  minTemp = 255;
  minTemp = EEPROM.read(6 + offset);
  if (minTemp == 255)
  {
    minTemp = 5;
  }
  EEPROM.update(6 + offset, setDate(minTemp, 5, (String)(_chanelNum + 1) + "Irr 6"));
}

int termostat (float currentTemp)
{
  int temp_min = EEPROM.read(0);
  int temp_max = EEPROM.read(1);
  if (currentTemp > temp_max)
  {
    openClosewindows(true);
  }
  else if (currentTemp < temp_min)
  {
    openClosewindows(false);
  }
}

void openClosewindows(bool open)
{
  OpenActuatorByNum(OPENACTUATOR1, STATUSACTUATOR1, open, POWERLINEACTUATOR1);
  OpenActuatorByNum(OPENACTUATOR2, STATUSACTUATOR2, open, POWERLINEACTUATOR2);
}

//преобразование значения в состояние форточки
int getStatus(int currentValue)
{
  if (currentValue < 320)
    return 0;
  else if (currentValue >= 320 && currentValue < 660)
    return 1;
  else if (currentValue >= 660 && currentValue < 900)
    return 2;
  else return 3;
}

void OpenActuatorByNum(int portNumber, int statusNumber, bool open, int powerline)
{
  //незабыть удалить значение для отладки при симуляции
  float devide = 2.7;
  digitalWrite(powerline, HIGH);
  int statusActuator = getStatus (analogRead(statusNumber) * devide);// / 341;
  if ((statusActuator == 3 && open) || (statusActuator == 1 && !open ))
  {
    digitalWrite(powerline, LOW);
    return;
  }
  int endstatus = statusActuator;
  if (open)
    endstatus = endstatus + 1;
  else
    endstatus = endstatus - 1;
  {
    while (1)
    {
      activateActuator(portNumber);
      statusActuator = getStatus (analogRead(statusNumber) * devide);// 341;
      if (statusActuator == endstatus) {
        break;
      }
    }
  }
  digitalWrite(powerline, LOW);
}

// проверка что рама закрыта
bool CheckOpenActuator(int statusNumber, int powerline)
{
  //незабыть удалить значение для отладки при симуляции
  float devide = 2.7;
  digitalWrite(powerline, HIGH);
  int statusActuator = getStatus (analogRead(statusNumber) * devide);// / 341;
  digitalWrite(powerline, LOW);
  return statusActuator > 1;
}

void activateActuator(int portNumber)
{
  digitalWrite(portNumber, HIGH);
  delay (2000);
  digitalWrite(portNumber, LOW);
}

void irrigation(int _chanelNum)
{
  if (_chanelNum != 0 && _chanelNum != 1) return;
  RTC.read(tm);
  int offset = 0;
  int offset2 = 0;
  if (_chanelNum == 1)
  { offset = 5;
    offset2 = 2;
  }
  int x = EEPROM.read(2 + offset);
  // ON irregation
  if (x == 1) {
    //считаение следующей даты полива
    int month = EEPROM.read(12 + offset2);
    if (month == 255 || month > 12)
    {
      month = tm.Month;
    }
    int day = EEPROM.read(13 + offset2);
    if (day == 255 || day > 31)
    {
      day = tm.Day;
    }
    int hour = EEPROM.read(4 + offset);
    int min = EEPROM.read(5 + offset);
    //проверям что дата полива наступила
    if (tm.Month == month && tm.Day == day &&
        (
          (tm.Hour == hour && tm.Minute >= min) ||
          (tm.Hour > hour && tm.Minute < min)
        )
       )
    {
      if (_chanelNum == 0)
        IrrActivate(EEPROM.read(6), IRRCHANEL1);
      else if (_chanelNum == 1)
        IrrActivate(EEPROM.read(6 + offset), IRRCHANEL2);
      else return;
      //сдвигаем дату следующего полива на количество дней из настройки
      int daycount = EEPROM.read(3 + offset);
      day = day + daycount;
      switch (month)
      {
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12:
          {
            if (day > 31)
            {
              day = 1;
              month = month + 1;
            }
          }
          break;
        case 4:
        case 6:
        case 9:
        case 11:
          {
            if (day > 30)
            {
              day = 1;
              month = month + 1;
            }
            break;
          }
        case 2:
          {
            if ((LEAP_YEAR(tm.Year) && day > 29) || (!LEAP_YEAR(tm.Year) && (day > 28)))
            {
              day = 1;
              month = month + 1;
            }
            break;
          }
      }
      //сохраняем следующую дату полива
      EEPROM.update(12 + offset2, month);
      EEPROM.update(13 + offset2, day);
    }
  }
}

void IrrActivate(int irrmin, int irrchanel)
{
  digitalWrite(irrchanel, HIGH);
  delay(1000.0 * 60.0 * (long)irrmin);
  digitalWrite(irrchanel, LOW);
}

Схема:

Вместо транзисторов BC546BP собираюсь использовать КТ315, TIP31 - КТ817, TIP32 - КТ816, которых осталось после школы целые коробки

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Собственно, чего именно вы хотите услышать? Вопросов, критики, похвал?

Похвал - пожалуйста: похвально, что взялись за такой проект, сам подобный делаю, и финиш ещё ооочень нескоро: https://github.com/Porokhnya/GreenhouseProject

Критики: да особо никакой, каждый ССЗБ. Только учтите, что тонких мест в этом деле - гора. Например: часы 1307 - фтопку, лучше 3231, т.к. 1307 сильно убегают. Датчик температуры один? Маловато для теплицы, имхо. Что будете делать, если кол-во актуаторов или каналов полива захочется увеличить? Плодить переменные или дефайны?

Дружеский совет - сразу разносите код на логические единицы, в разные файлы. Например: код для работы с часами - в один файл, код для работы с LCD - в другой. Иначе очень быстро потонете в длиннющей простыне.

Повторюсь: начинание годное, удачи!

brash_o
Offline
Зарегистрирован: 27.04.2015

Добрый день.

Скорее критику и замечания по схеме и/или по коду. Насчет часов спасибо за совет не знал.

Теплица маленькая 3 на 2 метра думаю 1 датчика температуры хватит, да и грядок в ней всего 2.

vde69
Offline
Зарегистрирован: 10.01.2016

у меня есть вопрос к бывалым:

какие датчики влажности почвы реально работают годами??? а то я не верю, что те , что на тексталите не закиснут через пару месяцев....

vde69
Offline
Зарегистрирован: 10.01.2016

brash_o пишет:

Добрый день.

Скорее критику и замечания по схеме и/или по коду. Насчет часов спасибо за совет не знал.

Теплица маленькая 3 на 2 метра думаю 1 датчика температуры хватит, да и грядок в ней всего 2.

 

еще критика:

для теплицы время вообще не нужно, нужед датчик освещенности (по нему можно определять в том числе и ПРАВИЛЬНОЕ время суток), датчик уличной температуры, датчик внутренеей температуры и датчик влажности воздуха (что-бы не допускать выпадение росы), ну и про полив - нужен датчик температуры воды....

brash_o
Offline
Зарегистрирован: 27.04.2015

vde69 пишет:

у меня есть вопрос к бывалым:

какие датчики влажности почвы реально работают годами??? а то я не верю, что те , что на тексталите не закиснут через пару месяцев....

 

Те которые на текстолите разрушаются за месяц где то. Видел народ делал из пары электродов такие датчики (например тут описание http://oldoctober.com/ru/humidity_sensor/)

brash_o
Offline
Зарегистрирован: 27.04.2015

vde69 пишет:

brash_o пишет:

Добрый день.

Скорее критику и замечания по схеме и/или по коду. Насчет часов спасибо за совет не знал.

Теплица маленькая 3 на 2 метра думаю 1 датчика температуры хватит, да и грядок в ней всего 2.

 

еще критика:

для теплицы время вообще не нужно, нужед датчик освещенности (по нему можно определять в том числе и ПРАВИЛЬНОЕ время суток), датчик уличной температуры, датчик внутренеей температуры и датчик влажности воздуха (что-бы не допускать выпадение росы), ну и про полив - нужен датчик температуры воды....

Спасибо за совет, в следующей версии добавлю датчики влажности и внешней температуры

Duino A.R.
Offline
Зарегистрирован: 25.05.2015

vde69 пишет:

какие датчики влажности почвы реально работают годами???


Здесь на форуме видел описание беспроводного датчика влажности. Судя по фото, электроды были из грифелей для цанговых карандашей. Диаметр около 2 мм. Думаю, высокая стойкость будет и у электродов из угольных стержней от батареек. Многократно видел под открытым небом разложившиеся "в кашу" батарейки с целыми угольными электродами.