Mengirim data detektor kebakaran dari arduino ke internet dengan antarmuka code igniter

Data sensor arduino

Sensor adalah instrumen atau komponen yang mampu mendeteksi perubahan kondisi objek dalam jangkauannya. Pengukuran besaran perubahan tersebut harus diubah dahulu menjadi bentuk digital sehingga dapat diproses oleh perangkat digital lainnya.

Data variabel ini bisa dimonitoring secara lokal melalui layar monitor maupun global melalui internet.

Hal yang mendukung keandalan sistem monitoring :

  1. realtime, yaitu data yang ditampilkan merupakan kondisi masa yang singkat, misalnya di perbarui setiap 1 detik.
  2. Data memiliki identitas seperti lokasi, waktu.
  3. Data yang diterima memiliki mekanisme penyaringan sehingga data yang ditampilkan terjamin.

Sensor detektor kebakaran

Terdapat beberapa Indikasi kebakaran yaitu :

  1. Api dideteksi dengan sensor flame
  2. Suhu dibaca dengan sensor suhu
  3. Asap dibaca dengan sensor asap (smoke detector)

Dalam hal pencegahan kebakaran, ketiga variabel ini terus menerus dimonitoring secara lokal.

Monitoring detektor kebakaran ini secara global juga diperlukan untuk memberikan informasi kepada pihak terkait.

Dalam contoh ini ketiga (modul) sensor ini digunakan bersama arduino dan akan dikirimkan ke mysql server.

Akses informasi sensor dengan codeIgniter

Informasi data saat ini dapat dengan mudah diakses dimanapun, namun dengan menggunakan codeIgniter diperoleh beberapa keuntungan yaitu :

  1. Pengembangan lebih mudah dengan baris program yang dapat disederhanakan.
  2. Aksesibilitas dapat dengan mudah dikontrol.
  3. Perlindungan terhadap server terutama server data lebih terjamin.

Skema Monitoring detektor kebakaran dengan codeIgniter

Komponen yang digunakan :

  1. Arduino Uno
  2. ESP8266
  3. Flame detector
  4. Sensor asap MQ7
  5. Sensor suhu dht11
  6. Relay
  7. Buzzer

Sketch / koding monitoring detektor kebakaran esp8266 – WebServer.

#include "WiFiEsp.h"
#include <SoftwareSerial.h>
#include "DHT.h"
 
char ssid[] = "****";        // Isi dengan nama profil Wifi
char pass[] = "********";            // password wifi
char server[] = "x.x.x.x";

long waktuMintaData = 1000; //minta data setiap 1000ms

#define pinFlame    A0
#define pinMQ       A1
#define pinDHT      A2
#define pinBuzzer   8
#define pinRelay    9

float setSuhu = 31.0;

String Respon = "";
long waktuMulai;
bool responDariServer = false;
bool prosesKirimDataKeServer = false;
 
WiFiEspClient client;
int status = WL_IDLE_STATUS;

SoftwareSerial wifi(2,3);
DHT dht(pinDHT, DHT11);
 
void setup()
{
  pinMode(pinFlame, INPUT_PULLUP);
  pinMode(pinMQ, INPUT_PULLUP);
  pinMode(pinDHT, INPUT_PULLUP);
  pinMode(pinBuzzer, OUTPUT);
  digitalWrite(pinRelay, HIGH);
  pinMode(pinRelay, OUTPUT);

  Serial.begin(115200);
 
  wifi.begin(115200);
  WiFi.init(&wifi);
 
  // check for the presence of the shield
  if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
    // don't continue
    while (true);
  }
 
  // attempt to connect to WiFi network
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network
    status = WiFi.begin(ssid, pass);
  }
 
  // you're connected now, so print out the data
  Serial.println("You're connected to the network");
   
  printWifiStatus();
  dht.begin();
  waktuMulai = millis();
}
 
void loop()
{
  float suhu = dht.readTemperature();
//print status
  Serial.println();
  Serial.print("Api = ");
  Serial.println(digitalRead(pinFlame));
  Serial.print("Asap = ");
  Serial.println(digitalRead(pinMQ));
  Serial.print("suhu = ");
  Serial.println(suhu);
  
  if(!digitalRead(pinFlame) && !digitalRead(pinMQ) && (suhu < setSuhu))
  {
    digitalWrite(pinRelay, HIGH);
    digitalWrite(pinBuzzer, LOW);
    
  }
  if(digitalRead(pinFlame))
  {
    digitalWrite(pinRelay, LOW);
    digitalWrite(pinBuzzer, HIGH);
  }
  if(digitalRead(pinMQ))
  {
    digitalWrite(pinRelay, LOW);
    digitalWrite(pinBuzzer, HIGH);
  }
  if(suhu >= setSuhu)
  {
    digitalWrite(pinBuzzer, HIGH);
  }
  //kirim data
  if(waktuMintaData < millis() - waktuMulai)
  {
    waktuMulai = millis();
    prosesKirimDataKeServer = kirimDataKeServer();
  }

  while (client.available()) 
  {
    char c = client.read();
    Respon += c;
  }

  Serial.print("prosesKirimDataKeServer = ");
  Serial.println(prosesKirimDataKeServer);

  if (!client.connected() && prosesKirimDataKeServer) {
    Serial.println("Disconnecting from server...");
    client.stop();
    responDariServer = true;
    prosesKirimDataKeServer = false;
  }

  // penanganan data yang diterima dari server
  if(responDariServer)
  {
    responDariServer = false;
    Serial.println(Respon);
    int posisiData = Respon.indexOf("\r\n\r\n");
    String Data = Respon.substring(posisiData+4);
    Data.trim();
 
    String variabel;
    String nilai;
 
    Serial.println("Data dari server");
    posisiData = Data.indexOf('=');
    if(posisiData > 0)
    {
      variabel = Data.substring(0,posisiData);
      nilai = Data.substring(posisiData+1);
   
      //===========Penanganan respon disini
      if(variabel == "setSuhu")
      {
        setSuhu = nilai.toFloat();
      }
//      Serial.print(variabel);
//      Serial.print(" = ");
//      Serial.println(nilai);
    }
    Respon = "";
  }
}

bool kirimDataKeServer()
{
  Serial.println();
  Serial.println("Starting connection to server...");
  // if you get a connection, report back via serial
  if (client.connect(server, 80)) {
    Serial.println("Connected to server");
    // Make a HTTP request
 
    client.print("GET /index.php/databaseArduino/dariArduino");
    client.print("?Api=");
    client.print(digitalRead(pinFlame));
     
    client.print("&Asap=");
    client.print(digitalRead(pinMQ));

    client.print("&Suhu=");
    client.print(dht.readTemperature());
     
    client.println(" HTTP/1.1");
    client.print("Host: ");
    client.println(server);
    client.println("Connection: close");
    client.println();
    return true;
  }
  return false;
}
 
void printWifiStatus()
{
  // print the SSID of the network you're attached to
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());
 
  // print your WiFi shield's IP address
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);
 
  // print the received signal strength
  long rssi = WiFi.RSSI();
  Serial.print("Signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
 
  IPAddress gateway = WiFi.gatewayIP();
  Serial.print("gateway:");
  Serial.print(gateway);
  Serial.println(" ");
}

