티스토리 뷰

반응형

집안 베란다에 언제 부턴가 푸르르던 화분들이 시들어간다. 바쁘다는 핑계로 관심을 조금씩 쏟지 못했더니 이런 일들이 벌어졌는데 막상 잘 키워 보겠다는 각오를 했지만 베란다에 내놓고 일주일 한번 물준다면 다행인 것 같다. 지금이야 베란다에서 자라는 식물에 불과하지만 점점 나만의 가든이 베란다를 벗어나 거실안까지 들어오는 날이 멀지 않았다고 본다면, 내가 관심을 못 갖을 때도 잘 자랄 수 있게 관리되게 하는 솔루션이 필요할 것 같다.

<시나리오>

화분의 토양의 수분이 부족하면 알림을 오게 하자.

시나리오는 간단한데, Trigger가 화분 속의 토양 수분상태이고, Action은 Notification 이다. 알림은 문자로 올 수도 있고 메일로 올 수도 있지만 상세하게 Activity를 나열해보면 이렇다.

1) 토양 수분 측정

2) 토양 수분을 Cloud 서버에 update

3) Cloud 서버에서 등록된 사용자에게 알림 전송

오늘은 토양수분 측정 센서를 이용해서 화분 속의 수분 상태를 측정해보도록 하자.

# 토양 수분 측정 센서

토양 수분 측정센서는 DFRobot에서 만든 SKU:SEN0193 으로 아래 페이지에서 자세한 정보를 찾아볼 수 있다.

http://www.dfrobot.com/wiki/index.php?title=Capacitive_Soil_Moisture_Sensor_SKU:SEN0193

3Pin 으로 인터페이스가 구성되어 있으며 Analog Output 내기 때문에 ADC를 이용해서 값을 읽고, 3.3V~5V를 공급해 준다. 또한 일정 부분까지 깊숙히 화분의 토양에 넣어야 하는데 자세히 보면 다음과 같이 녹색선 안쪽으로 오게 해야하며 방수되는 제품이 아니므로 빨간선이 넘도록 하면 된다.

다만, 최초 측정시 토양이 아닌 외부에 공기에 전체 노출 됬을 때 값과, 물에 완전히 잠겼을 때 값이 미리 구해놓고, Dry, Wet, Very Wet 세단계로 구성해놓는다. 회로를 먼저 구성해야하는데 RaspberryPI에는 ADC가 없기 때문에 별도의 ADC를 사용해야한다. ADC는 MCP3008, 3208 등 여러종류가 있지만 MCP3008 를 사용하기로 한다. MCP3008과 RaspberryPI는 SPI 통신으로 ADC 된 값을 넘겨준다.

#MCP3008

Datasheet : https://cdn-shop.adafruit.com/datasheets/MCP3008.pdf

MCP3008 ADC의 경우 8 채널을 가진 10 bit ADC 이다. 따라서 ADC 채널에서 입력된 아나로그 데이터를 SPI로 받을 때는 10bit 레지스터 값을 계산해서 읽어야 하는데 3바이트를 버퍼로 할당해서 첫번째 8비트는 비어있고 두번째 8비트 1, 2 번째 비트는 ADC 데이터의 1, 2 번째 비트고 세번째 8 bit는 하위 8 bit를 의미함으로 이를 더함으로써 10bit의 데이터를 완성한다.

datasheet에 있는 레지스터 그림이다. 이를 보면 좀더 이해가 쉬울 것이다. 코드로 나타내보면, 아래와 같이 shift 연산을 할 수 있다. 

result = data[1] & 0x03;
result <<= 8;
result += data[2];

그럼 ADC Channel 0 에 토양센서 analog data 신호 선을 꽂고, ADC와 라즈베리는 SPI0 번 bus를 이용해서 회로를 구성해 본다.

그리고 앞서 설명한 공기중에 완전히 노출 했을 때와, 물에 완전히 잠겼을 때 값을 우선 측정해보니 다음과 같다. 

공기중 완전 노출시(약330)

물 속에 전부 잠겼을때(약 660)

