이번 글에서는 Windows Mobile 휴대폰에서 기지국 정보를 구하는 방법을 소개합니다.
3G 네트워크에서는 기지국 하나가 커버할 수 있는 반경이 2-3km 정도 되기 때문에 기지국 정보만으로도 제한적인 위치 정보 기반 서비스를 할 수 있습니다. 특히 지하에서는 전파 전달이 잘 되지 않기 때문에 곳곳에 기지국을 설치해야 하죠. 때문에 기지국 ID를 알아낼 수 있다면 사용자가 현재 어느 지하철 역에 있는지, 어느 쇼핑몰 안에 있는지, 어느 대학교 안에 있는지를 구분해서 서비스를 제공할 수 있을 것입니다. 지하철에서는 배차 정보나 행선지 찾기 서비스에 응용할 수도 있고, 쇼핑몰에서는 약도나 상품 검색 서비스를 제공할 수 있을 것입니다.
C#으로 Cell ID를 가져오는 방법은 생각보다 복잡합니다. P/Invoke 라는 기술을 사용해야 하는데요. C 언어와 32비트 CPU에 대한 이해가 없으면 헤맬 가능성이 높습니다.
스크린 샷은 다음과 같습니다. 심플하게 MCC/MNC, LAC, CellId 를 찍어 주고 있습니다.
MCC는 국가 코드입니다. 450번이 우리나라인가봐요.
MNC는 통신망 코드입니다. 5번은 SKT 입니다. 전체 코드는
에 나열되어 있습니다.
LAC하고 CellId가 지역/기지국 정보입니다.
기지국 정보를 얻기 위해서는 RIL 을 사용해야 합니다. Radio Interface Layer 의 약자입니다. 이건 C#으로 제공되지 않는 API 라 P/Invoke를 사용해야 합니다.
[DllImport("ril.dll")]
static extern int RIL_Initialize(uint index, ResultCallback resultCallback, NotifyCallback notifyCallback, uint notificationFlags, IntPtr userParam, out IntPtr ril);
[DllImport("ril.dll")]
static extern int RIL_Deinitialize(IntPtr ril);
[DllImport("ril.dll")]
static extern int RIL_GetCellTowerInfo(IntPtr ril);
반환 값은 HRESULT 코드입니다. COM에서 유래된 코드죠. 이걸 그대로 반환하는 것보다는 한번 wrapping 해서 C# exception을 띄워 주는 것이 낫습니다. 문화가 바뀌었으니까요.
static IntPtr Initialize(ResultCallback resultCallback, IntPtr userParam)
{
IntPtr ril = IntPtr.Zero;
int hr = RIL_Initialize(1, resultCallback, null, 0, userParam, out ril);
if (hr < 0)
throw new RilException(hr);
if (ril == IntPtr.Zero)
throw new Exception("RIL_Initialize gave null pointer");
return ril;
}
static void Deinitialize(IntPtr ril)
{
int hr = RIL_Deinitialize(ril);
if (hr < 0)
throw new RilException(hr);
}
static void GetCellTowerInfo(IntPtr ril)
{
int hr = RIL_GetCellTowerInfo(ril);
if (hr < 0)
throw new RilException(hr);
}
리턴값 검사해서 에러가 있으면 RilException을 띄웁니다. 별거 아니고 그냥 HRESULT 값을 세팅하는 함수입니다.
이어서 notify를 받기 위한 delegation 과 struct를 선언해야겠죠. 기지국 정보를 받아오는 struct는 구조가 간단해서 쉽게 됩니다.
public delegate void ResultCallback(
uint resultCode,
int hrCommandId,
IntPtr pData,
uint cbData,
IntPtr userParam
);
public delegate void NotifyCallback(
uint notifyCode,
IntPtr pData,
uint cbData,
IntPtr userParam
);
[StructLayout(LayoutKind.Sequential)]
public class CellTowerInfo
{
public uint cbSize;
public uint dwParams;
public uint dwMobileCountryCode;
public uint dwMobileNetworkCode;
public uint dwLocationAreaCode;
public uint dwCellID;
public uint dwBaseStationID;
public uint dwBroadcastControlChannel;
public uint dwRxLevel;
public uint dwRxLevelFull;
public uint dwRxLevelSub;
public uint dwRxQuality;
public uint dwRxQualityFull;
public uint dwRxQualitySub;
public uint dwIdleTimeSlot;
public uint dwTimingAdvance;
public uint dwGPRSCellID;
public uint dwGPRSBaseStationID;
public uint dwNumBCCH;
}
이어서 기지국 정보를 가져오기 위한 코드를 살펴 봅니다. exception handling 때문에 길어 보이지만, 실제로 뭔가를 수행하는 부분은 5-6줄 밖에 되지 않습니다. 핵심 부분은 빨간 색으로 표시해놨습니다.
public static CellTowerInfo GetCellTowerInfo()
{
IntPtr ril = IntPtr.Zero;
try
{
using(AutoResetEvent wait = new AutoResetEvent(false))
{
ril = Initialize(GetCellTowerInfoResult, wait.Handle);
GetCellTowerInfo(ril);
// wait for callback.
wait.WaitOne();
return cellTowerInfo;
}
// Deinitialize() will be called in finally block.
}
catch (System.Exception)
{
// toss the exception
throw;
}
finally
{
try
{
if (ril != IntPtr.Zero)
Deinitialize(ril);
}
catch (System.Exception)
{
// suppress all exceptions here.
}
}
}
// This will be used as cache.
// Don't call GetCellTowerInfo() too frequently.
static CellTowerInfo cellTowerInfo = null;
// callback.
static void GetCellTowerInfoResult(uint resultCode, int hrCommandId, IntPtr pData, uint cbData, IntPtr userParam)
{
CellTowerInfo info = new CellTowerInfo();
Marshal.PtrToStructure(pData, info);
cellTowerInfo = info;
// tell main thread.
AutoResetEvent theEvent = new AutoResetEvent(false);
theEvent.Handle = userParam;
theEvent.Set();
}
RIL 인터페이스는 API를 호출한다고 바로 결과를 알려주지 않습니다. callback을 통해 asynchronous 하게 통지를 해주는데요. 하지만 함수를 호출해서 사용하는 입장에서는 synchronous 한게 편리합니다. 그걸 처리해주기 위해서 Event 객체를 하나 생성합니다. 나중에 callback에서 이벤트를 raise 해주죠.
이 코드에는 버그가 있습니다.
wait.WaitOne() 을 timeout 없이 무작정 기다리는 버그가 있는데요. 모종의 이유 때문에 callback 함수에서 exception이 발생하게 되면, event가 set 되지 않아 program이 hang 될 것입니다. 그리고 그런 상황은 생각보다 자주 일어날걸요. pData가 null이기만 해도 뻑나니까요. 아마 resultCode 에 따라 null 인 상황이 일어날 수 있을겁니다.
저는 위의 코드 전체를 Ril 이라는 static class 에 집어 넣었습니다. 그리고 Ril 클래스를 사용하는 부분의 코드는 아래와 같죠. 이제 정보를 가져오는 부분은 딱 한 줄이면 됩니다. 참 쉽죠?
Ril.CellTowerInfo info = Ril.GetCellTowerInfo();
if (info != null)
{
labelMCC.Text = String.Format("MCC/MNC = {0} / {1}", info.dwMobileCountryCode, info.dwMobileNetworkCode);
labelLAC.Text = String.Format("LAC = {0}", info.dwLocationAreaCode);
labelCellId.Text = String.Format("CellId = {0}", info.dwCellID);
labelUpdate.Text = DateTime.Now.ToString("yyyy.MM.dd HH:mm:ss");
}