program codeIgniter untuk monitoring data sensor arduino:

databaseArduino.php

<?php
class databaseArduino extends CI_Controller {
  public function __construct() {
    parent::__construct();
  }

  public function dariBrowser() {
    $this->load->model('Model_databaseArduino');
    $this->Model_databaseArduino->salinDataDariBrowser();
    $data['dataSensor'] = $this->Model_databaseArduino->ambilDataDariArduino();
    $data['dataParameter'] = $this->Model_databaseArduino->ambilDataDariBrowser();

    $this->load->view("data_sensor", $data);
  }

    public function dariArduino() {
    $this->load->model('Model_databaseArduino');
    $this->Model_databaseArduino->salinDataDariArduino();
    $data['dataParameter'] = $this->Model_databaseArduino->ambilDataDariBrowser();

    $this->load->view("data_parameter", $data);
  }
}
?>

Model_databaseArduino.php

<?php
class Model_databaseArduino extends CI_Model {

  public $title;
  public $content;
  public $date;

  public function ambilDataDariArduino()
  {
    $this->load->database();
    $query = $this->db->query("SELECT * FROM (
    SELECT * FROM `data_sensor` ORDER BY `nomor` DESC LIMIT 10
    ) sub
    ORDER BY `nomor` ASC");
    $this->db->close();  
    return $query->result();
  }

  public function salinDataDariArduino()
  {
    date_default_timezone_set('Asia/Jakarta'); # add your city to set local time zone
    $now = date('Y-m-d H:i:s');

    $this->load->database();
    $this->db->set('waktu', $now);
    $this->db->set('api', $this->input->get('Api'));
    $this->db->set('asap', $this->input->get('Asap'));
    $this->db->set('suhu', $this->input->get('Suhu'));
    $this->db->insert('data_sensor');
    $this->db->close();
  }

  public function ambilDataDariBrowser()
  {
    $this->load->database();
    $query = $this->db->query("SELECT * FROM `data_parameter` ORDER BY `nomor` DESC LIMIT 1");
    $this->db->close();  
    return $query->row();
  }

  public function salinDataDariBrowser()
  {
    date_default_timezone_set('Asia/Jakarta'); # add your city to set local time zone
    $now = date('Y-m-d H:i:s');

    $this->load->database();
    $this->db->set('waktu', $now);
    $this->db->set('setSuhu', $this->input->post('setSuhu'));
    $this->db->insert('data_parameter');
    $this->db->close();  
  }
}
?>

data_sensor.php

<?php
  defined('BASEPATH') OR exit('No direct script access allowed');

  if(isset($dataParameter))
  {
    echo "setSuhu=";
    echo $dataParameter->setSuhu;
  }
?>

data_parameter.php

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
?>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Welcome to CodeIgniter</title>

  <style type="text/css">

  ::selection { background-color: #E13300; color: white; }
  ::-moz-selection { background-color: #E13300; color: white; }

  body {
    background-color: #fff;
    margin: 40px;
    font: 13px/20px normal Helvetica, Arial, sans-serif;
    color: #4F5155;
  }

  a {
    color: #003399;
    background-color: transparent;
    font-weight: normal;
  }

  h1 {
    color: #444;
    background-color: transparent;
    border-bottom: 1px solid #D0D0D0;
    font-size: 19px;
    font-weight: normal;
    margin: 0 0 14px 0;
    padding: 14px 15px 10px 15px;
  }

  code {
    font-family: Consolas, Monaco, Courier New, Courier, monospace;
    font-size: 12px;
    background-color: #f9f9f9;
    border: 1px solid #D0D0D0;
    color: #002166;
    display: block;
    margin: 14px 0 14px 0;
    padding: 12px 10px 12px 10px;
  }

  #body {
    margin: 0 15px 0 15px;
  }

  p.footer {
    text-align: right;
    font-size: 11px;
    border-top: 1px solid #D0D0D0;
    line-height: 32px;
    padding: 0 10px 0 10px;
    margin: 20px 0 0 0;
  }

  #container {
    margin: 10px;
    border: 1px solid #D0D0D0;
    box-shadow: 0 0 8px #D0D0D0;
  }
  </style>
</head>
<body>



<!--
<form action="/form/data_submitted" method="get">
User Name: <input type="text" name="u_name" placeholder="Please Enter User Name" class="input_box">
<br>
User email: <input type="text" name="u_email" placeholder="Please Enter Email Address" class="input_box">
<input type="submit" value="Submit" class="submit">
</form>
-->

<?php

// echo $this->input->post('setSuhu');

echo form_open('databaseArduino/dariBrowser');
if(isset($dataParameter->setSuhu))
{
  echo form_input('setSuhu', $dataParameter->setSuhu);
}
else
{
  echo form_input('setSuhu', '30.0');
}
echo form_submit('suhuSubmit', 'Set Temperatur');
echo form_close();

echo "<br>";
echo "<strong>Data pembacaan sensor</strong>";
echo "<br>";
echo "<table>";

  echo "<tr>";
  echo "<td width='50'>Nomor</td>";
  echo "<td width='200'>Waktu</td>";
  echo "<td width='50'>Api</td>";
  echo "<td width='50'>Asap</td>";
  echo "<td width='50'>Suhu</td>";
  echo "</tr>";

  foreach ($dataSensor as $row)
{
  echo "<tr>";
  echo "<td>".$row->nomor."</td>";
  echo "<td>".$row->waktu."</td>";
  echo "<td>".$row->api."</td>";
  echo "<td>".$row->asap."</td>";
  echo "<td>".$row->suhu."</td>";
  echo "</tr>";
}
echo "</table>";
?>
</body>
</html>

file server : htdocs.zip

Trik pemograman arduino

Clock 1Hz

Clock 1Hz atau penanda detik tanpa rtc

long millisDetik;

void setup() {
  millisDetik = millis();
}

void loop() {
  if(millisDetik != millis() / 1000L)
  {
    millisDetik = millis() / 1000L;

    // Kode

  }
}

Menghemat RAM menggunakan flash string

Arduino memiliki memory ram yang kecil, salah satu langkah penghematan adalah dengan memasukkan konstanta text / string ke dalam flash seperti kode berikut:

  Serial.println(F("https://www.semesin.com/project"));

atau jika menggunakan alamat

const PROGMEM char text[] = "https://www.semesin.com/project";
void setup() {
  Serial.begin(9600);
  Serial.println((const __FlashStringHelper *)text);
}

Definisi nilai output

Nilai parameter fungsi digitalWrite relay (NC/NO) dan transistor/mosfet sering kali terbalik, untuk mempermudah pekerjaan nilainya LOW-nya lebih baik didefenisikan.

#define pinRelay   8
#define relayLOW   HIGH //relay dengan nilai kebalikan

void setup() {
  pinMode(pinRelay, OUTPUT);
}

void loop() {
  digitalWrite(pinRelay, relayLOW);//mati
  delay(1000);
  digitalWrite(pinRelay, !relayLOW);//hidup
  delay(1000);
}

Cast Float to Byte Array

Dalam komunikasi data berbentuk float, lebih baik mengirim data berupa byte array dari pada nilai string dari float

Serial.begin(9600);
float nilaiFloat = 0.15625;
byte *arrayByte;