두 값을 코드 내에 설정한 뒤, Dry, Wet, Very Wet 이렇게 세가지 단계로 토양의 수분의 결과를 눈으로 보여줄 것이다. 계산식은 센서의 wiki 사이트에 있는 것을 참조 했다.

SpiADC.TransferFullDuplex(writeBuffer, readBuffer); /* Read data from the ADC  */
adcValue = convertToInt(readBuffer);                /* Convert the returned bytes into an integer value */
const int AirValue = 660;   // Value_1
const int WaterValue = 330;  // Value_2
int intervals = (AirValue - WaterValue) / 3;

Debug.WriteLine(adcValue);
/* UI updates must be invoked on the UI thread */
var task = this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
     string value = "none";
     if (adcValue > WaterValue && adcValue < (WaterValue + intervals))
     {
          value = "very wet";
     }
     else if (adcValue > (WaterValue + intervals) && adcValue < (AirValue - intervals))
     {
          value = "wet";
     }
     else if (adcValue < AirValue && adcValue > (AirValue - intervals))
     {
          value = "Dry";
     }

     textPlaceHolder.Text = value;         /* Display the value on screen */
});

# CS, Xaml 코드 작성

UI에 해당하는 Xaml 은 토양수분 측정센서 값을 읽어서 계산한 식에 따라 Dry, Wet, Very Wet을 구분하여 String으로 화면에 표시해주도록 한다. 센서는 500ms 주기 단위로 읽어서 판단하도록 코드를 작성하고 SPI0 번 bus 사용한다.

<CS 코드>

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using Windows.UI.Xaml.Controls;
using Windows.Devices.Gpio;
using Windows.Devices.Spi;

