Membuat Service SMS Gateway untuk Engine Gammu
Pasti udah pada tau kan apa itu Gammu ? Klo enggak ya keterlaluan he he. Itu loh engine yang biasa digunakan untuk mengirim dan menerima sms dan biasanya sering digunakan untuk membuat sms gateway.
Di postingan ini saya tidak akan membahas bagaimana cara instalasi Gammu, konfigurasi dan database yang digunakan. Jadi saya mengasumsikan bahwa service Gammu Anda (GammuSMSD) sudah berjalan dengan baik, dengan kata lain jika ada sms masuk ke nomor yang Anda gunakan sebagai sms center, maka service Gammu otomatis akan meng-INSERT-kan sms tersebut ke tabel inbox.
Struktur Tabel GammuPermalink
Sebelum kita lanjut bagaimana membuat windows service untuk engine Gammu ada baiknya kita melihat dulu struktur tabel Gammu secara menyeluruh.
Dari keenam tabel di atas, kita cukup fokus di 3 tabel utama yaitu inbox
, outbox
dan outbox_multipart
. Tabel inbox otomatis terisi, jika ada sama masuk tentunya dengan syarat service Gammu Anda (GammuSMSD) sudah berjalan dengan baik. Di tabel inbox ini juga sudah ada field dengan nama Processed yang bisa kita gunakan sebagai flag/penanda sms yang sudah diproses.
Nah jika ada sms yang mau dikirim, tinggal kita INSERT-kan datanya secara manual ke tabel outbox. Dan jika smsnya panjang (lebih dari 160 karakter), smsnya harus kita pecah menjadi 2, 3, dst sesuai dengan jumlah karakter yang mau dikirim. Setelah dipecah, sms pertama masuk ke tabel outbox, dan sisanya masuk ke tabel outbox_multipart.
Untuk informasi lebih lanjut tentang struktur database Gammu bisa Anda cek di sini.
Konfigurasi GammuPermalink
Untuk konfigurasi gammu ada dua file yang harus kita edit nilainya yaitu file smsdrc
dan gammurc
. File ini biasanya ada di folder Instalasi Gammu\share\doc\gammu\examples\config
trus kita copykan ke folder ` Instalasi Gammu\bin`. Berikut adalah contoh konfigurasi gammu yang saya gunakan.
# konfigurasi file gammurc
[gammu]
device = com3:
connection = at115200
# konfigurasi file smsdrc
[gammu]
device = com3:
connection = at115200
[smsd]
# SMSD service to use, one of FILES, MYSQL, PGSQL, DBI
service = SQL
# File (or stderr, syslog, eventlog) where information will be logged
logfile = C:\gammu\log\gammu.log
# Amount of information being logged, each bit mean one level
debuglevel = 255
# How many seconds should SMSD wait after there is no message in outbox before scanning it again. Default is 30.
commtimeout = 1
# Shows how many seconds SMSD should wait for network answer during sending sms. If nothing happen during this time, sms will be resent. Default is 30.
sendtimeout = 10
# Phone communication settings
checksecurity = 0
CheckBattery = 0
# Database backends congfiguration
user = root
password = rahasia
host = gammuodbc
# pc can also contain port or socket path after colon (eg. localhost:/path/to/socket)
database = db_gammu
# DBI configuration
driver = odbc
# Possible values: mysql, pgsql, sqlite, mssql (Microsoft SQL Server), access (Microsoft Access), oracle
# ref: https://wammu.eu/docs/manual/smsd/config.html#gammu-smsdrc
sql = mysql
Coba perhatikan konfigurasi pada file smsdrc
di atas terutama di bagian host
, nilainya kita isikan dengan nama ODBC yang terhubung ke database gammu, dan untuk membuat ODBCnya kita gunakan fasilitas ODBC Data Source.
Untuk menambahkan ODBC di atas, sebelumnya kita harus menginstall terlebih dulu MySQL Connector/ODBC.
Environment Testing dan Development Tool yang DigunakanPermalink
- Windows 7 Pro 64 bit
- Visual Studio Community 2013 – untuk bahasa yang digunakan C#
- Tipe aplikasi – Windows Service
- Micro ORM Dapper.NET
- Gammu-1.38.4-Windows-64bit
- Database SQLite – untuk menyimpan data siswa, mata pelajaran dan nilai
- Database MySQL v5.5.45 – untuk menyimpan data gammu
- MySQL Connector/ODBC v5.3.9
- Modem Wavecom M1306B
Kok menggunakan Connector/ODBC ? Iya karena Gammu v1.38.4 sudah mendukung koneksi menggunakan ODBC. Jadi selain MySQL, dengan menggunakan ODBC dukungan databasenya jadi lebih banyak seperti PostgreSQL, SQLite, Oracle, MS SQL Server dan MS Access.
Contoh KasusPermalink
Untuk contoh kasusnya kita akan membuat service sms gateway untuk request data yang berhubungan dengan siswa seperti data pribadi, mata pelajaran dan nilai. Service sms gateway ini akan terhubung ke dua database yaitu database gammu yang tersimpan di MySQL dan database nilai yang tersimpan di SQLite. Sms gateway ini mempunyai beberapa format request data yaitu:
CEKMP # request data mata pelajaran
CEKSISWA#NIS # request data siswa
CEKNILAI#NIS#<KODE MP> # request data nilai, kode mp optional
Untuk data siswa, mata pelajaran dan nilainya disimpan menggunakan database SQLite.
Project SMS GatewayPermalink
Project SMS Gateway ini dibangun dengan menggunakan konsep separation of concern atau pemisahan kode program berdasarkan fungsinya. Semua kode untuk akses database harus dipisahkan dengan kode untuk pengaturan user inteface. Hal ini memungkinkan kode akses database yang dibuat untuk aplikasi desktop, dengan mudah digunakan untuk aplikasi lainnya seperti web, console atau windows service. Selain itu penerapan konsep separation of concern secara disiplin, dapat menghasilkan kode program yang dapat dites secara otomatis menggunakan tool Unit Testing.
Untuk urusan kode akses database, project ini menggunakan pattern/pola Repository Pattern yang berisi semua kode untuk mengakses database. Semua kode yang sepesifik terhadap implementasi akses database berhenti di sini, lapisan lebih atas tidak boleh tahu bagaimana akses database diterapkan, apakah menggunakan ADO.NET murni (raw ADO.NET) atau menggunakan tool ORM/Micro ORM seperti Dapper.NET, Entity Framework atau NHibernate. Lapisan lainya hanya perlu tahu fungsionalitas dari suatu method di dalam class Repository, tidak perlu tahu bagimana method tersebut diimplementasikan.
Idealnya secara logic, arsitektur aplikasi yang kita gunakan seperti berikut:
Tetapi untuk menyederhanakan pembahasan business logic layer bisa kita gabung ke presentation layer, sehingga arsitekturnya menjadi seperti berikut:
Nah untuk contoh project SMS Gateway ini menggunakan arsitektur yang kedua.
Yang pertama adalah project WindowsServiceGammu.Model
. Project ini bertipe Class Library
yang berisi class model/entity yang merupakan representasi dari sebuah table. Jadi kalo kita ingin menerapkan konsep OOP dalam pemrograman database maka class-class model/entity ini wajib kita gunakan.
Berikutnya adalah project WindowsServiceGammu.Repository
. Project ini bertipe Class Library
yang berisi class-class repository yang bertugas untuk menangani operasi CRUD. Biasanya untuk masing-masing class model/entity kita buatkan class repositorynya.
Pada gambar di atas saya menggunakan dua class context yaitu MySqlContext dan SQLiteContext. Class context adalah class yang bertanggung jawab untuk berinteraksi secara langsung dengan database. Jadi class contextlah yang bertugas untuk membuat koneksi, menjalankan perintah sql seperti insert, update, delete dan select atau objek database seperti store procedure dan function. Selain class context juga ada class-class repository yang bertugas untuk menangani operasi CRUD. Berikut adalah contoh class repository untuk class SiswaRepository dan GammuRepository.
using Dapper; | |
using log4net; | |
using WindowsServiceGammu.Model; | |
namespace WindowsServiceGammu.Repository | |
{ | |
public interface ISiswaRepository | |
{ | |
Siswa GetByNIS(string nis); | |
} | |
public class SiswaRepository : ISiswaRepository | |
{ | |
private IDapperContext _context; | |
private ILog _log; | |
public SiswaRepository(IDapperContext context, ILog log) | |
{ | |
this._context = context; | |
this._log = log; | |
} | |
public Siswa GetByNIS(string nis) | |
{ | |
Siswa siswa = null; | |
try | |
{ | |
var sql = @"select nis, nama | |
from siswa | |
where nis = @nis"; | |
siswa = _context.db.QuerySingleOrDefault<Siswa>(sql, new { nis }); | |
} | |
catch (Exception ex) | |
{ | |
_log.Error("Error:", ex); | |
} | |
return siswa; | |
} | |
} | |
} |
using Dapper; | |
using log4net; | |
using WindowsServiceGammu.Model.Gammu; | |
namespace WindowsServiceGammu.Repository | |
{ | |
public interface IGammuRepository | |
{ | |
/// <summary> | |
/// Method untuk membaca data sms di tabel inbox yang belum diproses | |
/// </summary> | |
/// <returns></returns> | |
IList<Inbox> ReadInbox(); | |
/// <summary> | |
/// Method untuk mengupdate status inbox menjadi sudah diproses | |
/// </summary> | |
/// <param name="inboxId"></param> | |
/// <returns></returns> | |
int UpdateInbox(int inboxId); | |
/// <summary> | |
/// Method untuk menyimpan data sms yang akan dikirim ke tabel outbox | |
/// </summary> | |
/// <param name="obj"></param> | |
/// <returns></returns> | |
int SaveOutbox(Outbox obj); | |
/// <summary> | |
/// Method untuk menyimpan data sms ke 2, 3, dst ke tabel outbox_multipart, jika data sms lebih dari 160 karakter | |
/// </summary> | |
/// <param name="obj"></param> | |
/// <returns></returns> | |
int SaveOutboxMultipart(OutboxMultipart obj); | |
} | |
public class GammuRepository : IGammuRepository | |
{ | |
private IDapperContext _context; | |
private ILog _log; | |
public GammuRepository(IDapperContext context, ILog log) | |
{ | |
this._context = context; | |
this._log = log; | |
} | |
public IList<Inbox> ReadInbox() | |
{ | |
IList<Inbox> listOfSMS = new List<Inbox>(); | |
try | |
{ | |
var sql = @"SELECT `ID`, `UDH`, `SenderNumber`, `TextDecoded`, `ReceivingDateTime` | |
FROM inbox | |
WHERE Processed = 'false' | |
ORDER BY ReceivingDateTime"; | |
listOfSMS = _context.db.Query<Inbox>(sql) | |
.ToList(); | |
} | |
catch (Exception ex) | |
{ | |
_log.Error("Error:", ex); | |
} | |
return listOfSMS; | |
} | |
public int UpdateInbox(int inboxId) | |
{ | |
var result = 0; | |
try | |
{ | |
var sql = @"UPDATE inbox SET Processed = 'true' | |
WHERE id = @inboxId"; | |
result = _context.db.Execute(sql, new { inboxId }); | |
} | |
catch (Exception ex) | |
{ | |
_log.Error("Error:", ex); | |
} | |
return result; | |
} | |
public int SaveOutbox(Outbox obj) | |
{ | |
var result = 0; | |
try | |
{ | |
var sql = @"INSERT INTO outbox (DestinationNumber, UDH, TextDecoded, MultiPart, CreatorID) | |
VALUES (@DestinationNumber, @UDH, @TextDecoded, @MultiPart, 'Gammu')"; | |
result = _context.db.Execute(sql, obj); | |
if (result > 0) | |
{ | |
sql = @"SELECT CONVERT(LAST_INSERT_ID(), SIGNED INTEGER) AS ID"; | |
obj.Id = _context.db.QuerySingleOrDefault<int>(sql); | |
} | |
} | |
catch (Exception ex) | |
{ | |
_log.Error("Error:", ex); | |
} | |
return result; | |
} | |
public int SaveOutboxMultipart(OutboxMultipart obj) | |
{ | |
var result = 0; | |
try | |
{ | |
var sql = @"INSERT INTO outbox_multipart(ID, UDH, TextDecoded, SequencePosition) | |
VALUES (@ID, @UDH, @TextDecoded, SequencePosition)"; | |
result = _context.db.Execute(sql, obj); | |
} | |
catch (Exception ex) | |
{ | |
_log.Error("Error:", ex); | |
} | |
return result; | |
} | |
} | |
} |
Untuk class repository lainnya bisa Anda cek di sini.
Dan yang terakhir adalah project WindowsServiceGammu.Service
yang bertipe Windows Service
. Project ini tidak bisa dijalankan secara langsung seperti halnya project dengan tipe Console atau Windows Form, tapi harus didaftarkan terlebih dahulu ke Windows Service.
Project ini hanya memanggil class-class model atau repository yang sudah ada.
Ada dua class utama yang digunakan oleh project ini yang pertama yaitu class MainService
yang merupakan turunan dari class ServiceBase. Class ServiceBase adalah class bawaan .NET Framework yang merupakan class dasar agar sebuah aplikasi bisa di register/menjadi bagian dari aplikasi service (Windows Service).
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Data; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.ServiceProcess; | |
using System.Text; | |
using log4net; | |
namespace WindowsServiceGammu.Service | |
{ | |
public partial class MainService : ServiceBase | |
{ | |
private readonly List<TaskBase> _listOfTask; | |
private readonly ILog _log; | |
private const int RefreshInterval = 1000; // In milliseconds | |
public MainService() | |
{ | |
InitializeComponent(); | |
_log = Program.log; | |
// Add in this list the tasks to run periodically. | |
// Tasks frequencies are set in the corresponding classes. | |
_listOfTask = new List<TaskBase> | |
{ | |
new SMSGatewayTask(RefreshInterval, _log) | |
}; | |
} | |
protected override void OnStart(string[] args) | |
{ | |
try | |
{ | |
_log.Info("Services started ..."); | |
_listOfTask.ForEach(t => t.StartService()); | |
} | |
catch (Exception ex) | |
{ | |
_log.Error("Error:", ex); | |
Stop(); | |
} | |
} | |
protected override void OnStop() | |
{ | |
_log.Info("Services stoped ..."); | |
_listOfTask.ForEach(t => t.StopService()); | |
} | |
} | |
} |
Berikutnya adalah class SMSGatewayTask
yang bertugas untuk membaca tabel inbox-nya gammu, memvalidasi perintah yang ada dan mengenerate pesan balasan dan kemudian menyimpannya ke tabel outbox. Semua aktivitas membaca dan menyimpan kembali ke database ini dibantu oleh class-class repository.
using log4net; | |
using WindowsServiceGammu.Model; | |
using WindowsServiceGammu.Model.Gammu; | |
using WindowsServiceGammu.Repository; | |
namespace WindowsServiceGammu.Service | |
{ | |
public class SMSGatewayTask : TaskBase | |
{ | |
private ILog _log; | |
public SMSGatewayTask(int refreshInterval, ILog log) | |
: base(refreshInterval) // In milliseconds | |
{ | |
_log = log; | |
} | |
protected override void ExecTask() | |
{ | |
using (IDapperContext mysqlContext = new MySqlContext()) | |
{ | |
IGammuRepository gammuRepo = new GammuRepository(mysqlContext, _log); | |
var listOfInbox = gammuRepo.ReadInbox(); | |
foreach (var inbox in listOfInbox) | |
{ | |
var phoneNumber = inbox.SenderNumber; | |
if (phoneNumber.Substring(0, 3) == "+62") | |
{ | |
var keyword = inbox.TextDecoded; | |
var prefix = keyword; | |
var msg = string.Empty; | |
if (keyword.IndexOf("#") >= 0) // karakter # -> separator keyword | |
{ | |
var nis = string.Empty; | |
var kodeMP = string.Empty; | |
var arrKeyword = keyword.Split('#'); | |
prefix = arrKeyword[0]; | |
switch (prefix.ToUpper()) | |
{ | |
case "CEKSISWA": // FORMAT PERINTAH: CEKSISWA#NIS | |
nis = arrKeyword[1]; // nis di ambil dari parameter pertama | |
msg = GetBalasanCekSiswa(nis); | |
break; | |
case "CEKNILAI": // FORMAT PERINTAH: CEKNILAI#NIS#<OPTIONAL KODE MP> | |
nis = arrKeyword[1]; // nis di ambil dari parameter pertama | |
kodeMP = arrKeyword.Count() > 2 ? arrKeyword[2] : string.Empty; | |
msg = GetBalasanCekNilai(nis, kodeMP); | |
break; | |
default: | |
break; | |
} | |
} | |
else | |
{ | |
// FORMAT PERINTAH: CEKMP | |
if (keyword.ToUpper() == "CEKMP") | |
{ | |
msg = GetBalasanCekMP(); | |
} | |
else // keyword tidak valid | |
{ | |
msg = string.Format("Keyword {0} tidak terdaftar", keyword.ToUpper()); | |
} | |
} | |
SaveOutbox(msg, inbox, gammuRepo); | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// Method untuk menyimpan pesan yang akan dikirim ke tabel outbox | |
/// </summary> | |
/// <param name="msg"></param> | |
/// <param name="inbox"></param> | |
/// <param name="gammuRepo"></param> | |
private void SaveOutbox(string msg, Inbox inbox, IGammuRepository gammuRepo) | |
{ | |
var result = 0; | |
// insert ke tabel outbox | |
var jumlahSMS = (int)Math.Ceiling((double)msg.Length / 160); | |
if (jumlahSMS > 1) // balasan sms > 160 karakter, sms dipecah sebelum dikirim | |
{ | |
var listSms = msg.SplitByLength(153) | |
.ToList(); | |
var smsKe = 1; | |
var outboxID = 0; | |
foreach (var sms in listSms) | |
{ | |
var udh = inbox.UDH; | |
if (udh.Length == 0) | |
{ | |
udh = string.Format("050003A7{0:00}{1:00}", listSms.Count, smsKe); | |
} | |
else | |
{ | |
udh = inbox.UDH.Substring(0, inbox.UDH.Length - 4); | |
udh = string.Format("{0}{1:00}{2:00}", udh, listSms.Count, smsKe); | |
} | |
if (smsKe == 1) | |
{ | |
var outbox = new Outbox | |
{ | |
DestinationNumber = inbox.SenderNumber, | |
UDH = udh, | |
TextDecoded = sms, | |
MultiPart = "true" | |
}; | |
result = gammuRepo.SaveOutbox(outbox); | |
if (result > 0) | |
{ | |
outboxID = outbox.Id; | |
} | |
} | |
else // sms ke 2, 3, dst, simpan ke tabel outbox_multipart | |
{ | |
var outboxMultipart = new OutboxMultipart | |
{ | |
Id = outboxID, | |
UDH = udh, | |
TextDecoded = sms, | |
SequencePosition = smsKe | |
}; | |
result = gammuRepo.SaveOutboxMultipart(outboxMultipart); | |
} | |
smsKe++; | |
} | |
} | |
else // balasan sms <= 160 karakter | |
{ | |
var outbox = new Outbox | |
{ | |
DestinationNumber = inbox.SenderNumber, | |
UDH = string.Empty, | |
TextDecoded = msg, | |
MultiPart = "false" | |
}; | |
result = gammuRepo.SaveOutbox(outbox); | |
} | |
if (result > 0) | |
{ | |
// update status pesan di inbox menjadi sudah diproses | |
result = gammuRepo.UpdateInbox(inbox.Id); | |
} | |
} | |
/// <summary> | |
/// Method untuk mengenerate pesan balasan untuk keyword: CEKSISWA#NIS | |
/// </summary> | |
/// <param name="nis"></param> | |
/// <returns></returns> | |
private string GetBalasanCekSiswa(string nis) | |
{ | |
var msg = string.Empty; | |
using (IDapperContext sqliteContext = new SQLiteContext()) | |
{ | |
ISiswaRepository siswaRepo = new SiswaRepository(sqliteContext, _log); | |
var siswa = siswaRepo.GetByNIS(nis); | |
if (siswa == null) | |
{ | |
msg = string.Format("NIS: {0} tidak ditemukan", nis); | |
} | |
else | |
{ | |
msg = string.Format("NIS: {0}\nNAMA: {1}", siswa.nis, siswa.nama); | |
} | |
} | |
return msg; | |
} | |
/// <summary> | |
/// Method untuk mengenerate pesan balasan untuk keyword: CEKNILAI#NIS#<OPTIONAL KODE MP> | |
/// </summary> | |
/// <param name="nis"></param> | |
/// <param name="kodeMP"></param> | |
/// <returns></returns> | |
private string GetBalasanCekNilai(string nis, string kodeMP) | |
{ | |
var msg = string.Empty; | |
IList<Nilai> listOfNilai = new List<Nilai>(); | |
using (IDapperContext sqliteContext = new SQLiteContext()) | |
{ | |
ISiswaRepository siswaRepo = new SiswaRepository(sqliteContext, _log); | |
var siswa = siswaRepo.GetByNIS(nis); | |
if (siswa == null) | |
{ | |
msg = string.Format("NIS: {0} tidak ditemukan", nis); | |
} | |
else | |
{ | |
INilaiRepository nilaiRepo = new NilaiRepository(sqliteContext, _log); | |
if (nis.Length > 0 && kodeMP.Length > 0) | |
{ | |
var nilai = nilaiRepo.GetByNIS(nis, kodeMP); | |
listOfNilai.Add(nilai); | |
} | |
else | |
{ | |
listOfNilai = nilaiRepo.GetByNIS(nis); | |
} | |
msg = string.Format("NIS: {0}\nNAMA: {1}\n", siswa.nis, siswa.nama); | |
msg += "Nilai:\n"; | |
foreach (var nilai in listOfNilai) | |
{ | |
msg += string.Format("{0}: {1}\n", nilai.kode, nilai.nilai); | |
} | |
} | |
} | |
return msg; | |
} | |
/// <summary> | |
/// Method untuk mengenerate pesan balasan untuk keyword: CEKMP | |
/// </summary> | |
/// <returns></returns> | |
private string GetBalasanCekMP() | |
{ | |
var msg = string.Empty; | |
using (IDapperContext sqliteContext = new SQLiteContext()) | |
{ | |
IMataPelajaranRepository mataPelajaranRepo = new MataPelajaranRepository(sqliteContext, _log); | |
var listOfMataPelajaran = mataPelajaranRepo.GetAll(); | |
msg = string.Empty; | |
msg = "kode mata pelajaran:\n"; | |
foreach (var mataPelajaran in listOfMataPelajaran) | |
{ | |
msg += string.Format("{0}: {1}\n", mataPelajaran.kode, mataPelajaran.deskripsi); | |
} | |
} | |
return msg; | |
} | |
} | |
} |
Instalasi Windows ServicePermalink
Seperti yang sudah saya jelaskan di atas, project dengan tipe Windows Service tidak bisa dijalankan secara langsung seperti halnya project dengan tipe Console atau Windows Form, tapi harus didaftarkan terlebih dahulu ke Windows Service. Untuk mendaftarkannya kita gunakan tool bawaan .NET Framework yaitu InstallUtil.exe.
# install service
%WINDIR%\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe WindowsServiceGammu.Service.exe
# uninstall service
%WINDIR%\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe WindowsServiceGammu.Service.exe /u
Jika berhasil seharusnya service sms gateway yang sudah kita buat akan terdaftar di Windows Service.
Selamat MENCOBA
Comments