arrayByte = (byte*)&nilaiFloat;
Serial.print(nilaiFloat);
Serial.print(" = ");
Serial.print(arrayByte[3],HEX);
Serial.print(' ');
Serial.print(arrayByte[2],HEX);
Serial.print(' ');
Serial.print(arrayByte[1],HEX);
Serial.print(' ');
Serial.println(arrayByte[0],HEX);

Aksi tombol repeat (tekan lama dan berulang)

Penggunaan tombol untuk merubah nilai settingan naik dan turun dengan fitur ‘tekan sekali-berubah sekali’ dan ‘tekan lama-berubah berulang’  (repeating button) bisa menggunakan metode berikut:

#define pinTombolUp         2
pinMode(pinTombolUp, INPUT_PULLUP);
if (!digitalRead(pinTombolUp))
{
  delay(50);//debounce
  bool tombolStart = true;
  while (!digitalRead(pinTombolUp))
  {


    // Aksi tombol disini


    if (tombolStart)
    {
      //tekan pertama delay 10*50 = 500ms
      for (byte d = 0; d < 10 && !digitalRead(pinTombolUp); d++)
      {
        delay(50);
      }
      tombolStart = false;
    }
    else
    {
      delay(100);//perulangan aksi dengan delay 100ms
    }
  }
}

Konstanta memory

Konstanta memory mikrokontroller pada arduino

  Serial.println(FLASHEND);//ukuran flash
  Serial.println(RAMEND);//ukuran ram
  Serial.println(XRAMEND);//ukuran ram tambahan
  Serial.println(E2END);//ukuran eeprom

Offset anggota struct

Ketika menggunakan memori EEPROM untuk menyimpan struktur data (struct), maka untuk merujuk data tertentu dapat menggunakan fungsi ‘offsetof’ (jika alamat struct di EEPROM = 0)

struct Setting
{
  byte tokenSetting;
  byte jumlahDatabase;
  byte password[4];
};

Serial.println(offsetof(Setting, tokenSetting));
Serial.println(offsetof(Setting, jumlahDatabase));

Karakter Simbol untuk LCD Matrix

LCD karakter menyimpan font di dalam memory internalnya dengan penomoran 0 hingga 255. Sebagi contoh simbol derajat ‘°’ bernilai 223 (hex 0xDF) maka untuk mencetak karakter diluar char (0x80 – 0xFF) nilai tersebut di cast dulu menjadi char.

lcd.print((char)223);

Input keyboard untuk LCD 16×2 menggunakan Arduino

Keyboard dan mouse merupakan perangkat PC (personal computer) yang berfungsi sebagai interface atau perantara antara pengguna dan pc. Perangkat ini umumnya terhubung menggunakan interface usb, akan tetapi umumnya keyboard dan mouse juga memiliki interface PS2 dan AT Bus.

PS2

PS2 atau Personal System/2 adalah salah satu protokol komunikasi antara komputer dan perangkat lain yang dikembangkan pada tahun 1987. Untuk keyboard dan mouse standar soket PS2-nya berbentuk seperti ini :

AT Bus

AT atau advanced technology bus dikembangkan pada tahun 1984. Standar interface AT Bus untuk keyboad berbentuk seperti ini:

USB Keyboard / mouse

Saat sekarang keyboad dan mouse dengan interface PS2 dan AT bus sudah tidak ditemukan yang digantikan dengan soket dengan interface USB. Namun kebanyakan keyboard dan mouse masih mendukung interface PS2 dengan kombinasi sebagai berikut :

Skema penggunaan keyboard dan mouse sebagai input

contoh sketch/program arduino menggunakan keyboard

#include <PS2_Semesin.h>
#include <LiquidCrystal_I2C.h>
#include <PS2Code.h>

#define keyboardDATAPIN   4
#define keyboardClockPIN  3

#define MAX_COL 16
#define MAX_ROW  2
int8_t cols = 0;
int8_t rows = 0;

LiquidCrystal_I2C lcd(0x3F, MAX_COL, MAX_ROW);
PS2 keyboard;

void setup()
{
  Serial.begin(9600);
  Serial.println("Keyboard arduino dengan tampilan I2C LCD");
  Serial.println("https://www.semesin.com/project/");
  Serial.println();

  lcd.begin();
  lcd.backlight();

  keyboard.begin( keyboardDATAPIN, keyboardClockPIN );
  keyboard.setNoBreak(1);
  keyboard.setNoRepeat( 1 );
}


void loop()
{
  if ( keyboard.available() )
  {
    char keyboardData = keyboard.read();
    if(keyboardData >= ' ' && keyboardData <= '~')
    {
      lcd.print(keyboardData);
      cols++;
      if ( cols >= MAX_COL )
      {
        cols = 0;
        rows++;
        if ( rows >= MAX_ROW )
        {
          rows = 0;
        }
      }
      lcd.setCursor( cols, rows );
      Serial.print(keyboardData);
    }
    else
    {
      Serial.println();
      Serial.print("Special char = ");
      Serial.println(keyboardData,HEX);
      switch ( keyboardData )
      {
        case PS2_KEY_ENTER:
        case PS2_KEY_KP_ENTER:
          cols = 0;
          rows++;
          if ( rows >= MAX_ROW )
            rows = 0;
          break;
        case PS2_KEY_PGDN:
          rows = MAX_ROW - 1;
          break;
        case PS2_KEY_PGUP:
          rows = 0;
          break;
        case PS2_KEY_L_ARROW:
          cols--;
          if ( cols < 0 )
          {
            cols = MAX_COL - 1;
            rows--;
            if ( rows < 0 )
              rows = MAX_ROW - 1;
          }
          break;
        case PS2_KEY_R_ARROW:
          cols++;
          if ( cols >= MAX_COL )
          {
            cols = 0;
            rows++;
            if ( rows >= MAX_ROW )
              rows = 0;
          }
          break;
        case PS2_KEY_UP_ARROW:
          rows--;
          if ( rows < 0 )
            rows = 0;
          break;
        case PS2_KEY_DN_ARROW:
          rows++;
          if ( rows >= MAX_ROW )
            rows = MAX_ROW - 1;
          break;
        case PS2_KEY_BS:
          cols--;
          if ( cols < 0 )
          {
            cols = MAX_COL - 1;
            rows--;
            if ( rows < 0 )
              rows = MAX_ROW - 1;
          }
          lcd.setCursor( cols, rows );
          lcd.write( ' ' );
          break;
        case PS2_KEY_HOME:
          cols = 0;
          rows = 0;
          break;
        case PS2_KEY_END:
          cols = MAX_COL - 1;
          rows = MAX_ROW - 1;
          break;
      }
      lcd.setCursor( cols, rows );
    }
  }
}

library LiquidCrystal-I2C.zip, PS2_Semesin.zip

Pengontrolan tegangan menggunakan PWM pada arduino

Pengontrolan tegangan berfungsi untuk menjaga kestabilan tegangan keluaran ke beban sehingga beban bisa bekerja semestinya. Contohnya jika beban lampu yang intensitas cahayanya bergantung kepada tegangan, maka dengan tegangan yang stabil akan mengeluarkan cahaya yang stabil pula.