namespace Moisture
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            /* Register for the unloaded event so we can clean up upon exit */
            Unloaded += MainPage_Unloaded;

            /* Initialize GPIO and SPI */
            InitAll();
        }

        /* Initialize GPIO and SPI */
        private async void InitAll()
        {
            if (ADC_DEVICE == AdcDevice.NONE)
            {
                StatusText.Text = "Please change the ADC_DEVICE variable to either MCP3002 or MCP3208, or MCP3008";
                return;
            }

            try
            {
                await InitSPI();    /* Initialize the SPI bus for communicating with the ADC      */

            }
            catch (Exception ex)
            {
                StatusText.Text = ex.Message;
                return;
            }

            /* Now that everything is initialized, create a timer so we read data every 500mS */
            periodicTimer = new Timer(this.Timer_Tick, null, 0, 100);

            StatusText.Text = "Status: Running";
        }

        private async Task InitSPI()
        {
            try
            {
                var settings = new SpiConnectionSettings(SPI_CHIP_SELECT_LINE);
                settings.ClockFrequency = 500000;   /* 0.5MHz clock rate                                        */
                settings.Mode = SpiMode.Mode0;      /* The ADC expects idle-low clock polarity so we use Mode0  */

                var controller = await SpiController.GetDefaultAsync();
                SpiADC = controller.GetDevice(settings);
            }

            /* If initialization fails, display the exception and stop running */
            catch (Exception ex)
            {
                throw new Exception("SPI Initialization Failed", ex);
            }
        }

        /* Read from the ADC, update the UI, and toggle the LED */
        private void Timer_Tick(object state)
        {
            ReadADC();
        }

        public void ReadADC()
        {
            byte[] readBuffer = new byte[3]; /* Buffer to hold read data*/
            byte[] writeBuffer = new byte[3] { 0x00, 0x00, 0x00 };

            /* Setup the appropriate ADC configuration byte */
            switch (ADC_DEVICE)
            {
                case AdcDevice.MCP3002:
                    writeBuffer[0] = MCP3002_CONFIG;
                    break;
                case AdcDevice.MCP3208:
                    writeBuffer[0] = MCP3208_CONFIG;
                    break;
                case AdcDevice.MCP3008:
                    writeBuffer[0] = MCP3008_CONFIG[0];
                    writeBuffer[1] = MCP3008_CONFIG[1];
                    break;
            }

            SpiADC.TransferFullDuplex(writeBuffer, readBuffer); /* Read data from the ADC                           */
            adcValue = convertToInt(readBuffer);                /* Convert the returned bytes into an integer value */
            const int AirValue = 660;   // Value_1
            const int WaterValue = 330;  // Value_2
            int intervals = (AirValue - WaterValue) / 3;
           // int soilMoistureValue = adcValue;
            Debug.WriteLine(adcValue);
            /* UI updates must be invoked on the UI thread */
            var task = this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                string value = "none";
                if (adcValue > WaterValue && adcValue < (WaterValue + intervals))
                {
                    value = "very wet";
                }
                else if (adcValue > (WaterValue + intervals) && adcValue < (AirValue - intervals))
                {
                    value = "wet";
                }
                else if (adcValue < AirValue && adcValue > (AirValue - intervals))
                {
                    value = "Dry";
                }

                textPlaceHolder.Text = value;         /* Display the value on screen                      */
            });
        }

        /* Convert the raw ADC bytes to an integer */
        public int convertToInt(byte[] data)
        {
            int result = 0;
            switch (ADC_DEVICE)
            {
                case AdcDevice.MCP3002:
                    result = data[0] & 0x03;
                    result <<= 8;
                    result += data[1];
                    break;
                case AdcDevice.MCP3208:
                    result = data[1] & 0x0F;
                    result <<= 8;
                    result += data[2];
                    break;
                case AdcDevice.MCP3008:
                    result = data[1] & 0x03;
                    result <<= 8;
                    result += data[2];
                    break;
            }
            return result;
        }

        private void MainPage_Unloaded(object sender, object args)
        {
            /* It's good practice to clean up after we're done */
            if(SpiADC != null)
            {
                SpiADC.Dispose();
            }
        }
        enum AdcDevice { NONE, MCP3002, MCP3208, MCP3008};

        /* Important! Change this to either AdcDevice.MCP3002, AdcDevice.MCP3208 or AdcDevice.MCP3008 depending on which ADC you chose     */
        private AdcDevice ADC_DEVICE = AdcDevice.MCP3008;

        private const int LED_PIN = 4; // Use pin 12 if you are using DragonBoard
        private GpioPin ledPin;

        private const string SPI_CONTROLLER_NAME = "SPI0";  /* Friendly name for Raspberry Pi 2 SPI controller          */
        private const Int32 SPI_CHIP_SELECT_LINE = 0;       /* Line 0 maps to physical pin number 24 on the Rpi2        */
        private SpiDevice SpiADC;

        private const byte MCP3002_CONFIG = 0x68; /* 01101000 channel configuration data for the MCP3002 */
        private const byte MCP3208_CONFIG = 0x06; /* 00000110 channel configuration data for the MCP3208 */
        private readonly byte[] MCP3008_CONFIG = { 0x01, 0x80 }; /* 00000001 10000000 channel configuration data for the MCP3008 */

        private Timer periodicTimer;
        private int adcValue;
    }
}

UI는 단순하게 String을 출력해주는 부분이기에 다음으로 C# 코드만 보면, Timer를 생성해서 주기적으로 ADC를 읽는 것이라 코드 적으로 큰 어려움은 없다. 다만 ADC의 레지스터를 어떻게 읽어야하는지 datasheet를 잘 보는게 가장 중요했던 것 같다. 자 그럼 실제 화분의 센서를 꽂아보자.

물을 준지 몇일 됬는데 토양상태가 유관으로 보기에도 조금 젖어있었는데 센서를 꽂아 판독된 결과는 'wet'을 가리킨다. 그러면 실제 물을 주었을때 변화가 되는지 한번 보자.

물을 일정량 주니까 Very Wet 상태가 되었다. 물론 센서 주변의 물이 많을경우 전체적으로 Very wet 상태가 아니어도 very wet이라고 판단할 수 있지만 전반적인 토양의 수분 상태를 측정할 수 있다고 본다. 이제 데이터를 샘플링해서 Cloud로 전송하는 것을 구현해야하는데, 우선 오늘은 여기까지!

반응형
댓글