Hal yang menyebabkan ketidakstabilan tegangan diantaranya :

  1. Perubahan beban (penambahan dan pengurangan)
  2. Perubahan nilai masukan (input)
  3. Faktor luar seperti interferensi.

Faktor yang mempengaruhi keandalan pengontrolan tegangan :

  1. Kecepatan respon dari pengontrol tegangan terhadap perubahan yang terjadi, semakin cepat semakin baik.
  2. Sistem koreksi yang digunakan, seperti PID, fuzzy
  3. Karakteristik sensor dan beban yang digunakan

Pengontrolan tegangan dengan arduino

Pengendalian tegangan harus memiliki masukan sensor tegangan dan aktuator kontrol tegangan. Pada aplikasi arduino pembacaan tegangan menggunakan ADC dan aktuasi kontrol tegangan menggunakan PWM.

Dalam contoh ini, sistim koreksi tegangan menggunakan metode proporsional, yaitu semakin besar selisih tegangan dan input maka akan semakin besar pula penambahan nilai PWM.

skema sistem kontrol tegangan mengguakan arduino:

Sketch / koding Penngendalian Tegangan dengan PWM:

#define pinSensorTegangan     0
#define pinOutputPWM          9
#define setTegangan           2.5//volt
#define faktorProporsional    0.1

float keluaran;

void setup() {
  pinMode(pinOutputPWM, OUTPUT);
  
  Serial.begin(9600);
  Serial.println("Sumber tegangan stabil (automatic voltage regulator) menggunakan kontrol proporsional");
  Serial.println("https://www.semesin.com/project/");
  Serial.println();

  keluaran = setTegangan;
}

void loop() {
  uint16_t adc = analogRead(pinSensorTegangan);
  float tegangan = map(adc, 0, 1024, 0, 500)/100.0;
  float selisih = setTegangan - tegangan;
  float proporsional = faktorProporsional * selisih;
  
  keluaran += proporsional;
  keluaran = constrain(keluaran, 0, 5);
  byte keluaranPWM = map(keluaran*100, 0, 5*100, 0, 255);
  analogWrite(pinOutputPWM, keluaranPWM);

  //Plot serial, hapus untuk menambah kecepatan
  Serial.print(tegangan);
  Serial.print(", ");
  Serial.print(keluaranPWM);
  Serial.println();
  delay(10);
}

Hasil Plot sinyal PWM (merah) dan tegangan keluaran (biru) terhadap perubahan beban.

Tombol power otomatis arduino

tombol pengunci (self-latching)

Rangkaian kunci tombol otomatis bisa diterapkan dengan bermacam komponen utama, yang umum digunakan adalah dengan menggunakan relay, transistor, mosfet dan SCR.

Penggrendel tombol dengan relay

Dalam sistem listrik rangkaian ini dikenal sebagai DOL (direct on line). Jika tombol ‘nyala’ ditekan maka tegangan akan masuk ke relay melalui tombol ‘padam’, kemudian relay akan aktif dan NO dalam keadaan kontak dan mengunci tegangan tetap masuk ke coil relai dan mengunci posisi relay aktif.

Jika tombol ‘padam’ ditekan dalam keadaan rangkaian aktif, maka suplai  kunci akan terputus dan relay kembali nonaktif.

Pengancing tombol dengan SCR

Masih menggunakan interface yang sama tetapi menggunakan SCR/thyristor 2P4M sebagai penguncinya. Jika tombol ‘nyala ditekan, akan mengalirkan arus ‘gate trigger’ melalui resistor 330 ohmm dan menjadikan SCR dalam keadaan on-state yang selalu aktif walaupun tombol ‘nyala dilepaskan’.

Jika tombol ‘padam’ ditekan, maka suplay arus ke SCR melalui beban outpun akan hilang dan otomatis akan mematikan operasi SCR.

Tombol Vcc otomatis pada arduino menggunakan MOSFET

Tombol power dan arduino bisa dikombinasikan (saling melengkapi) antara pemberi sumber, dan pengunci sumber.

Cara kerjanya adalah pada saat tombol power ditekan, gate pada mosfet akan diberi tegangan 5V, dan menjadikan mosfet menghantarkan GND arduino ke ground sumber.

Sesaat setelah menerima power dari sumber/baterai melalui mosfet, arduino akan menjalankan perintah untuk mengunci kondisi mosfet tetap aktif.

Untuk menonaktifkan mosfetm, cukup dengan menjadikan pin Gate bernilai LOW.

contoh sketch Sumber baterai dengan tombol otomais:

#define pinPower A0
#define pinLED   13

void setup() {
  Serial.begin(9600);
  Serial.println("Power arduino otomatis menggunakan MOSFET");
  Serial.println("https://www.semesin.com/project/");
  Serial.println();
  Serial.println("Jangan beri power supply / catu daya USB");
  Serial.println("Akan dimatikan dalam 10 detik");
  Serial.println();
  
  pinMode(pinPower, OUTPUT);
  pinMode(pinLED, OUTPUT);
  digitalWrite(pinPower, HIGH);
  digitalWrite(pinLED, LOW);

  delay(7000);
  digitalWrite(pinLED, HIGH);//Tanda arduino akan dimatikan 3 detik lagi
  delay(3000);

  //Fungsi mematikan power Vcc
  digitalWrite(pinPower, LOW);
  
}

void loop() {

}

Tombol power otomatis pada arduino menggunakan SCR (hemat energi)

Prinsip kerja power otomatis pada arduino ini adalah ketika tombol ‘power’ ditekan, maka gate dari scr akan diberi trigger untuk mengaktifkan SCR, setelah tombol dilepaskan SCR akan tetap aktif tanpa sumber arus lain. Tidak seperti menggunakan transistor atau mosfet dimana arus dan tegangan harus disuplai dari arduino.

Untuk mematikan power ini, cukup dengan meng-ground-kan / logika LOW pada pin gate.

contoh sketch kunci tombol power otomatis:

#define pinPower A0
#define pinLED   13

void setup() {
  Serial.begin(9600);
  Serial.println("Power arduino otomatis ");
  Serial.println("https://www.semesin.com/project/");
  Serial.println();
  Serial.println("Jangan beri power supply / catu daya USB");
  Serial.println("Akan dimatikan dalam 10 detik");
  Serial.println();
  
  pinMode(pinLED, OUTPUT);
  digitalWrite(pinLED, LOW);

  delay(7000);
  digitalWrite(pinLED, HIGH);//Tanda arduino akan dimatikan 3 detik lagi
  delay(3000);

  //Fungsi mematikan power Vcc
  digitalWrite(pinPower, LOW);
  pinMode(pinPower, OUTPUT);
  
}

void loop() {

}

Kalkulator online persamaan garis lurus antara dua titik

Menghitung persamaan garis linear dengan rumus :

masukkan nilai (x1, y1), (x2, y2) dan nilai x jika ingin mencari nilai y pada titik x tersebut pada kotak berikut :

x1 =
y1 =
x2 =
y2 =
Persamaan garis lurus
Gradien m =
.
Konstanta c =
cari nilai y dengan x =
hasil
y =

 

Persamaan garis lurus adalah garis dalam bentuk matematis berpangkat satu. Persamaan umumnya adalah :

y = mx + c

dimana m = gradien dan c = konstanta.

Gradien adalah kemiringan dari suatu garis dan konstanta adalah ketinggian garis pada x = 0.

Komposisi larutan HCl dan H2O2 (praktek pelarut PCB dengan vixal dan vanish)

Asam Klorida (HCL)

Hidrogen klorida adalah asam kuat yang terdisosiasi dengan air dan sangat korosif, Jika terioniasasi akan melepaskan H+ dan ion Cl-. Massa relatif (Mr) HCL adalah 36,46094 gram/mol.

Tabel konsentrasi HCL beserta molaritasnya

Nomor Konsentrasi (kg HCl/kg) Massa jenis (gram/liter) Molaritas (mol/liter)
1 10% 1048 2.87
2 20% 1098 6.02
3 30% 1149 9.45
4 32% 1159 10.17
5 34% 1169 10.90
6 36% 1179 11.64
7 38% 1189 12.39

Hidrogen Peroksida (H2O2)

Adalah cairan oksidator kuat yang berbau khas keasaman dan mudah larut dalam air. Massa relatif H2O2 =  34,01468 gram/mol dan massa jenis H2O2 = 1.450 gram/liter

Tembaga (Cu)

Massa relatif tembaga (Cu) = 63,546 gram/mol, dan massa jenis tembaga 8.900 gram/liter.

Pada PCB elektronik ketebalan lapisan tembaga berkisar 0,034 mm.

Menghitung campuran HCL + H2O2 untuk Etching PCB

Persamaan kesetimbangan kimia campuran etching H2O2 + HCL:

Cu + 2 HCl + H2O2 = CuCl2 + 2 H2O

Hasil dari reaksi ini adalah CuCl2 (Copper(II) chloride) yang merupakan asam lemah.

Massa relatif dari masing-masing senyawa adalah :

  • Cu = 250.03961
  • HCl = 36.46094
  • H2O2 = 34.01468

Vixal + Vanish sebagai pelarut PCB

Konsentrasi HCl pada vixal adalah 17 %v/v

Konsentrasi H2Opada vanish adalah 5 %v/v

Untuk mendapatkan volume setiap molnya dari tabel molaritas senyawa digunakan rumus :

Jika konsentrasi tidak ada dalam tabel, volume untuk setiap mol digunakan rumus:

dengan formula volume per mol dari persen tersebut diperoleh:

Volume per mol HCl 17% vixal = 36,46094 / (0.17 x 1083) = 0,198 liter.

Volume per mol H2O2 Vanish = 34,01468 / (0.05 x 1450) = 0,469 liter.

Volume per mol tembaga murni = 63,546 / (1 x 8900) = 0,00714 liter atau 63,546 gram.

cat: massa jenis digunakan pendekatan umum.

Jadi dengan campuran :

(2 x 0.18 liter Vixal 17%) + 0.469 liter Vanish 5% akan melarutkan 0,00714 liter tembaga.

Jika ketebalan tembaga pada PCB 0.034 mm maka luas yang akan digerus oleh larutan tersebut adalah :

Luas tembaga = 0,00714 liter / 0.00034 dm = 21 dm² (2100 cm²). cukup untuk 10 lembar PCB kosong.

Namun dalam praktek sering ditemui kendala berikut:

  1. Konsentrasi bahan aktif yang tidak sesuai.
  2. Jenis pelarut bukan air.
  3. Tembaga PCB bukan tembaga murni.
  4. Kontaminan pada pelarut atau di permukaan PCB.

Komunikasi data Bluetooth dengan App Inventor

Bluetooth adalah Sistem jaringan lokal tanpa kabel. Bluetooth telah digunakan secara luas pada perangkat android dan telepon pintar lainnya.

Menghubungkan ke bluetooth server

Untuk mempermudah jalan program app inventor, proses menghubungkan bluetooth ke server dimasukkan kedalam sebuah prosedur.

Prosedur ‘Hubungkan_Bluetooth’ ini bisa dipanggil dengan variabel ‘alamatBluetooth’ yang berisi alamat unik bluetooth. Sebelum dihubungkan prosedur akan mengecek status ‘bluetooth.available’, jika aktif, dilanjutkan dengan pengecekan status ‘bluetooth.IsConnected’, dan mematikan bluetooth jika sedang terkoneksi.

Selanjutnya bluetooth dihubungkan dengan fungsi ‘bluetoothClient.Connect’ dengan variabel alamat yang diberikan.

Pemanggilan bluetooth saat Inisialisasi

Apabila sistem menghubungkan bluetooth dibuat otomatis, maka bluetooth akan dihubungkan keserver dengan alamat yang tersimpan sebelumnya pada database ‘TinyDB’. Pemanfaatan database ini adalah untuk otomatisasi data saat aplikasi app dibuka tanpa perlu disetting ulang, cukup menggunakan settingan terakhir.

Koneksi bluetooth dengan alamat langsung

Jika alamat bluetooth server sudah diketahui, app bisa langsung mengkondisikan tombol sebagai pemanggil prosedur ‘hubungkan_bluetooth’ dengan alamat yang ada pada ‘textbox’.

Menghubungkan bluetooth dengan alamat pada daftar bluetooth

Daftar perangkat bluetooth yang terhubung ke android bisa diambil sebagai referensi alamat. Sebelum digunakan, elemen ‘listPicker’ digunakan sebagai penampung daftar bluetooth yang tersedia.

Apabila item yang ada dalam daftar sudah dpilih, maka akan dipanggil prosedur ‘hubungkan_Bluetooth’ dengan variabel alamat terpilih.

Putuskan hubungan bluetooth

Jika ingin memutuskan bluetooth dari server, cukup menggunakan fungsi ‘BluetoothClient.Disconect’

Cek koneksi bluetooth

Status konksitas bluetooth bisa diketahui dengan fungsi ‘BluetoothClient.IsConnected’

Mengirim data string melalui bluetooth

Untuk mengirim data melalui bluetooth, terlebih dahulu dipastikan koneksi bluetooth dengan fungsi’bluetoothClient.IsConnected’. Dan apabila terhubung, proses pengiriman string dilakukan dengan memanggil fungsi ‘bluetoothClient.SendText’ dengan variabel ‘text’ yang diisi contohnya text dalam ‘textBox’.

Menerima data dari bluetooth

Jika ada data yang dikirim melalui bluetooth, data tersebut bisa dideteksi dengan fungsi ‘bluetoothClient.SendText’ seperti pada block berikut :

Dengan bantuan ‘clock.Timer’, setiap interval waktu dilakukan pengecekan apabila ada data yang diterima lewat bluetooth, kemudian mengambilnya dengan fungsi ‘bluetooth.ReceiveText’.

Namun fungsi diatas memiliki beberapa kelemahan yaitu:

  1. Data yang diterima tidak diparsing, sehingga data apapun yang diterima adalah merupakan deretan String.
  2. Jika dalam satu interval waktu diterima beberapa data, maka data tersebut dianggap suatu kesatuan.
  3. Jika proses pengiriman data berlangsung saat interupsi timer terjadi, maka data tersebut akan terpotong (data tidak utuh).

Untuk menutui itu digunakan algoritma app inventor yang lebih rumit namun mampu berjalan pada jumlah data besar.

Proses pengambilan data dari buffer android dilakukan secara parsial sehingga terhindar dari pemotongan, dengan metode ini dimungkinkan menerima beberapa nilai variabel sekaligus (dalam lalu lintas kecepatan tinggi).

Dalam contoh ini digunakan format ‘namaVariabel’=’nilai’, dalam contoh sketch arduino :

void setup() {
  Serial.begin(9600);
  Serial.println("Koneksi Beluetooth pada Android dengan App Inventor");
  Serial.println("https://www.semesin.com/project/");
  Serial.println();
}

byte variabel1 = 0;
uint16_t variabel2 = 1000L;

void loop() {
  while (Serial.available())
  {
    Serial.write(Serial.read());
  }
  delay(1000);
  Serial.println("var1=" + String(variabel1));
  Serial.println("var2=" + String(variabel2));
  variabel1 += 1;
  variabel2 += 10;
}

Mendeteksi kegagalan bluetooth

Fungsi ‘bluetoothClient.BluetoothError’ akan mendeteksi jika terjadi kegagalan bluetooth seperti kesalahan dalam proses menghubungkan, mengirim atau menerima data. Blok diatas akan mengirimkan pesan kepada pengguna melalui label ‘pesan’.

Design cara komunikasi bluetooth app inventor:

Block lengkap sistem komunikasi bluetooth android:

apk menghubungkan bluetooh dengan app inventor:

Multi screen dengan App Inventor

Aplikasi App Inventor dibuat diatas screen. Screen adalah halaman tempat meletakkan interface, layout dan komponen lainnya.

Tutorial pindah antar screen ini mencakup metode pindah screen yang biasa digunakan.

Layout scroll

Apabila ruang (space) satu screen tidak mencukupi, maka bisa memanfaatkan layout dalam mode scroll baik vertikal maupun horizontal.

Penggunaan layout scroll sangat efektif karena ruang yang disediakan lebih besar.

Banyak Screen App Inventor

Pada aplikasi App Inventor, kita bisa membangun sebuah app dengan lebih dari satu screen. Dengan banyak screen kita bisa mengelompokkan komponen-komponen agar lebih terorganisir.

Screen bertindak sebagai alas tempat komponen. Apabila sebuah screen saat ini (screen induk) membuka screen lainnya (screen anak), maka screen anak akan menimpa screen induk.

Perpindahan antar Screen dengan nilai awal dan nilai balik

Sebuah screen bisa mengirim nilai awal (start value) ke screen anaknya. juga sebaliknya screen anak bisa mengirim hasil (result) ke screen induknya.

Dari contoh diatas :

  1. Button1 adalah tombol untuk membuka ‘screen2’, dengan kontrol ‘open another screen’ dan variabel ‘screenName’ diisi dengan ‘Screen2’, maka ketika button1 ditekan ‘screen2’ akan dibuka diatas ‘screen1’.
  2. Button2 adalah tombol untuk membuka screen3, pemanfaatan fungsi ‘open another screen with start value’ dengan variabel ‘screenName’ dan ‘startValue’ diisi dengan input interface ‘textBox1.text’, maka screen induk akan mengirimkan nilai awal dalam ‘textbox1.text’ ke ‘screen3’.
  3. Fungsi ‘other screen closed’ akan dijalankan saat terdeteksi sebuah screen anak ditutup dan dan kembali ke screen ini (induk). variabel ‘result’ akan memiliki nilai yang dikirim oleh screen anak.

Perpindahan antar screen anak

Pindah atar screen akan bisa dalam dua metode:

  1. Pindah ke screen anak lainnya sebagai screen cucu (tanpa menutup screen sekarang)
  2. Pindah ke screen anak lainnya sebagai ganti anak (dengan menutup screen sekarang).

Pada blok diatas adalah metode pindah antar screen anak sebagai ganti screen sekarang dengan tetap menginduk ke ‘screen1’.

Penggunaan fungsi ‘if’ dengan variabel ‘true’ hanya sebuah trik untuk menempatkan prosedur/fungsi lain dibawah fungsi ‘open another screen’.

Nilai awal dari screen induk

Saat ‘screen3’ dibuka, app akan menjalankan terlebih dahulu fungsi ‘Initialize’. Fungsi ini ditugaskan mengambil nilai awal yang dikirimkan oleh screen induk dalam variabel ‘get start value’ dan menyimpannya kedalam interface ‘textBox1/text’

Mengirim nilai balik ke screen induk

Ada beberapa cara untuk kembali ke screen induk, diantaranya adalah fungsi ‘BackPressed’ atau penggunaan tombol.

Fungsi ‘close screen with value’ berfungsi menutup screen dengan mengirimkan nilai balik dalam variabel ‘result’.

design app inventor banyak screen:

Screenshoot tukar screen app inventor:

apk multi screen app inventor: MultiScreen.apk

Fungsi Waktu App Inventor

Data waktu pada App Inventor berupa tanggal dan waktu yang dinyatakan dalam orde milidetik dari 1 Januari 1970. Data waktu ini bisa digunakan untuk beberapa keperluan diantaranya hitung mundur dan durasi.

Data waktu Sekarang (now)

Interface / modul clock memiliki fungsi ‘now’ yang menyediakan data waktu serta fungsi format datetime guna mendapatkan text waktu yang diinginkan.

Block diatas akan mengganti text pada label1 dengan text waktu sekarang dalam format MM/dd/yyyy hh:mm:ss a. Penggantian dilakukan setiap interval dari clock1 (default 1 detik).

Ambil waktu sistem android

Untuk mengambil data waktu ketika tombol di tekan dengan format tertentu bisa menggunakan block berikut:

Ketika button1 ditekan, text pada label2 akan diganti dengan waktu sekarang dalam format hh:mm.

Edit data waktu dengan TimePicker

TimePicker berfungsi sebagai dialog pengedit waktu dengan nilai default waktu sekarang (now)

Ketika tombol [selesai] pada dialog timePicker ditekan, text pada tombol [TimePicker] akan diganti dengan hasil pengeditan jam dan menit.

Angka dua digit (time pattern) app inventor

Agar tampilan waktu jam:menit terlihat rapi dalam format waktu hh:mm, App inventor 2 belum mengakomodir text pattern (kecuali sensor clock), untuk itu bisa dikombinasikan dengan perhitungan matematis yakni apabila angka kecil dari 10 maka akan ditambahkan ‘0’ diawalnya.

Hitung mundur (Countdown)

Hitung mundur adalah metode penghitungan waktu tunda. Hitung mundur bisa dimanfaatkan untuk berbagai keperluan seperti aktifasi sebuah interface/screen dan sebagainya.

Hitung mundur dengan app inventor diaplikasikan dalam orde mili detik, agar lebih mudah waktu hitung mundur dikonversi menjadi detik dengan fungsi DurationToSeconds.

Untuk perancangan hitung mundur dengan app inventor dibutuhkan variabel waktu mulai dan durasi. Dengan bantuan ‘Clock’ penghitungan dideteksi setiap detik.

Hitung munder dengan metode time epoch ini bisa disetting dalam orde tahunan atau sesuai kebutuhan.

Menghitung durasi dengan app inventor

Durasi / selisih waktu adalah waktu yang dibutuhkan antara dua evenr/trigger, hasilnya diperoleh dengan formula waktu event2 dikurangi waktu event1.

Penghitungan durasi diukur dalam orde mili detik.

Block lengkap fungsi waktu menggunakan app inventor:

Design fungsi waktu dengan app inventor:

Tampilan app fungsi waktu memakai app inventor:

File apk fungsi waktu pada app inventor: FungsiWaktu.apk

Hitung mundur Arduino (volatile countdown)

Hitung mundur volatile (tidak menguap) adalah metode hitung mundur digital yang terus berlangsung walaupun catu daya (power supply) dimatikan. Jadi hitung mundur berlanjut sesuai waktu normal ketika catu daya aktif kembali.

Countdown arduino ini berfungsi sebagai penghitung mundur hingga jangka tahunan karena menggunakan metode epoch time. Waktu acuan yang digunakan adalah waktu RTC (DS1307/DS3231).

Waktu epoch adalah jumlah detik hingga saat ini dari tangga 1 Januari 1970.

Hitung mundur menggunakan Arduino dan RTC memanfaatkan EEPROM untuk menyimpan data-data berikut :

  1. Aktif
  2. Waktu mulai hitung mundur (epoch waktu)
  3. Waktu hitung mundur (dalam detik)

Karena perangkat ini memiliki fitur volatile maka dibutuhkan mekanisme pengujian/pengecekan data waktu yang disimpan yaitu :

  1. Apakah ada hitung mundur yang aktif
  2. Jika aktif apakah epoch waktu sekarang lebih besar dari epoch waktu hitung mundur mulai
  3. jika aktif apakan epoch waktu sekarang kecil dari jumlah epoch waktu mulai ditambah jumlah detik hitung mundur.

jika syarat ini di penuhi maka hitung mundur dengan arduino dilanjutkan.

berikut skema Arduino countdown (berlaku untuk DS1307/DS3231):

koding / sketch arduino hitung mundur:

#include <Wire.h>
#include <TimeLib.h>
#include <DS1307RTC.h>
#include <EEPROM.h>

#define alamatEEPROMCountDownAktif  0
#define alamatEEPROMCountDownMulai  1
#define alamatEEPROMCountDownDetik  5

byte countDownAktif;
uint32_t countDownMulai;
uint32_t countDownDetik;
uint32_t RTCEpoch;
byte detikSebelumnya = 60;

void setup() {
  Serial.begin(9600);
  Serial.println("Hitung mundur Arduino (volatile countdown)");
  Serial.println("https://www.semesin.com/project/");
  Serial.println("Entri waktu hitung mundur (dalam detik) :");
  Serial.println();

  countDownAktif = EEPROM.read(alamatEEPROMCountDownAktif);
  if(countDownAktif)
  {
    EEPROM.get(alamatEEPROMCountDownMulai, countDownMulai);
    EEPROM.get(alamatEEPROMCountDownDetik, countDownDetik);
    Serial.println("Hitung mundur aktif");
  }
  
}

void loop() {
  tmElements_t tm;

  if(Serial.available())
  {
    if (RTC.read(tm))
    {
      delay(200);
      countDownDetik = Serial.parseInt();
      countDownMulai = makeTime(tm);
      countDownAktif = true;
      EEPROM.write(alamatEEPROMCountDownAktif, countDownAktif);
      EEPROM.put(alamatEEPROMCountDownDetik, countDownDetik);
      EEPROM.put(alamatEEPROMCountDownMulai, countDownMulai);
      Serial.println("Waktu hitung mundur = " + String(countDownDetik) + " detik");
    }
    else
    {
      Serial.println("Gagal membaca RTC");
    }
  }
  
  if (RTC.read(tm)) 
  {
    if(detikSebelumnya != tm.Second)
    {
      if(countDownAktif)
      {
        RTCEpoch = makeTime(tm);
        uint32_t waktuCountDown = countDownDetik - (RTCEpoch - countDownMulai);
        Serial.println("Hitung mundur : " + String(waktuCountDown) + " detik");
  
        if(RTCEpoch < countDownMulai)
        {
          countDownAktif = false;
          EEPROM.write(alamatEEPROMCountDownAktif, countDownAktif);
          Serial.println("Hitung mundur di matikan karena Waktu RTC salah");
          Serial.println("Entri waktu hitung mundur (dalam detik) :");
        }
        else if(RTCEpoch > (countDownMulai + countDownDetik))
        {
          countDownAktif = false;
          EEPROM.write(alamatEEPROMCountDownAktif, countDownAktif);
          Serial.println("Hitung mundur kadaluarsa");
          Serial.println("Entri waktu hitung mundur (dalam detik) :");
        }
        else if(waktuCountDown == 0)
        {
          countDownAktif = false;
          EEPROM.write(alamatEEPROMCountDownAktif, countDownAktif);
          Serial.println("Hitung mundur berakhir");
          Serial.println("Entri waktu hitung mundur (dalam detik) :");
        }
      }
      detikSebelumnya = tm.Second;
    }
  }
  else
  {
    Serial.println("Gagal membaca RTC");
  }
  delay(100);
}

Keluaran serial monitor hitung mundur berbasis arduino:

Menu LCD Arduino dengan keypad

Aplikasi menu arduino memang menarik namun tidak mudah untuk dibuat. Arduino menggunakan menu merupakan aplikasi yang menampilkan sejumlah pilihan sehingga pengguna bisa memilih/merubah pilihannya.

Menu interaktif lebih cocok digunakan apabila sejumlah pilihan tidak bisa ditampilkan dalam satu halaman. Misalnya menampilkan menu pada LCD karakter 16×2 yang hanya bisa menampung 16 karakter setiap barisnya.

Salah satu menu yang sering digunakan adalah menu-menu makanan dan minuman pada penerapan restoran yang menggunakan sistem digital terkoneksi.

Kelebihan Menu I2C LCD Arduino ini adalah:

  1. Tampilan interaktif bergilir setiap 1 detik dan tampil 5 detik ketika hendak dipilih.
  2. Menggunakan keypad 4×4 sehingga lebih lega.

Video menu interaktif arduino:

Dalam perancangan berbasis arduino ini digunakan komponen berikut :

  1. Arduino Uno
  2. LCD 1602 + I2C
  3. Keypad 4×4

skema menu arduino LCD dan keypad:

koding/sketch menu keypad arduino:

#include <LiquidCrystal_I2C.h>
#include <Keypad.h>

const byte ROWS = 4; //four rows
const byte COLS = 4; //three columns
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
byte rowPins[ROWS] = {4, 5, 6, 7};
byte colPins[COLS] = {8, 9, 10, 11};

Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

LiquidCrystal_I2C lcd(0x3F, 16, 2);

struct daftarMenu {
  char strMenu[17];
  uint32_t harga;
  bool pilihan;
};

char menuUtama[][17] = {
  "0..9 - Pilih    ",
  "* - Pilih       ",
  "# - Batal       ",
  "A - Makanan     ",
  "B - Minuman     ",
  "C - Total/Pesan ",
  "D - Batal       ",
};
daftarMenu menuMakanan[] = {
  {"1 Nasi Goreng   ", 13000L, false},
  {"2 Mie Goreng    ", 8000L, false},
  {"3 Bihun Goreng  ", 8000L, false},
  {"4 Mie Rebus     ", 6000L, false},
  {"5 Gado-gado     ", 13000L, false},
  {"6 Soto Padang   ", 15000L, false},
  {"7 Sate Padang   ", 18000L, false},
};
daftarMenu menuMinuman[] = {
  {"1 Es Campur     ", 6000L, false},
  {"2 Es Tebak      ", 7500L, false},
  {"3 Es Kosong     ", 2000L, false},
  {"4 Jus Jeruk     ", 6000L, false},
  {"5 Jus Pokat     ", 6500L, false},
  {"6 Kopi          ", 4000L, false},
  {"7 Teh Panas     ", 4000L, false},
  {"8 Teh Telur     ", 8000L, false},
};

int8_t indexMenu = -1;
byte menuLevel = 0;
byte menuLevelSebelumnya = -1;

enum ModeMenu {
  modeMenuMakanan,
  modeMenuMinuman,
};
ModeMenu modeMenu;
#define standarWaktuTampil  1000L
#define LihatWaktuTampil    5000L
#define jumlahMakanan       sizeof(menuMakanan)/sizeof(menuMakanan[0])
#define jumlahMinuman       sizeof(menuMinuman)/sizeof(menuMinuman[0])

uint16_t waktuTampil;
unsigned long millisMulai;

void setup()
{
  Serial.begin(9600);
  Serial.println("Menu LCD Arduino dengan keypad");
  Serial.println("https://www.semesin.com/project/");

  lcd.begin ();
  lcd.backlight();
  //  tampilanDepan();
  millisMulai = millis();
  resetPilihan();
}

void loop()
{
  char key = keypad.getKey();

  if (key) {
    Serial.println(key);
    switch (key)
    {
      case 'A':
        menuLevel = 1;
        indexMenu = -1;
        modeMenu = modeMenuMakanan;
        break;
      case 'B':
        menuLevel = 1;
        indexMenu = -1;
        modeMenu = modeMenuMinuman;
        break;
      case 'C':
        menuLevel = 2;
        updateMenu();
        break;
      case 'D':
        resetPilihan();
        menuLevel = 0;
        indexMenu = -1;
        updateMenu();
        break;
      case '*':
        if (menuLevel == 2)
        {
          pesananMasuk();
        }
        else
        {
          if (waktuTampil == LihatWaktuTampil)
          {
            if (modeMenu == modeMenuMakanan)
            {
              menuMakanan[indexMenu].pilihan = true;
            }
            else if (modeMenu == modeMenuMinuman)
            {
              menuMinuman[indexMenu].pilihan = true;
            }
            updateMenu();
          }
          else
          {
            waktuTampil = LihatWaktuTampil;
            millisMulai = millis();
          }
        }
        break;
      case '#':
        if (menuLevel == 2)
        {
          menuLevel = 1;
        }
        else
        {
          if (waktuTampil == LihatWaktuTampil)
          {
            if (modeMenu == modeMenuMakanan)
            {
              menuMakanan[indexMenu].pilihan = false;
            }
            else if (modeMenu == modeMenuMinuman)
            {
              menuMinuman[indexMenu].pilihan = false;
            }
            updateMenu();
          }
          else
          {
            waktuTampil = LihatWaktuTampil;
            millisMulai = millis();
          }
        }
        break;
      default:
        indexMenu = key - '1';
        updateMenu();
        millisMulai = millis();
        waktuTampil = LihatWaktuTampil;
        break;
    }
  }

  if (millis() - millisMulai > waktuTampil)
  {
    millisMulai = millis();
    waktuTampil = standarWaktuTampil;
    indexMenu++;
    updateMenu();
    menuLevelSebelumnya = menuLevel;
  }
}
void pesananMasuk()
{
  //Aksi pesanan masuk
  lcd.setCursor(0, 0);
  lcd.println("  Terima Kasih  ");
  lcd.setCursor(0, 1);
  lcd.println("Silahkan tunggu ");
  delay(3000);
  Serial.println("Pesanan masuk!!!");
  menuLevel = 0;
  indexMenu = -1;
}
void updateMenu()
{
  if (menuLevel == 0)
  {
    if (indexMenu == sizeof(menuUtama) / sizeof(menuUtama[0]))
    {
      indexMenu = 0;
    }
    if (menuLevelSebelumnya != menuLevel)
    {
      lcd.clear();
      lcd.print("Selamat Datang");
    }
    tampilMenuUtama(indexMenu);
  }
  else if (menuLevel == 2)
  {
    lcd.setCursor(0, 0);
    lcd.print("Rp. ");
    formatStrHarga(totalPilihan());
    lcd.setCursor(0, 1);
    lcd.print("* Ya   # kembali");
  }
  else if (modeMenu == modeMenuMakanan)
  {
    if (indexMenu >= jumlahMakanan)
    {
      indexMenu = 0;
    }
    tampilMenuMakanan(indexMenu);
  }
  else if (modeMenu == modeMenuMinuman)
  {
    if (indexMenu >= jumlahMinuman)
    {
      indexMenu = 0;
    }
    tampilMenuMinuman(indexMenu);
  }
}
void resetPilihan()
{
  for (byte i = 0; i < jumlahMakanan; i++)
  {
    menuMakanan[i].pilihan = false;
  }
  for (byte i = 0; i < jumlahMinuman; i++)
  {
    menuMinuman[i].pilihan = false;
  }
}
uint32_t totalPilihan()
{
  uint32_t total = 0;
  for (byte i = 0; i < jumlahMakanan; i++)
  {
    if (menuMakanan[i].pilihan)
    {
      total += menuMakanan[i].harga;
    }
  }
  for (byte i = 0; i < jumlahMinuman; i++)
  {
    if (menuMinuman[i].pilihan)
    {
      total += menuMinuman[i].harga;
    }
  }
  return total;
}
void tampilMenuUtama(byte index)
{
  lcd.setCursor(0, 1);
  lcd.print(menuUtama[index]);
}
void tampilMenuMakanan(byte index)
{
  lcd.setCursor(0, 0);
  lcd.print(menuMakanan[index].strMenu);
  lcd.setCursor(0, 1);
  lcd.print("Rp. ");
  formatStrHarga(menuMakanan[index].harga);
  if (menuMakanan[index].pilihan)
  {
    lcd.setCursor(15, 1);
    lcd.print("*");
  }
}
void tampilMenuMinuman(byte index)
{
  lcd.setCursor(0, 0);
  lcd.print(menuMinuman[index].strMenu);
  lcd.setCursor(0, 1);
  lcd.print("Rp. ");
  formatStrHarga(menuMinuman[index].harga);
  if (menuMinuman[index].pilihan)
  {
    lcd.setCursor(15, 1);
    lcd.print("*");
  }
}
void formatStrHarga(uint32_t harga)
{
  String strHarga = String(harga);
  uint8_t panjangStr = strHarga.length();
  uint8_t offset = 3 - (panjangStr % 3);
  for (byte i = 0; i < strHarga.length(); i++)
  {
    lcd.print(strHarga[i]);
    if (!((strHarga.length() + i - offset + 1) % 3))
    {
      if (i != strHarga.length() - 1)
      {
        lcd.print('.');
      }
    }
  }
  for (byte i = 0; i < 16 - 5 - strHarga.length(); i++)
  {
    lcd.print(' ');
  }
}